Compare commits
10 Commits
173cee75d5
...
v1.0.26
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43e33eb991 | ||
|
|
7fcc64f6e3 | ||
|
|
9d6829eceb | ||
|
|
4838bcef4b | ||
|
|
0fcf85a081 | ||
|
|
62fcc4bfce | ||
|
|
eef5210c99 | ||
|
|
402cbf90eb | ||
| decc7f9564 | |||
|
|
e552e22de9 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"expo": {
|
"expo": {
|
||||||
"name": "Hey Mama",
|
"name": "Dear Mama",
|
||||||
"slug": "client",
|
"slug": "client",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 43 KiB |
@@ -3,7 +3,7 @@
|
|||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 70;
|
objectVersion = 56;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||||
1A1DE01D4133812B2E2BA692 /* libPods-client.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E3328F0E595C1F4A244DF238 /* libPods-client.a */; };
|
1A1DE01D4133812B2E2BA692 /* libPods-client.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E3328F0E595C1F4A244DF238 /* libPods-client.a */; };
|
||||||
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
|
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
|
||||||
A1B2C3D4E5F60718293A4B5C /* 情绪小组件/EmotionWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60718293A4B5B /* 情绪小组件/EmotionWidget.swift */; };
|
A1B2C3D4E5F60718293A4B5C /* EmotionWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60718293A4B5B /* EmotionWidget.swift */; };
|
||||||
A8C1D2E3F4A5B6C7D8E9F0A2 /* AppGroupStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C1D2E3F4A5B6C7D8E9F0A1 /* AppGroupStorage.swift */; };
|
A8C1D2E3F4A5B6C7D8E9F0A2 /* AppGroupStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C1D2E3F4A5B6C7D8E9F0A1 /* AppGroupStorage.swift */; };
|
||||||
A8C1D2E3F4A5B6C7D8E9F0A3 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EB3DAF802F2A4B8D00450593 /* WidgetKit.framework */; };
|
A8C1D2E3F4A5B6C7D8E9F0A3 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EB3DAF802F2A4B8D00450593 /* WidgetKit.framework */; };
|
||||||
A8C1D2E3F4A5B6C7D8E9F0B2 /* AppGroupStorageBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = A8C1D2E3F4A5B6C7D8E9F0B1 /* AppGroupStorageBridge.m */; };
|
A8C1D2E3F4A5B6C7D8E9F0B2 /* AppGroupStorageBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = A8C1D2E3F4A5B6C7D8E9F0B1 /* AppGroupStorageBridge.m */; };
|
||||||
@@ -49,12 +49,12 @@
|
|||||||
/* End PBXCopyFilesBuildPhase section */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
13B07F961A680F5B00A75B9A /* HeyMama.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HeyMama.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
13B07F961A680F5B00A75B9A /* DearMama.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DearMama.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = client/Images.xcassets; sourceTree = "<group>"; };
|
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = client/Images.xcassets; sourceTree = "<group>"; };
|
||||||
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = client/Info.plist; sourceTree = "<group>"; };
|
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = client/Info.plist; sourceTree = "<group>"; };
|
||||||
3C76CA16D0801CBF0D731C7C /* Pods-client.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-client.release.xcconfig"; path = "Target Support Files/Pods-client/Pods-client.release.xcconfig"; sourceTree = "<group>"; };
|
3C76CA16D0801CBF0D731C7C /* Pods-client.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-client.release.xcconfig"; path = "Target Support Files/Pods-client/Pods-client.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
75F52ADE07CAE9D9736D7671 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = client/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
75F52ADE07CAE9D9736D7671 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = client/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||||
A1B2C3D4E5F60718293A4B5B /* 情绪小组件/EmotionWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "情绪小组件/EmotionWidget.swift"; sourceTree = "<group>"; };
|
A1B2C3D4E5F60718293A4B5B /* EmotionWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "情绪小组件/EmotionWidget.swift"; sourceTree = "<group>"; };
|
||||||
A8C1D2E3F4A5B6C7D8E9F0A1 /* AppGroupStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppGroupStorage.swift; path = client/AppGroupStorage.swift; sourceTree = "<group>"; };
|
A8C1D2E3F4A5B6C7D8E9F0A1 /* AppGroupStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppGroupStorage.swift; path = client/AppGroupStorage.swift; sourceTree = "<group>"; };
|
||||||
A8C1D2E3F4A5B6C7D8E9F0B1 /* AppGroupStorageBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = AppGroupStorageBridge.m; path = client/AppGroupStorageBridge.m; sourceTree = "<group>"; };
|
A8C1D2E3F4A5B6C7D8E9F0B1 /* AppGroupStorageBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = AppGroupStorageBridge.m; path = client/AppGroupStorageBridge.m; sourceTree = "<group>"; };
|
||||||
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = client/SplashScreen.storyboard; sourceTree = "<group>"; };
|
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = client/SplashScreen.storyboard; sourceTree = "<group>"; };
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
EB3DAF952F2A4B8F00450593 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
|
EB3DAF952F2A4B8F00450593 /* Exceptions for "情绪小组件" folder in "情绪小组件Extension" target */ = {
|
||||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||||
membershipExceptions = (
|
membershipExceptions = (
|
||||||
EmotionWidget.swift,
|
EmotionWidget.swift,
|
||||||
@@ -85,7 +85,18 @@
|
|||||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
EB3DAF842F2A4B8E00450593 /* 情绪小组件 */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (EB3DAF952F2A4B8F00450593 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = "情绪小组件"; sourceTree = "<group>"; };
|
EB3DAF842F2A4B8E00450593 /* 情绪小组件 */ = {
|
||||||
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
|
exceptions = (
|
||||||
|
EB3DAF952F2A4B8F00450593 /* Exceptions for "情绪小组件" folder in "情绪小组件Extension" target */,
|
||||||
|
);
|
||||||
|
explicitFileTypes = {
|
||||||
|
};
|
||||||
|
explicitFolders = (
|
||||||
|
);
|
||||||
|
path = "情绪小组件";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -175,7 +186,7 @@
|
|||||||
83CBBA001A601CBA00E9B192 /* Products */ = {
|
83CBBA001A601CBA00E9B192 /* Products */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
13B07F961A680F5B00A75B9A /* HeyMama.app */,
|
13B07F961A680F5B00A75B9A /* DearMama.app */,
|
||||||
EB3DAF7F2F2A4B8D00450593 /* 情绪小组件Extension.appex */,
|
EB3DAF7F2F2A4B8D00450593 /* 情绪小组件Extension.appex */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
@@ -202,7 +213,7 @@
|
|||||||
EB3DAFD42F2A5FC100450593 /* Recovered References */ = {
|
EB3DAFD42F2A5FC100450593 /* Recovered References */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
A1B2C3D4E5F60718293A4B5B /* 情绪小组件/EmotionWidget.swift */,
|
A1B2C3D4E5F60718293A4B5B /* EmotionWidget.swift */,
|
||||||
);
|
);
|
||||||
name = "Recovered References";
|
name = "Recovered References";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -239,7 +250,7 @@
|
|||||||
);
|
);
|
||||||
name = client;
|
name = client;
|
||||||
productName = client;
|
productName = client;
|
||||||
productReference = 13B07F961A680F5B00A75B9A /* HeyMama.app */;
|
productReference = 13B07F961A680F5B00A75B9A /* DearMama.app */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
};
|
};
|
||||||
EB3DAF7E2F2A4B8D00450593 /* 情绪小组件Extension */ = {
|
EB3DAF7E2F2A4B8D00450593 /* 情绪小组件Extension */ = {
|
||||||
@@ -471,7 +482,7 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
A1B2C3D4E5F60718293A4B5C /* 情绪小组件/EmotionWidget.swift in Sources */,
|
A1B2C3D4E5F60718293A4B5C /* EmotionWidget.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -516,7 +527,7 @@
|
|||||||
);
|
);
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.damer.mindfulness;
|
PRODUCT_BUNDLE_IDENTIFIER = com.damer.mindfulness;
|
||||||
PRODUCT_NAME = HeyMama;
|
PRODUCT_NAME = DearMama;
|
||||||
SKIP_INSTALL = NO;
|
SKIP_INSTALL = NO;
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
SUPPORTS_MACCATALYST = NO;
|
SUPPORTS_MACCATALYST = NO;
|
||||||
@@ -557,7 +568,7 @@
|
|||||||
);
|
);
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.damer.mindfulness;
|
PRODUCT_BUNDLE_IDENTIFIER = com.damer.mindfulness;
|
||||||
PRODUCT_NAME = HeyMama;
|
PRODUCT_NAME = DearMama;
|
||||||
SKIP_INSTALL = NO;
|
SKIP_INSTALL = NO;
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
SUPPORTS_MACCATALYST = NO;
|
SUPPORTS_MACCATALYST = NO;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||||
BuildableName = "HeyMama.app"
|
BuildableName = "DearMama.app"
|
||||||
BlueprintName = "client"
|
BlueprintName = "client"
|
||||||
ReferencedContainer = "container:client.xcodeproj">
|
ReferencedContainer = "container:client.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||||
BuildableName = "HeyMama.app"
|
BuildableName = "DearMama.app"
|
||||||
BlueprintName = "client"
|
BlueprintName = "client"
|
||||||
ReferencedContainer = "container:client.xcodeproj">
|
ReferencedContainer = "container:client.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||||
BuildableName = "HeyMama.app"
|
BuildableName = "DearMama.app"
|
||||||
BlueprintName = "client"
|
BlueprintName = "client"
|
||||||
ReferencedContainer = "container:client.xcodeproj">
|
ReferencedContainer = "container:client.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
</AnalyzeAction>
|
</AnalyzeAction>
|
||||||
<ArchiveAction
|
<ArchiveAction
|
||||||
buildConfiguration = "Release"
|
buildConfiguration = "Release"
|
||||||
customArchiveName = "Hey Mama"
|
customArchiveName = "Dear Mama"
|
||||||
revealArchiveInOrganizer = "YES">
|
revealArchiveInOrganizer = "YES">
|
||||||
<PostActions>
|
<PostActions>
|
||||||
<ExecutionAction
|
<ExecutionAction
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||||
BuildableName = "HeyMama.app"
|
BuildableName = "DearMama.app"
|
||||||
BlueprintName = "client"
|
BlueprintName = "client"
|
||||||
ReferencedContainer = "container:client.xcodeproj">
|
ReferencedContainer = "container:client.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 143 KiB |
@@ -7,7 +7,7 @@
|
|||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Hey Mama</string>
|
<string>Dear Mama</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ if [[ -z "$APP_PLIST" ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
APP_DIR="$(/usr/bin/dirname "$APP_PLIST")"
|
APP_DIR="$(/usr/bin/dirname "$APP_PLIST")"
|
||||||
APP_NAME="$(/usr/bin/basename "$APP_DIR")" # 例如 HeyMama.app
|
APP_NAME="$(/usr/bin/basename "$APP_DIR")" # 例如 DearMama.app
|
||||||
APP_REL_PATH="Applications/$APP_NAME"
|
APP_REL_PATH="Applications/$APP_NAME"
|
||||||
|
|
||||||
bundle_id="$(/usr/bin/plutil -extract CFBundleIdentifier raw -o - "$APP_PLIST" 2>/dev/null || true)"
|
bundle_id="$(/usr/bin/plutil -extract CFBundleIdentifier raw -o - "$APP_PLIST" 2>/dev/null || true)"
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ private func resolveLang() -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func resolveTitle(lang: String) -> String {
|
private func resolveTitle(lang: String) -> String {
|
||||||
// 需求:品牌文案「正念」统一改为 Hey Mama
|
// 需求:品牌文案统一为 Dear Mama
|
||||||
return "Hey Mama"
|
return "Dear Mama"
|
||||||
}
|
}
|
||||||
|
|
||||||
private func resolveFooterHint(lang: String) -> String {
|
private func resolveFooterHint(lang: String) -> String {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Metro 配置:支持 import 本地 .svg 为 React 组件
|
// Metro 配置:支持 import 本地 .svg 为 React 组件
|
||||||
// 说明:Expo SDK 54 + react-native-svg-transformer 的常见配置方式
|
// 说明:Expo SDK 54 + react-native-svg-transformer 的常见配置方式
|
||||||
|
const path = require('path');
|
||||||
const { getDefaultConfig } = require('expo/metro-config');
|
const { getDefaultConfig } = require('expo/metro-config');
|
||||||
|
|
||||||
/** @type {import('expo/metro-config').MetroConfig} */
|
/** @type {import('expo/metro-config').MetroConfig} */
|
||||||
@@ -14,6 +15,10 @@ config.resolver = {
|
|||||||
...config.resolver,
|
...config.resolver,
|
||||||
assetExts: config.resolver.assetExts.filter((ext) => ext !== 'svg'),
|
assetExts: config.resolver.assetExts.filter((ext) => ext !== 'svg'),
|
||||||
sourceExts: [...config.resolver.sourceExts, 'svg'],
|
sourceExts: [...config.resolver.sourceExts, 'svg'],
|
||||||
|
// 确保 react-native-text-size 从项目 node_modules 解析(避免 Metro 解析不到)
|
||||||
|
extraNodeModules: {
|
||||||
|
'react-native-text-size': path.resolve(__dirname, 'node_modules/react-native-text-size'),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = config;
|
module.exports = config;
|
||||||
|
|||||||
1
client/package-lock.json
generated
@@ -9719,6 +9719,7 @@
|
|||||||
"version": "4.0.0-rc.1",
|
"version": "4.0.0-rc.1",
|
||||||
"resolved": "https://registry.npmmirror.com/react-native-text-size/-/react-native-text-size-4.0.0-rc.1.tgz",
|
"resolved": "https://registry.npmmirror.com/react-native-text-size/-/react-native-text-size-4.0.0-rc.1.tgz",
|
||||||
"integrity": "sha512-CysqjU2jK6Yc+a+kEI222pUyTY2ywcU2HqbFqf1KHymW6OPTdvBBHqbEJKL0QiLhQaFYDbqicM+h990s9TP00g==",
|
"integrity": "sha512-CysqjU2jK6Yc+a+kEI222pUyTY2ywcU2HqbFqf1KHymW6OPTdvBBHqbEJKL0QiLhQaFYDbqicM+h990s9TP00g==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react-native": ">=0.59.0"
|
"react-native": ">=0.59.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
"start": "expo start",
|
"start": "expo start",
|
||||||
"start:clean": "expo start -c",
|
"start:clean": "expo start -c",
|
||||||
"android": "expo run:android",
|
"android": "expo run:android",
|
||||||
"ios": "expo run:ios --scheme \"Hey Mama\"",
|
"ios": "expo run:ios --scheme \"Dear Mama\"",
|
||||||
"ios:clean": "npm run clean:cache && npm run clean:ios-build && expo run:ios --scheme \"Hey Mama\"",
|
"ios:clean": "npm run clean:cache && npm run clean:ios-build && expo run:ios --scheme \"Dear Mama\"",
|
||||||
"web": "expo start --web",
|
"web": "expo start --web",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"clean:cache": "rm -rf node_modules/.cache .expo 2>/dev/null; echo 'Cleared .expo and node_modules/.cache'",
|
"clean:cache": "rm -rf node_modules/.cache .expo 2>/dev/null; echo 'Cleared .expo and node_modules/.cache'",
|
||||||
|
|||||||
@@ -20,11 +20,12 @@ type TextSizeMeasureParams = {
|
|||||||
|
|
||||||
type TextSizeMeasureResult = { width: number };
|
type TextSizeMeasureResult = { width: number };
|
||||||
|
|
||||||
async function loadReactNativeTextSize(): Promise<{
|
function loadReactNativeTextSize(): {
|
||||||
measure: (params: TextSizeMeasureParams) => Promise<TextSizeMeasureResult>;
|
measure: (params: TextSizeMeasureParams) => Promise<TextSizeMeasureResult>;
|
||||||
}> {
|
} {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// 使用 require 确保 Metro 能解析并打包该原生模块(动态 import 在某些环境下无法被正确解析)
|
||||||
const mod: any = await import('react-native-text-size');
|
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-explicit-any
|
||||||
|
const mod: any = require('react-native-text-size');
|
||||||
return mod?.default ?? mod;
|
return mod?.default ?? mod;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +43,7 @@ function toTextSizeFontSpecs(fontSpec: FontSpec): Pick<TextSizeMeasureParams, 'f
|
|||||||
* - usePreciseWidth=true,取更精确的宽度(开销更大,但对本算法更稳定)
|
* - usePreciseWidth=true,取更精确的宽度(开销更大,但对本算法更稳定)
|
||||||
*/
|
*/
|
||||||
export const defaultMeasureWidthImpl: MeasureWidthImpl = async ({ text, fontSpec }) => {
|
export const defaultMeasureWidthImpl: MeasureWidthImpl = async ({ text, fontSpec }) => {
|
||||||
const TextSize = await loadReactNativeTextSize();
|
const TextSize = loadReactNativeTextSize();
|
||||||
if (!TextSize || typeof TextSize.measure !== 'function') {
|
if (!TextSize || typeof TextSize.measure !== 'function') {
|
||||||
// 典型原因:在 Expo Go 中运行,或没有使用包含该原生模块的 Development Build。
|
// 典型原因:在 Expo Go 中运行,或没有使用包含该原生模块的 Development Build。
|
||||||
// 这里抛出更明确的错误,方便上层捕获并在 meta.reason=MEASURE_FAILED 时看到根因。
|
// 这里抛出更明确的错误,方便上层捕获并在 meta.reason=MEASURE_FAILED 时看到根因。
|
||||||
|
|||||||
@@ -88,7 +88,7 @@
|
|||||||
"errorDesc": "It’s okay if enabling fails. You can keep using the app."
|
"errorDesc": "It’s okay if enabling fails. You can keep using the app."
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"title": "Hey Mama",
|
"title": "Dear Mama",
|
||||||
"like": "Like",
|
"like": "Like",
|
||||||
"dislike": "Dislike",
|
"dislike": "Dislike",
|
||||||
"favorites": "Favorites",
|
"favorites": "Favorites",
|
||||||
@@ -127,7 +127,7 @@
|
|||||||
"homeScreen": "Home Screen Widget",
|
"homeScreen": "Home Screen Widget",
|
||||||
"howToTitle": "How to add the widget",
|
"howToTitle": "How to add the widget",
|
||||||
"howToDesc1": "Long-press on the Home Screen until the apps jiggle, then tap “+” in the top-left corner.",
|
"howToDesc1": "Long-press on the Home Screen until the apps jiggle, then tap “+” in the top-left corner.",
|
||||||
"howToDesc2": "Search “Hey Mama”, choose a widget size you like, then tap “Add Widget”.",
|
"howToDesc2": "Search “Dear Mama”, choose a widget size you like, then tap “Add Widget”.",
|
||||||
"previewDate": "Thu, Jan 29",
|
"previewDate": "Thu, Jan 29",
|
||||||
"previewQuote": "I’m proud of who I am, even while becoming who I want to be."
|
"previewQuote": "I’m proud of who I am, even while becoming who I want to be."
|
||||||
},
|
},
|
||||||
@@ -141,10 +141,10 @@
|
|||||||
"language": "Language",
|
"language": "Language",
|
||||||
"version": "Version",
|
"version": "Version",
|
||||||
"widgetTitle": "iOS Widget",
|
"widgetTitle": "iOS Widget",
|
||||||
"widgetDesc": "Put gentle reminders on your home screen: long-press → tap “+” → search “Hey Mama” → add a size you like."
|
"widgetDesc": "Put gentle reminders on your home screen: long-press → tap “+” → search “Dear Mama” → add a size you like."
|
||||||
},
|
},
|
||||||
"consent": {
|
"consent": {
|
||||||
"title": "Hey mama.",
|
"title": "Dear mama.",
|
||||||
"subtitle": "You’re doing okay\nright now.",
|
"subtitle": "You’re doing okay\nright now.",
|
||||||
"subtitleSecondary": "",
|
"subtitleSecondary": "",
|
||||||
"agree": "Agree & Continue",
|
"agree": "Agree & Continue",
|
||||||
@@ -260,7 +260,7 @@
|
|||||||
"errorDesc": "開啟失敗也沒關係,你仍然可以繼續使用應用。"
|
"errorDesc": "開啟失敗也沒關係,你仍然可以繼續使用應用。"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"title": "Hey Mama",
|
"title": "Dear Mama",
|
||||||
"like": "喜歡",
|
"like": "喜歡",
|
||||||
"dislike": "不喜歡",
|
"dislike": "不喜歡",
|
||||||
"favorites": "收藏",
|
"favorites": "收藏",
|
||||||
@@ -299,7 +299,7 @@
|
|||||||
"homeScreen": "桌面小工具",
|
"homeScreen": "桌面小工具",
|
||||||
"howToTitle": "如何加入小工具",
|
"howToTitle": "如何加入小工具",
|
||||||
"howToDesc1": "長按主畫面空白處進入編輯,點左上角「+」新增小工具。",
|
"howToDesc1": "長按主畫面空白處進入編輯,點左上角「+」新增小工具。",
|
||||||
"howToDesc2": "搜尋「Hey Mama」,選擇喜歡的尺寸,點「加入小工具」。",
|
"howToDesc2": "搜尋「Dear Mama」,選擇喜歡的尺寸,點「加入小工具」。",
|
||||||
"previewDate": "1月29日週四 · 已至臘月十一",
|
"previewDate": "1月29日週四 · 已至臘月十一",
|
||||||
"previewQuote": "我也對現在的自己感到滿意,即使我仍在努力成為想成為的人。"
|
"previewQuote": "我也對現在的自己感到滿意,即使我仍在努力成為想成為的人。"
|
||||||
},
|
},
|
||||||
@@ -313,7 +313,7 @@
|
|||||||
"language": "語言",
|
"language": "語言",
|
||||||
"version": "版本",
|
"version": "版本",
|
||||||
"widgetTitle": "iOS 小工具",
|
"widgetTitle": "iOS 小工具",
|
||||||
"widgetDesc": "把溫柔提醒放到桌面上:長按主畫面 → 點「+」 → 搜尋「Hey Mama」 → 添加你喜歡的尺寸。"
|
"widgetDesc": "把溫柔提醒放到桌面上:長按主畫面 → 點「+」 → 搜尋「Dear Mama」 → 添加你喜歡的尺寸。"
|
||||||
},
|
},
|
||||||
"consent": {
|
"consent": {
|
||||||
"title": "我們知道,",
|
"title": "我們知道,",
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
"errorDesc": "It’s okay if enabling fails. You can keep using the app."
|
"errorDesc": "It’s okay if enabling fails. You can keep using the app."
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"title": "Hey Mama",
|
"title": "Dear Mama",
|
||||||
"like": "Like",
|
"like": "Like",
|
||||||
"dislike": "Dislike",
|
"dislike": "Dislike",
|
||||||
"favorites": "Favorites",
|
"favorites": "Favorites",
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
"language": "Language",
|
"language": "Language",
|
||||||
"version": "Version",
|
"version": "Version",
|
||||||
"widgetTitle": "iOS Widget",
|
"widgetTitle": "iOS Widget",
|
||||||
"widgetDesc": "Put gentle reminders on your home screen: long-press → tap “+” → search “Hey Mama” → add a size you like."
|
"widgetDesc": "Put gentle reminders on your home screen: long-press → tap “+” → search “Dear Mama” → add a size you like."
|
||||||
},
|
},
|
||||||
"consent": {
|
"consent": {
|
||||||
"title": "You Are Perfect.",
|
"title": "You Are Perfect.",
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
"errorDesc": "No pasa nada si falla. Puedes seguir usando la app."
|
"errorDesc": "No pasa nada si falla. Puedes seguir usando la app."
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"title": "Hey Mama",
|
"title": "Dear Mama",
|
||||||
"like": "Me gusta",
|
"like": "Me gusta",
|
||||||
"dislike": "No me gusta",
|
"dislike": "No me gusta",
|
||||||
"favorites": "Favoritos",
|
"favorites": "Favoritos",
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
"version": "Versión",
|
"version": "Versión",
|
||||||
"widgetTitle": "Widget de iOS",
|
"widgetTitle": "Widget de iOS",
|
||||||
"widgetDesc": "Pon recordatorios en tu pantalla: mantén pulsado → “+” → busca “Hey Mama” → añade el tamaño."
|
"widgetDesc": "Pon recordatorios en tu pantalla: mantén pulsado → “+” → busca “Dear Mama” → añade el tamaño."
|
||||||
},
|
},
|
||||||
"consent": {
|
"consent": {
|
||||||
"agree": "Aceptar y Continuar",
|
"agree": "Aceptar y Continuar",
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
"errorDesc": "Tudo bem se falhar. Você pode continuar usando o app."
|
"errorDesc": "Tudo bem se falhar. Você pode continuar usando o app."
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"title": "Hey Mama",
|
"title": "Dear Mama",
|
||||||
"like": "Curtir",
|
"like": "Curtir",
|
||||||
"dislike": "Não curtir",
|
"dislike": "Não curtir",
|
||||||
"favorites": "Favoritos",
|
"favorites": "Favoritos",
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
"version": "Versão",
|
"version": "Versão",
|
||||||
"widgetTitle": "Widget do iOS",
|
"widgetTitle": "Widget do iOS",
|
||||||
"widgetDesc": "Coloque lembretes na tela inicial: pressione e segure → “+” → procure “Hey Mama” → adicione o tamanho."
|
"widgetDesc": "Coloque lembretes na tela inicial: pressione e segure → “+” → procure “Dear Mama” → adicione o tamanho."
|
||||||
},
|
},
|
||||||
"consent": {
|
"consent": {
|
||||||
"agree": "Concordar e Continuar",
|
"agree": "Concordar e Continuar",
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
"errorDesc": "开启失败,请稍后重试(模拟器可能无法获取推送 Token,建议用真机测试)。"
|
"errorDesc": "开启失败,请稍后重试(模拟器可能无法获取推送 Token,建议用真机测试)。"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"title": "Hey Mama",
|
"title": "Dear Mama",
|
||||||
"like": "点赞",
|
"like": "点赞",
|
||||||
"dislike": "讨厌",
|
"dislike": "讨厌",
|
||||||
"favorites": "收藏",
|
"favorites": "收藏",
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
"language": "语言",
|
"language": "语言",
|
||||||
"version": "版本",
|
"version": "版本",
|
||||||
"widgetTitle": "iOS 小组件",
|
"widgetTitle": "iOS 小组件",
|
||||||
"widgetDesc": "把温柔提醒放到桌面上:长按主屏幕 → 点“+” → 搜索“Hey Mama” → 添加你喜欢的尺寸。"
|
"widgetDesc": "把温柔提醒放到桌面上:长按主屏幕 → 点“+” → 搜索“Dear Mama” → 添加你喜欢的尺寸。"
|
||||||
},
|
},
|
||||||
"consent": {
|
"consent": {
|
||||||
"title": "你本就完美。",
|
"title": "你本就完美。",
|
||||||
|
|||||||
@@ -92,7 +92,7 @@
|
|||||||
"errorDesc": "開啟失敗也沒關係,你仍然可以繼續使用應用。"
|
"errorDesc": "開啟失敗也沒關係,你仍然可以繼續使用應用。"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"title": "Hey Mama",
|
"title": "Dear Mama",
|
||||||
"like": "喜歡",
|
"like": "喜歡",
|
||||||
"dislike": "不喜歡",
|
"dislike": "不喜歡",
|
||||||
"favorites": "收藏",
|
"favorites": "收藏",
|
||||||
@@ -131,7 +131,7 @@
|
|||||||
"homeScreen": "桌面小工具",
|
"homeScreen": "桌面小工具",
|
||||||
"howToTitle": "如何加入小工具",
|
"howToTitle": "如何加入小工具",
|
||||||
"howToDesc1": "長按主畫面空白處進入編輯,點左上角「+」新增小工具。",
|
"howToDesc1": "長按主畫面空白處進入編輯,點左上角「+」新增小工具。",
|
||||||
"howToDesc2": "搜尋「Hey Mama」,選擇喜歡的尺寸,點「加入小工具」。",
|
"howToDesc2": "搜尋「Dear Mama」,選擇喜歡的尺寸,點「加入小工具」。",
|
||||||
"previewDate": "1月29日週四 · 已至臘月十一",
|
"previewDate": "1月29日週四 · 已至臘月十一",
|
||||||
"previewQuote": "我也對現在的自己感到滿意,即使我仍在努力成為想成為的人。"
|
"previewQuote": "我也對現在的自己感到滿意,即使我仍在努力成為想成為的人。"
|
||||||
},
|
},
|
||||||
@@ -145,7 +145,7 @@
|
|||||||
"language": "語言",
|
"language": "語言",
|
||||||
"version": "版本",
|
"version": "版本",
|
||||||
"widgetTitle": "iOS 小工具",
|
"widgetTitle": "iOS 小工具",
|
||||||
"widgetDesc": "把溫柔提醒放到桌面上:長按主畫面 → 點「+」 → 搜尋「Hey Mama」 → 添加你喜歡的尺寸。"
|
"widgetDesc": "把溫柔提醒放到桌面上:長按主畫面 → 點「+」 → 搜尋「Dear Mama」 → 添加你喜歡的尺寸。"
|
||||||
},
|
},
|
||||||
"consent": {
|
"consent": {
|
||||||
"title": "我們知道,",
|
"title": "我們知道,",
|
||||||
|
|||||||
31
server/alembic/versions/0003_add_push_send_log_payload.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
"""add push_send_log payload snapshot
|
||||||
|
|
||||||
|
Revision ID: 0003_add_push_send_log_payload
|
||||||
|
Revises: 0002_init_push_tables
|
||||||
|
Create Date: 2026-02-12
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "0003_add_push_send_log_payload"
|
||||||
|
down_revision = "0002_init_push_tables"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column("push_send_log", sa.Column("content_id", sa.Integer(), nullable=True, comment="推送文案内容 ID(可选)"))
|
||||||
|
op.add_column("push_send_log", sa.Column("title", sa.String(length=128), nullable=True, comment="推送标题(可选)"))
|
||||||
|
op.add_column("push_send_log", sa.Column("body", sa.Text(), nullable=True, comment="推送正文(可选)"))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column("push_send_log", "body")
|
||||||
|
op.drop_column("push_send_log", "title")
|
||||||
|
op.drop_column("push_send_log", "content_id")
|
||||||
|
|
||||||
@@ -7,16 +7,70 @@ from fastapi.responses import HTMLResponse
|
|||||||
from pydantic import BaseModel, HttpUrl
|
from pydantic import BaseModel, HttpUrl
|
||||||
|
|
||||||
from app.core.config import get_settings
|
from app.core.config import get_settings
|
||||||
from app.legal_docs import PRIVACY_POLICY_MD, TERMS_OF_USE_MD, choose_content_by_lang, render_as_simple_html
|
from app.legal_docs import (
|
||||||
|
PRIVACY_POLICY_MD,
|
||||||
|
TERMS_OF_USE_MD,
|
||||||
|
choose_content_by_lang,
|
||||||
|
render_as_simple_html,
|
||||||
|
split_bilingual_markdown,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/v1/legal", tags=["legal"])
|
router = APIRouter(prefix="/v1/legal", tags=["legal"])
|
||||||
|
|
||||||
INTERNAL_SENTINEL = "__internal__"
|
INTERNAL_SENTINEL = "__internal__"
|
||||||
|
|
||||||
|
# 技术支持页文案(EN / TC)
|
||||||
|
SUPPORT_MD = """Dear Mama | Technical Support
|
||||||
|
Last updated: February 2026
|
||||||
|
|
||||||
|
If you need technical support for Dear Mama, please contact us:
|
||||||
|
- Support email: leitinglan@project-c.org
|
||||||
|
|
||||||
|
Support scope (including but not limited to):
|
||||||
|
- App installation or update issues
|
||||||
|
- App crashes, freezes, or abnormal behavior
|
||||||
|
- Notification or widget related issues
|
||||||
|
- Difficulty accessing privacy policy or terms pages
|
||||||
|
|
||||||
|
To help us process your request faster, please include:
|
||||||
|
- Device model and OS version
|
||||||
|
- App version
|
||||||
|
- A brief issue description and occurrence time
|
||||||
|
- Screenshots or screen recording (if available)
|
||||||
|
|
||||||
|
Service commitment:
|
||||||
|
- We generally respond within 3-5 business days
|
||||||
|
- Emergency availability may vary by holidays and local time
|
||||||
|
|
||||||
|
---
|
||||||
|
Dear Mama|技術支援
|
||||||
|
最後更新日期:2026 年 2 月
|
||||||
|
|
||||||
|
如需 Dear Mama 的技術支援,請透過以下方式聯絡我們:
|
||||||
|
- 支援信箱:leitinglan@project-c.org
|
||||||
|
|
||||||
|
支援範圍(包含但不限於):
|
||||||
|
- App 安裝或更新問題
|
||||||
|
- App 閃退、卡頓或異常行為
|
||||||
|
- 通知或小組件相關問題
|
||||||
|
- 隱私政策或使用條款頁面無法開啟
|
||||||
|
|
||||||
|
為了加快處理,建議提供:
|
||||||
|
- 裝置型號與系統版本
|
||||||
|
- App 版本號
|
||||||
|
- 問題描述與發生時間
|
||||||
|
- 截圖或螢幕錄影(如有)
|
||||||
|
|
||||||
|
服務承諾:
|
||||||
|
- 一般會在 3-5 個工作日內回覆
|
||||||
|
- 節假日或時區差異時,回覆時間可能延長
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
# 当前多语言仅支持 EN / TC(繁体)
|
# 当前多语言仅支持 EN / TC(繁体)
|
||||||
ResolvedLang = Literal["en", "tc"]
|
ResolvedLang = Literal["en", "tc"]
|
||||||
|
BILINGUAL_CONTENT_LANGUAGE = "en, zh-Hant"
|
||||||
|
|
||||||
|
|
||||||
class LegalLinksResponse(BaseModel):
|
class LegalLinksResponse(BaseModel):
|
||||||
@@ -85,6 +139,18 @@ def _normalize_internal_url(request: Request, url: str, internal_path: str) -> s
|
|||||||
return _join_base_url(str(request.base_url), internal_path)
|
return _join_base_url(str(request.base_url), internal_path)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_bilingual_content(text: str) -> str:
|
||||||
|
"""
|
||||||
|
固定输出双语协议内容:EN 在前,TC 在后。
|
||||||
|
若缺少 TC,则仅返回 EN。
|
||||||
|
"""
|
||||||
|
|
||||||
|
en, tc = split_bilingual_markdown(text)
|
||||||
|
if not tc:
|
||||||
|
return en
|
||||||
|
return f"{en}\n\n---\n{tc}"
|
||||||
|
|
||||||
|
|
||||||
@router.get("/links", response_model=LegalLinksResponse)
|
@router.get("/links", response_model=LegalLinksResponse)
|
||||||
async def get_legal_links(request: Request) -> LegalLinksResponse:
|
async def get_legal_links(request: Request) -> LegalLinksResponse:
|
||||||
"""
|
"""
|
||||||
@@ -109,11 +175,18 @@ async def get_privacy_policy(request: Request) -> HTMLResponse:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
accept_language = request.headers.get("accept-language")
|
accept_language = request.headers.get("accept-language")
|
||||||
|
if accept_language:
|
||||||
lang = _resolve_lang(accept_language)
|
lang = _resolve_lang(accept_language)
|
||||||
content, resolved = choose_content_by_lang(PRIVACY_POLICY_MD, lang)
|
content, resolved = choose_content_by_lang(PRIVACY_POLICY_MD, lang)
|
||||||
title = "Hey Mama | Privacy Policy" if resolved == "en" else "Hey Mama|隱私權政策"
|
title = "Dear Mama | Privacy Policy" if resolved == "en" else "Dear Mama|隱私權政策"
|
||||||
page = render_as_simple_html(title=title, content=content)
|
html_lang = "en" if resolved == "en" else "zh-Hant"
|
||||||
return HTMLResponse(content=page, headers={"Content-Language": "en" if resolved == "en" else "zh-Hant"})
|
page = render_as_simple_html(title=title, content=content, html_lang=html_lang)
|
||||||
|
return HTMLResponse(content=page, headers={"Content-Language": html_lang})
|
||||||
|
|
||||||
|
content = _build_bilingual_content(PRIVACY_POLICY_MD)
|
||||||
|
title = "Dear Mama | Privacy Policy / 隱私權政策"
|
||||||
|
page = render_as_simple_html(title=title, content=content, html_lang="en")
|
||||||
|
return HTMLResponse(content=page, headers={"Content-Language": BILINGUAL_CONTENT_LANGUAGE})
|
||||||
|
|
||||||
|
|
||||||
@router.get("/terms", response_class=HTMLResponse)
|
@router.get("/terms", response_class=HTMLResponse)
|
||||||
@@ -123,9 +196,37 @@ async def get_terms_of_use(request: Request) -> HTMLResponse:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
accept_language = request.headers.get("accept-language")
|
accept_language = request.headers.get("accept-language")
|
||||||
|
if accept_language:
|
||||||
lang = _resolve_lang(accept_language)
|
lang = _resolve_lang(accept_language)
|
||||||
content, resolved = choose_content_by_lang(TERMS_OF_USE_MD, lang)
|
content, resolved = choose_content_by_lang(TERMS_OF_USE_MD, lang)
|
||||||
title = "Hey Mama – Terms of Use" if resolved == "en" else "Hey Mama 使用條款"
|
title = "Dear Mama – Terms of Use" if resolved == "en" else "Dear Mama 使用條款"
|
||||||
page = render_as_simple_html(title=title, content=content)
|
html_lang = "en" if resolved == "en" else "zh-Hant"
|
||||||
return HTMLResponse(content=page, headers={"Content-Language": "en" if resolved == "en" else "zh-Hant"})
|
page = render_as_simple_html(title=title, content=content, html_lang=html_lang)
|
||||||
|
return HTMLResponse(content=page, headers={"Content-Language": html_lang})
|
||||||
|
|
||||||
|
content = _build_bilingual_content(TERMS_OF_USE_MD)
|
||||||
|
title = "Dear Mama – Terms of Use / 使用條款"
|
||||||
|
page = render_as_simple_html(title=title, content=content, html_lang="en")
|
||||||
|
return HTMLResponse(content=page, headers={"Content-Language": BILINGUAL_CONTENT_LANGUAGE})
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/support", response_class=HTMLResponse)
|
||||||
|
async def get_support_page(request: Request) -> HTMLResponse:
|
||||||
|
"""
|
||||||
|
技术支持页面(用于 App 审核的可访问 URL)。
|
||||||
|
"""
|
||||||
|
|
||||||
|
accept_language = request.headers.get("accept-language")
|
||||||
|
if accept_language:
|
||||||
|
lang = _resolve_lang(accept_language)
|
||||||
|
content, resolved = choose_content_by_lang(SUPPORT_MD, lang)
|
||||||
|
title = "Dear Mama | Technical Support" if resolved == "en" else "Dear Mama|技術支援"
|
||||||
|
html_lang = "en" if resolved == "en" else "zh-Hant"
|
||||||
|
page = render_as_simple_html(title=title, content=content, html_lang=html_lang)
|
||||||
|
return HTMLResponse(content=page, headers={"Content-Language": html_lang})
|
||||||
|
|
||||||
|
content = _build_bilingual_content(SUPPORT_MD)
|
||||||
|
title = "Dear Mama | Technical Support / 技術支援"
|
||||||
|
page = render_as_simple_html(title=title, content=content, html_lang="en")
|
||||||
|
return HTMLResponse(content=page, headers={"Content-Language": BILINGUAL_CONTENT_LANGUAGE})
|
||||||
|
|
||||||
|
|||||||
@@ -153,6 +153,32 @@ async def register(req: PushRegisterRequest, db: AsyncSession = Depends(get_db))
|
|||||||
token.is_active = True
|
token.is_active = True
|
||||||
token.last_seen_at = _ensure_utc(now)
|
token.last_seen_at = _ensure_utc(now)
|
||||||
|
|
||||||
|
# 额外:尽早写入/补齐时区与语言(用于按用户时区生成排程)
|
||||||
|
# 说明:
|
||||||
|
# - 用户首次授权后会立即调用 /register,但不一定马上进入“每日提醒”确认页
|
||||||
|
# - 若 push_preferences 里 timezone 为空,会导致排程回退到 UTC,体验不符合预期
|
||||||
|
if req.device_meta:
|
||||||
|
tz = (req.device_meta.timezone or "").strip() or None
|
||||||
|
loc = (req.device_meta.locale or "").strip() or None
|
||||||
|
if tz or loc:
|
||||||
|
qpref = select(PushPreference).where(PushPreference.client_user_id == req.client_user_id)
|
||||||
|
rpref = await db.execute(qpref)
|
||||||
|
pref = rpref.scalar_one_or_none()
|
||||||
|
if pref is None:
|
||||||
|
pref = PushPreference(
|
||||||
|
client_user_id=req.client_user_id,
|
||||||
|
enabled=False,
|
||||||
|
times_per_day=0,
|
||||||
|
timezone=tz,
|
||||||
|
locale=loc,
|
||||||
|
)
|
||||||
|
db.add(pref)
|
||||||
|
else:
|
||||||
|
if tz and not (pref.timezone or "").strip():
|
||||||
|
pref.timezone = tz
|
||||||
|
if loc and not (pref.locale or "").strip():
|
||||||
|
pref.locale = loc
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
@@ -187,7 +213,10 @@ async def put_preferences(req: PushPreferencesRequest, db: AsyncSession = Depend
|
|||||||
else:
|
else:
|
||||||
pref.enabled = enabled
|
pref.enabled = enabled
|
||||||
pref.times_per_day = times
|
pref.times_per_day = times
|
||||||
|
# 注意:只在客户端显式传入时覆盖,避免把已保存的 timezone/locale 清空导致排程回退到 UTC
|
||||||
|
if req.timezone is not None:
|
||||||
pref.timezone = req.timezone
|
pref.timezone = req.timezone
|
||||||
|
if req.locale is not None:
|
||||||
pref.locale = req.locale
|
pref.locale = req.locale
|
||||||
if req.user_profile is not None:
|
if req.user_profile is not None:
|
||||||
pref.user_profile_json = req.user_profile.model_dump(mode="json")
|
pref.user_profile_json = req.user_profile.model_dump(mode="json")
|
||||||
@@ -257,7 +286,7 @@ async def test_push(
|
|||||||
_ = UserProfileV1_2.model_validate(pref.user_profile_json)
|
_ = UserProfileV1_2.model_validate(pref.user_profile_json)
|
||||||
|
|
||||||
# V1:先发固定测试文案;后续在定时任务中替换为推荐模块的 push 场景模板
|
# V1:先发固定测试文案;后续在定时任务中替换为推荐模块的 push 场景模板
|
||||||
title = req.title or "Hey Mama"
|
title = req.title or "Dear Mama"
|
||||||
body = req.body or "这是一条测试推送(dev)。"
|
body = req.body or "这是一条测试推送(dev)。"
|
||||||
|
|
||||||
expo_res = await _send_expo_push(to=token.push_token, title=title, body=body, data={"client_user_id": req.client_user_id})
|
expo_res = await _send_expo_push(to=token.push_token, title=title, body=body, data={"client_user_id": req.client_user_id})
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
|
|
||||||
from sqlalchemy import Date, DateTime, Index, SmallInteger, String, Text, UniqueConstraint, func
|
from sqlalchemy import Date, DateTime, Index, Integer, SmallInteger, String, Text, UniqueConstraint, func
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from app.db.base import Base
|
from app.db.base import Base
|
||||||
@@ -33,5 +33,10 @@ class PushSendLog(Base):
|
|||||||
status: Mapped[str] = mapped_column(String(length=16), nullable=False, server_default="scheduled", comment="scheduled/sent/failed")
|
status: Mapped[str] = mapped_column(String(length=16), nullable=False, server_default="scheduled", comment="scheduled/sent/failed")
|
||||||
error: Mapped[str | None] = mapped_column(Text, nullable=True, comment="失败原因(可选)")
|
error: Mapped[str | None] = mapped_column(Text, nullable=True, comment="失败原因(可选)")
|
||||||
|
|
||||||
|
# 发送内容快照(用于观测 + 去重)
|
||||||
|
content_id: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="推送文案内容 ID(可选)")
|
||||||
|
title: Mapped[str | None] = mapped_column(String(length=128), nullable=True, comment="推送标题(可选)")
|
||||||
|
body: Mapped[str | None] = mapped_column(Text, nullable=True, comment="推送正文(可选)")
|
||||||
|
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.now(), comment="创建时间")
|
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.now(), comment="创建时间")
|
||||||
|
|
||||||
|
|||||||
@@ -11,21 +11,21 @@ ResolvedLang = Literal["en", "tc"]
|
|||||||
# 协议原文(直接来自仓库中的 Markdown 文档)。
|
# 协议原文(直接来自仓库中的 Markdown 文档)。
|
||||||
# 说明:为了保证“点击一定有内容”,这里在后端内置了一份可展示的协议文本。
|
# 说明:为了保证“点击一定有内容”,这里在后端内置了一份可展示的协议文本。
|
||||||
# 线上若你们有官网/静态站点托管页面,可通过环境变量 LEGAL_* 覆盖为外部链接。
|
# 线上若你们有官网/静态站点托管页面,可通过环境变量 LEGAL_* 覆盖为外部链接。
|
||||||
PRIVACY_POLICY_MD = """Hey Mama | Privacy Policy
|
PRIVACY_POLICY_MD = """Dear Mama | Privacy Policy
|
||||||
Last updated: February 2026
|
Last updated: February 2026
|
||||||
|
|
||||||
1. Introduction
|
1. Introduction
|
||||||
Welcome to Hey Mama (“the App,” “we,” “us”).
|
Welcome to Dear Mama (“the App,” “we,” “us”).
|
||||||
We respect your privacy and are committed to protecting your personal information. This Privacy Policy explains how we collect, use, store, and protect information when you use the App.
|
We respect your privacy and are committed to protecting your personal information. This Privacy Policy explains how we collect, use, store, and protect information when you use the App.
|
||||||
By downloading, accessing, or using the App, you acknowledge that you have read, understood, and agreed to this Privacy Policy.
|
By downloading, accessing, or using the App, you acknowledge that you have read, understood, and agreed to this Privacy Policy.
|
||||||
|
|
||||||
2. Data Controller and Scope
|
2. Data Controller and Scope
|
||||||
The App is operated and maintained by the Hey Mama team.
|
The App is operated and maintained by the Dear Mama team.
|
||||||
This Privacy Policy applies to information processing activities related to your use of the App.
|
This Privacy Policy applies to information processing activities related to your use of the App.
|
||||||
|
|
||||||
3. Information We Collect
|
3. Information We Collect
|
||||||
3.1 Information You Provide
|
3.1 Information You Provide
|
||||||
Hey Mama does not require account registration and does not require you to provide personally identifiable information.
|
Dear Mama does not require account registration and does not require you to provide personally identifiable information.
|
||||||
During your use of the App, you may optionally provide or generate the following information:
|
During your use of the App, you may optionally provide or generate the following information:
|
||||||
- Reminder settings (e.g., reminder frequency)
|
- Reminder settings (e.g., reminder frequency)
|
||||||
- Text content you view, save as favorites, or create within the App (if available)
|
- Text content you view, save as favorites, or create within the App (if available)
|
||||||
@@ -59,7 +59,7 @@ We do not:
|
|||||||
- Use your data for third-party advertising purposes
|
- Use your data for third-party advertising purposes
|
||||||
|
|
||||||
7. Third-Party Services
|
7. Third-Party Services
|
||||||
Hey Mama currently does not integrate third-party advertising or marketing services.
|
Dear Mama currently does not integrate third-party advertising or marketing services.
|
||||||
The App may rely on necessary operating system and app store services to provide functionality (for example, push notification delivery mechanisms).
|
The App may rely on necessary operating system and app store services to provide functionality (for example, push notification delivery mechanisms).
|
||||||
If we later integrate third-party analytics or technical services, we will update this Privacy Policy accordingly.
|
If we later integrate third-party analytics or technical services, we will update this Privacy Policy accordingly.
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ If we later integrate third-party analytics or technical services, we will updat
|
|||||||
We retain information only for as long as necessary to achieve the purposes described above. We implement reasonable technical and organizational measures to protect information against unauthorized access, disclosure, alteration, or loss.
|
We retain information only for as long as necessary to achieve the purposes described above. We implement reasonable technical and organizational measures to protect information against unauthorized access, disclosure, alteration, or loss.
|
||||||
|
|
||||||
9. Minors
|
9. Minors
|
||||||
Hey Mama is not designed for children, and we do not knowingly collect personal information from users under the age of 13.
|
Dear Mama is not designed for children, and we do not knowingly collect personal information from users under the age of 13.
|
||||||
If you are a minor, please use the App with the consent and supervision of a parent or guardian.
|
If you are a minor, please use the App with the consent and supervision of a parent or guardian.
|
||||||
|
|
||||||
10. Changes to This Privacy Policy
|
10. Changes to This Privacy Policy
|
||||||
@@ -75,17 +75,17 @@ We may update this Privacy Policy from time to time. The updated version will be
|
|||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
Hey Mama|隱私權政策
|
Dear Mama|隱私權政策
|
||||||
最後更新日期:2026 年 2 月
|
最後更新日期:2026 年 2 月
|
||||||
|
|
||||||
一、前言
|
一、前言
|
||||||
歡迎使用 Hey Mama(以下簡稱「本 App」、「我們」)。
|
歡迎使用 Dear Mama(以下簡稱「本 App」、「我們」)。
|
||||||
我們重視您的隱私,並致力於保護您的個人資料安全。本隱私權政策說明您在使用 Hey Mama 時,我們如何收集、使用、保存與保護相關資訊。
|
我們重視您的隱私,並致力於保護您的個人資料安全。本隱私權政策說明您在使用 Dear Mama 時,我們如何收集、使用、保存與保護相關資訊。
|
||||||
當您下載、存取或使用本 App,即表示您已閱讀、理解並同意本隱私權政策之內容。
|
當您下載、存取或使用本 App,即表示您已閱讀、理解並同意本隱私權政策之內容。
|
||||||
|
|
||||||
二、我們收集的資訊
|
二、我們收集的資訊
|
||||||
1. 使用者主動提供的資訊
|
1. 使用者主動提供的資訊
|
||||||
Hey Mama 不要求建立帳號,亦不強制使用者提供可識別個人身分的資料。
|
Dear Mama 不要求建立帳號,亦不強制使用者提供可識別個人身分的資料。
|
||||||
在使用過程中,您可能會選擇性提供或產生以下資訊:
|
在使用過程中,您可能會選擇性提供或產生以下資訊:
|
||||||
- 提醒設定(例如提醒頻率)
|
- 提醒設定(例如提醒頻率)
|
||||||
- 使用者在 App 內閱讀、收藏或建立的文字內容(如有)
|
- 使用者在 App 內閱讀、收藏或建立的文字內容(如有)
|
||||||
@@ -99,7 +99,7 @@ Hey Mama 不要求建立帳號,亦不強制使用者提供可識別個人身
|
|||||||
這些資訊無法直接識別您的身分,僅用於維持 App 穩定性與改善使用體驗。
|
這些資訊無法直接識別您的身分,僅用於維持 App 穩定性與改善使用體驗。
|
||||||
|
|
||||||
三、推送通知
|
三、推送通知
|
||||||
在取得您同意後,Hey Mama 可能會向您發送提醒推送,例如每日肯定語提示。
|
在取得您同意後,Dear Mama 可能會向您發送提醒推送,例如每日肯定語提示。
|
||||||
- 推送內容僅包含一般文字資訊
|
- 推送內容僅包含一般文字資訊
|
||||||
- 不包含任何敏感個人資料
|
- 不包含任何敏感個人資料
|
||||||
- 您可隨時於裝置系統設定中關閉通知功能
|
- 您可隨時於裝置系統設定中關閉通知功能
|
||||||
@@ -117,14 +117,14 @@ Hey Mama 不要求建立帳號,亦不強制使用者提供可識別個人身
|
|||||||
- 將資料用於第三方廣告投放
|
- 將資料用於第三方廣告投放
|
||||||
|
|
||||||
六、第三方服務
|
六、第三方服務
|
||||||
目前 Hey Mama 未整合第三方廣告或行銷服務。
|
目前 Dear Mama 未整合第三方廣告或行銷服務。
|
||||||
如未來整合第三方分析或技術服務,我們將於本政策中另行說明並更新。
|
如未來整合第三方分析或技術服務,我們將於本政策中另行說明並更新。
|
||||||
|
|
||||||
七、資料保存與安全
|
七、資料保存與安全
|
||||||
我們僅在達成上述目的所需期間內保存相關資訊,並採取合理的技術與管理措施,以防止資料遭未經授權存取、洩漏、竄改或遺失。
|
我們僅在達成上述目的所需期間內保存相關資訊,並採取合理的技術與管理措施,以防止資料遭未經授權存取、洩漏、竄改或遺失。
|
||||||
|
|
||||||
八、未成年人說明
|
八、未成年人說明
|
||||||
Hey Mama 並非專為兒童設計,亦不刻意收集未滿 13 歲使用者的個人資料。
|
Dear Mama 並非專為兒童設計,亦不刻意收集未滿 13 歲使用者的個人資料。
|
||||||
若您為未成年人,請在監護人同意與陪同下使用本 App。
|
若您為未成年人,請在監護人同意與陪同下使用本 App。
|
||||||
|
|
||||||
九、隱私權政策的變更
|
九、隱私權政策的變更
|
||||||
@@ -132,17 +132,17 @@ Hey Mama 並非專為兒童設計,亦不刻意收集未滿 13 歲使用者的
|
|||||||
更新後的版本將公布於 App 內或相關頁面,您於政策更新後繼續使用本 App,即視為同意更新內容。
|
更新後的版本將公布於 App 內或相關頁面,您於政策更新後繼續使用本 App,即視為同意更新內容。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
TERMS_OF_USE_MD = """Hey Mama – Terms of Use
|
TERMS_OF_USE_MD = """Dear Mama – Terms of Use
|
||||||
Last updated: February 2026
|
Last updated: February 2026
|
||||||
Welcome to Hey Mama (“the App,” “we,” or “us”).
|
Welcome to Dear Mama (“the App,” “we,” or “us”).
|
||||||
Please read these Terms of Use carefully before downloading, accessing, or using the App. By using the App, you agree to be bound by these Terms.
|
Please read these Terms of Use carefully before downloading, accessing, or using the App. By using the App, you agree to be bound by these Terms.
|
||||||
|
|
||||||
1. Intended Audience
|
1. Intended Audience
|
||||||
Hey Mama is intended for adults only.
|
Dear Mama is intended for adults only.
|
||||||
The App is not designed for children, and users must ensure they have the legal capacity to use the App under applicable laws.
|
The App is not designed for children, and users must ensure they have the legal capacity to use the App under applicable laws.
|
||||||
|
|
||||||
2. Services Provided
|
2. Services Provided
|
||||||
Hey Mama provides text-based content and features, including but not limited to:
|
Dear Mama provides text-based content and features, including but not limited to:
|
||||||
- Daily affirmations and mindfulness text
|
- Daily affirmations and mindfulness text
|
||||||
- User-configured reminders and push notifications
|
- User-configured reminders and push notifications
|
||||||
- Home screen widgets displaying affirmation text
|
- Home screen widgets displaying affirmation text
|
||||||
@@ -184,17 +184,17 @@ Updated versions will be made available within the App or related pages. Continu
|
|||||||
These Terms shall be governed by and construed in accordance with the applicable laws of our operating jurisdiction.
|
These Terms shall be governed by and construed in accordance with the applicable laws of our operating jurisdiction.
|
||||||
|
|
||||||
---
|
---
|
||||||
Hey Mama 使用條款
|
Dear Mama 使用條款
|
||||||
最後更新日期:2026 年 2 月
|
最後更新日期:2026 年 2 月
|
||||||
歡迎使用 Hey Mama(以下簡稱「本 App」、「我們」)。
|
歡迎使用 Dear Mama(以下簡稱「本 App」、「我們」)。
|
||||||
在下載、存取或使用本 App 前,請您仔細閱讀本使用條款。當您開始使用本 App,即表示您已閱讀、理解並同意遵守本條款。
|
在下載、存取或使用本 App 前,請您仔細閱讀本使用條款。當您開始使用本 App,即表示您已閱讀、理解並同意遵守本條款。
|
||||||
|
|
||||||
1. 服務對象與使用資格
|
1. 服務對象與使用資格
|
||||||
Hey Mama 僅供成年人使用(intended for adults)。
|
Dear Mama 僅供成年人使用(intended for adults)。
|
||||||
本 App 並非為兒童設計,使用者應確認自己具備依所在地法律使用本服務的完全行為能力。
|
本 App 並非為兒童設計,使用者應確認自己具備依所在地法律使用本服務的完全行為能力。
|
||||||
|
|
||||||
2. 服務內容
|
2. 服務內容
|
||||||
Hey Mama 提供以文字形式為主的內容與功能,包括但不限於:
|
Dear Mama 提供以文字形式為主的內容與功能,包括但不限於:
|
||||||
- 每日肯定語與正念文字內容
|
- 每日肯定語與正念文字內容
|
||||||
- 使用者設定的提醒與推送通知
|
- 使用者設定的提醒與推送通知
|
||||||
- 桌面小組件顯示肯定語文字
|
- 桌面小組件顯示肯定語文字
|
||||||
@@ -265,7 +265,7 @@ def choose_content_by_lang(text: str, lang: ResolvedLang) -> tuple[str, Resolved
|
|||||||
return en, "en"
|
return en, "en"
|
||||||
|
|
||||||
|
|
||||||
def render_as_simple_html(title: str, content: str) -> str:
|
def render_as_simple_html(title: str, content: str, html_lang: str = "en") -> str:
|
||||||
"""
|
"""
|
||||||
将文本以简单 HTML 的方式展示(使用 pre 保留换行并自动换行)。
|
将文本以简单 HTML 的方式展示(使用 pre 保留换行并自动换行)。
|
||||||
不做 Markdown 渲染,避免引入额外依赖,确保“最小可用、必有内容”。
|
不做 Markdown 渲染,避免引入额外依赖,确保“最小可用、必有内容”。
|
||||||
@@ -273,8 +273,9 @@ def render_as_simple_html(title: str, content: str) -> str:
|
|||||||
|
|
||||||
safe_title = html.escape(title)
|
safe_title = html.escape(title)
|
||||||
safe_content = html.escape(content)
|
safe_content = html.escape(content)
|
||||||
|
safe_lang = html.escape(html_lang or "en")
|
||||||
return f"""<!doctype html>
|
return f"""<!doctype html>
|
||||||
<html lang="en">
|
<html lang="{safe_lang}">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from zoneinfo import ZoneInfo
|
|||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from celery import current_app, shared_task
|
from celery import current_app, shared_task
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select, update
|
||||||
|
|
||||||
from app.core.config import get_settings
|
from app.core.config import get_settings
|
||||||
from app.db.models.push_preference import PushPreference
|
from app.db.models.push_preference import PushPreference
|
||||||
@@ -44,7 +44,8 @@ def _pick_reco_locale(pref_locale: Optional[str]) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _pick_title(locale: str) -> str:
|
def _pick_title(locale: str) -> str:
|
||||||
return "每日提醒" if str(locale) == "tc" else "Daily Reminder"
|
# 需求:tc 语言使用繁体标题
|
||||||
|
return "每日推薦" if str(locale) == "tc" else "Daily Reminder"
|
||||||
|
|
||||||
|
|
||||||
async def _send_expo_push(*, to: str, title: str, body: str, data: Optional[dict[str, Any]] = None) -> dict[str, Any]:
|
async def _send_expo_push(*, to: str, title: str, body: str, data: Optional[dict[str, Any]] = None) -> dict[str, Any]:
|
||||||
@@ -67,7 +68,11 @@ async def _send_expo_push(*, to: str, title: str, body: str, data: Optional[dict
|
|||||||
|
|
||||||
def _uniform_jitter_times(*, start: datetime, end: datetime, n: int) -> list[datetime]:
|
def _uniform_jitter_times(*, start: datetime, end: datetime, n: int) -> list[datetime]:
|
||||||
"""
|
"""
|
||||||
将窗口均匀切分为 n 个区间,并在每段内随机取一个时间点(抖动)。
|
将窗口均匀切分为 n 个区间,并在每段内取“中点 + 受限抖动”的时间点。
|
||||||
|
|
||||||
|
目的:
|
||||||
|
- 尽量均匀分布(避免相邻两条推送随机到非常接近的时间)
|
||||||
|
- 仍保留一定随机性,避免过于机械
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if n <= 0:
|
if n <= 0:
|
||||||
@@ -84,8 +89,15 @@ def _uniform_jitter_times(*, start: datetime, end: datetime, n: int) -> list[dat
|
|||||||
if seg <= 0:
|
if seg <= 0:
|
||||||
out.append(seg_start)
|
out.append(seg_start)
|
||||||
continue
|
continue
|
||||||
jitter = random.random() * seg
|
|
||||||
out.append(seg_start + timedelta(seconds=jitter))
|
# 受限抖动:在每段的 [25%, 75%] 区间内取点
|
||||||
|
# 这样相邻两段的最小间隔为 50% 段长,能显著减少“随机挤在一起”。
|
||||||
|
mid = seg_start + timedelta(seconds=seg * 0.5)
|
||||||
|
jitter = (random.random() - 0.5) * (seg * 0.5) # [-0.25*seg, +0.25*seg]
|
||||||
|
out.append(mid + timedelta(seconds=jitter))
|
||||||
|
|
||||||
|
# 保序(理论上天然有序,这里再保险)
|
||||||
|
out.sort()
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
@@ -167,6 +179,7 @@ async def _generate_schedule_once(*, now_utc: datetime, max_users: int = 5000) -
|
|||||||
created += 1
|
created += 1
|
||||||
|
|
||||||
# 投递 ETA 发送任务
|
# 投递 ETA 发送任务
|
||||||
|
try:
|
||||||
current_app.send_task(
|
current_app.send_task(
|
||||||
"tasks.push.send_scheduled",
|
"tasks.push.send_scheduled",
|
||||||
kwargs={
|
kwargs={
|
||||||
@@ -177,6 +190,14 @@ async def _generate_schedule_once(*, now_utc: datetime, max_users: int = 5000) -
|
|||||||
eta=dt_utc,
|
eta=dt_utc,
|
||||||
)
|
)
|
||||||
scheduled += 1
|
scheduled += 1
|
||||||
|
except Exception as e:
|
||||||
|
# 关键:如果投递失败(例如 broker 短暂不可用),不要让 log 永远卡在 scheduled
|
||||||
|
log.status = "failed"
|
||||||
|
log.error = f"enqueue_failed:{type(e).__name__}"
|
||||||
|
try:
|
||||||
|
await session.commit()
|
||||||
|
except Exception:
|
||||||
|
await session.rollback()
|
||||||
|
|
||||||
return {"created": created, "scheduled": scheduled}
|
return {"created": created, "scheduled": scheduled}
|
||||||
|
|
||||||
@@ -209,6 +230,34 @@ async def _send_once_async(*, client_user_id: str, local_date: date, slot_index:
|
|||||||
return {"status": "noop", "reason": "no_log"}
|
return {"status": "noop", "reason": "no_log"}
|
||||||
if str(log.status) == "sent":
|
if str(log.status) == "sent":
|
||||||
return {"status": "noop", "reason": "already_sent"}
|
return {"status": "noop", "reason": "already_sent"}
|
||||||
|
if str(log.status) not in ("scheduled", "sending"):
|
||||||
|
# 例如 failed/skipped:不再重复尝试
|
||||||
|
return {"status": "noop", "reason": f"not_retryable:{log.status}"}
|
||||||
|
|
||||||
|
# 原子抢占:避免重复发送
|
||||||
|
# - scheduled:正常抢占 scheduled -> sending
|
||||||
|
# - sending:如果长时间卡在 sending(进程崩溃/网络异常等),允许“超时接管”继续执行
|
||||||
|
now_utc_naive = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||||
|
steal_cutoff = now_utc_naive - timedelta(minutes=10)
|
||||||
|
res = await session.execute(
|
||||||
|
update(PushSendLog)
|
||||||
|
.where(
|
||||||
|
PushSendLog.id == log.id,
|
||||||
|
(
|
||||||
|
(PushSendLog.status == "scheduled")
|
||||||
|
| (
|
||||||
|
(PushSendLog.status == "sending")
|
||||||
|
& (PushSendLog.sent_at.is_(None))
|
||||||
|
& (PushSendLog.scheduled_at <= steal_cutoff)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.values(status="sending", error=None)
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
|
if (res.rowcount or 0) <= 0:
|
||||||
|
return {"status": "noop", "reason": "already_in_progress_or_processed"}
|
||||||
|
log.status = "sending"
|
||||||
|
|
||||||
# 2) 当前偏好检查(用户可能中途关闭/改次数)
|
# 2) 当前偏好检查(用户可能中途关闭/改次数)
|
||||||
qpref = select(PushPreference).where(PushPreference.client_user_id == client_user_id)
|
qpref = select(PushPreference).where(PushPreference.client_user_id == client_user_id)
|
||||||
@@ -235,6 +284,7 @@ async def _send_once_async(*, client_user_id: str, local_date: date, slot_index:
|
|||||||
await session.commit()
|
await session.commit()
|
||||||
return {"status": "failed", "reason": "no_active_token"}
|
return {"status": "failed", "reason": "no_active_token"}
|
||||||
|
|
||||||
|
try:
|
||||||
# 4) 生成文案(复用推荐模块 push 场景)
|
# 4) 生成文案(复用推荐模块 push 场景)
|
||||||
reco_locale = str(normalize_locale(_pick_reco_locale(pref.locale)))
|
reco_locale = str(normalize_locale(_pick_reco_locale(pref.locale)))
|
||||||
title = _pick_title(reco_locale)
|
title = _pick_title(reco_locale)
|
||||||
@@ -247,34 +297,76 @@ async def _send_once_async(*, client_user_id: str, local_date: date, slot_index:
|
|||||||
build_user_profile_from_questionnaire(QuestionnaireAnswersV1_2()).model_dump()
|
build_user_profile_from_questionnaire(QuestionnaireAnswersV1_2()).model_dump()
|
||||||
)
|
)
|
||||||
|
|
||||||
# 直接复用 reco 的 Celery 任务实现(同步函数)
|
# 关键:这里不能调用 tasks.reco.generate(内部会 asyncio.run),否则会嵌套事件循环崩溃。
|
||||||
from app.tasks.reco import generate as reco_generate
|
from app.tasks.reco import run_reco_payload_async
|
||||||
|
|
||||||
reco_payload = reco_generate(scene="push", user_profile=user_profile.model_dump(), k=1, locale=reco_locale)
|
# 去重:用户推送过的内容尽量不再推送
|
||||||
body = ""
|
# 说明:
|
||||||
|
# - 依赖 push_send_log.content_id(需先完成对应 DB 迁移)
|
||||||
|
# - 为避免历史过长导致 already_recommended_ids 过大,这里取“最近若干条已推送内容”近似全量去重
|
||||||
|
used_ids: list[int] = []
|
||||||
try:
|
try:
|
||||||
|
qused = (
|
||||||
|
select(PushSendLog.content_id)
|
||||||
|
.where(
|
||||||
|
PushSendLog.client_user_id == client_user_id,
|
||||||
|
PushSendLog.content_id.is_not(None),
|
||||||
|
PushSendLog.id != log.id,
|
||||||
|
)
|
||||||
|
# 优先排除最近发送过的内容
|
||||||
|
.order_by(PushSendLog.local_date.desc(), PushSendLog.slot_index.desc())
|
||||||
|
.limit(5000)
|
||||||
|
)
|
||||||
|
rused = await session.execute(qused)
|
||||||
|
used_ids = [int(x) for x in rused.scalars().all() if x is not None]
|
||||||
|
except Exception:
|
||||||
|
used_ids = []
|
||||||
|
|
||||||
|
body = ""
|
||||||
|
picked_content_id: int | None = None
|
||||||
|
try:
|
||||||
|
reco_payload = await run_reco_payload_async(
|
||||||
|
scene="push",
|
||||||
|
user_profile=user_profile,
|
||||||
|
k=3,
|
||||||
|
locale=reco_locale,
|
||||||
|
already_recommended_ids=used_ids,
|
||||||
|
)
|
||||||
items = (reco_payload or {}).get("items") or []
|
items = (reco_payload or {}).get("items") or []
|
||||||
if items and isinstance(items, list):
|
if items and isinstance(items, list):
|
||||||
body = str(items[0].get("text") or "").strip()
|
for it in items:
|
||||||
|
if not isinstance(it, dict):
|
||||||
|
continue
|
||||||
|
cid = it.get("content_id")
|
||||||
|
txt = str(it.get("text") or "").strip()
|
||||||
|
if not txt:
|
||||||
|
continue
|
||||||
|
if cid is not None:
|
||||||
|
try:
|
||||||
|
cid_i = int(cid)
|
||||||
|
except Exception:
|
||||||
|
cid_i = None
|
||||||
|
else:
|
||||||
|
cid_i = None
|
||||||
|
if cid_i is not None and cid_i in used_ids:
|
||||||
|
continue
|
||||||
|
picked_content_id = cid_i
|
||||||
|
body = txt
|
||||||
|
break
|
||||||
except Exception:
|
except Exception:
|
||||||
body = ""
|
body = ""
|
||||||
|
|
||||||
if not body:
|
if not body:
|
||||||
body = "给自己一句温柔的话。"
|
# tc 语言兜底文案使用繁体
|
||||||
|
body = "給自己一句溫柔的話。" if reco_locale == "tc" else "给自己一句温柔的话。"
|
||||||
|
|
||||||
# 5) 发送
|
# 5) 发送
|
||||||
try:
|
|
||||||
expo_res = await _send_expo_push(
|
expo_res = await _send_expo_push(
|
||||||
to=str(token.push_token),
|
to=str(token.push_token),
|
||||||
title=title,
|
title=title,
|
||||||
body=body,
|
body=body,
|
||||||
data={"client_user_id": client_user_id, "scene": "push"},
|
data={"client_user_id": client_user_id, "scene": "push"},
|
||||||
)
|
)
|
||||||
except Exception as e:
|
|
||||||
log.status = "failed"
|
|
||||||
log.error = f"send_failed:{type(e).__name__}"
|
|
||||||
await session.commit()
|
|
||||||
return {"status": "failed", "error": str(e)}
|
|
||||||
|
|
||||||
# 6) 解析 Expo 回执,必要时停用 token
|
# 6) 解析 Expo 回执,必要时停用 token
|
||||||
try:
|
try:
|
||||||
@@ -297,8 +389,17 @@ async def _send_once_async(*, client_user_id: str, local_date: date, slot_index:
|
|||||||
log.status = "sent"
|
log.status = "sent"
|
||||||
log.sent_at = datetime.now(timezone.utc).replace(tzinfo=None)
|
log.sent_at = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||||
log.error = None
|
log.error = None
|
||||||
|
log.title = title
|
||||||
|
log.body = body
|
||||||
|
log.content_id = picked_content_id
|
||||||
await session.commit()
|
await session.commit()
|
||||||
return {"status": "sent", "expo": expo_res}
|
return {"status": "sent", "expo": expo_res}
|
||||||
|
except Exception as e:
|
||||||
|
# 兜底:任何未预期异常都不要让状态卡在 sending
|
||||||
|
log.status = "failed"
|
||||||
|
log.error = f"unexpected:{type(e).__name__}"
|
||||||
|
await session.commit()
|
||||||
|
return {"status": "failed", "error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
@shared_task(name="tasks.push.send_scheduled")
|
@shared_task(name="tasks.push.send_scheduled")
|
||||||
@@ -310,3 +411,50 @@ def send_scheduled(*, client_user_id: str, local_date: str, slot_index: int) ->
|
|||||||
d = date.fromisoformat(str(local_date))
|
d = date.fromisoformat(str(local_date))
|
||||||
return asyncio.run(_send_once_async(client_user_id=str(client_user_id), local_date=d, slot_index=int(slot_index)))
|
return asyncio.run(_send_once_async(client_user_id=str(client_user_id), local_date=d, slot_index=int(slot_index)))
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task(name="tasks.push.requeue_overdue")
|
||||||
|
def requeue_overdue(*, grace_seconds: int = 300, limit: int = 200) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
补偿任务:扫描“已到时间但仍处于 scheduled”的记录并重新投递发送任务。
|
||||||
|
|
||||||
|
目的:
|
||||||
|
- 覆盖 broker 短暂不可用、worker 重启、ETA 任务丢失等导致的“scheduled 卡住”
|
||||||
|
- 与 send_scheduled 内部的原子状态抢占配合,避免重复发送
|
||||||
|
"""
|
||||||
|
|
||||||
|
now_utc_naive = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||||
|
cutoff = now_utc_naive - timedelta(seconds=int(grace_seconds))
|
||||||
|
|
||||||
|
async def _run() -> dict[str, Any]:
|
||||||
|
requeued = 0
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
q = (
|
||||||
|
select(PushSendLog)
|
||||||
|
.where(
|
||||||
|
PushSendLog.status.in_(("scheduled", "sending")),
|
||||||
|
PushSendLog.sent_at.is_(None),
|
||||||
|
PushSendLog.scheduled_at <= cutoff,
|
||||||
|
)
|
||||||
|
.order_by(PushSendLog.scheduled_at.asc())
|
||||||
|
.limit(int(limit))
|
||||||
|
)
|
||||||
|
rows = await session.execute(q)
|
||||||
|
logs = list(rows.scalars().all())
|
||||||
|
for log in logs:
|
||||||
|
try:
|
||||||
|
current_app.send_task(
|
||||||
|
"tasks.push.send_scheduled",
|
||||||
|
kwargs={
|
||||||
|
"client_user_id": str(log.client_user_id),
|
||||||
|
"local_date": str(log.local_date),
|
||||||
|
"slot_index": int(log.slot_index),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
requeued += 1
|
||||||
|
except Exception:
|
||||||
|
# 忽略单条投递失败,交给下一轮补偿
|
||||||
|
continue
|
||||||
|
return {"status": "ok", "requeued": requeued, "cutoff": cutoff.isoformat()}
|
||||||
|
|
||||||
|
return asyncio.run(_run())
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,45 @@ async def _run_reco_async(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def run_reco_payload_async(
|
||||||
|
*,
|
||||||
|
scene: Scene,
|
||||||
|
user_profile: UserProfileV1_2,
|
||||||
|
already_recommended_ids: Optional[list[Any]] = None,
|
||||||
|
touched_or_viewed_ids: Optional[list[Any]] = None,
|
||||||
|
k: Optional[int] = None,
|
||||||
|
now: Optional[datetime] = None,
|
||||||
|
locale: Optional[str] = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
在“已有事件循环”内运行推荐并返回 payload。
|
||||||
|
|
||||||
|
用途:
|
||||||
|
- 供 Push 等 async 任务内部调用,避免 `asyncio.run()` 嵌套导致 RuntimeError
|
||||||
|
- 也便于未来在 API/任务间复用
|
||||||
|
"""
|
||||||
|
|
||||||
|
effective_now = _ensure_now(now)
|
||||||
|
effective_locale = _ensure_locale(locale)
|
||||||
|
|
||||||
|
# k 默认按场景(与 generate 保持一致)
|
||||||
|
if k is None:
|
||||||
|
k_i = 30 if scene == "feed" else 1
|
||||||
|
else:
|
||||||
|
k_i = int(k)
|
||||||
|
|
||||||
|
result = await _run_reco_async(
|
||||||
|
scene=scene,
|
||||||
|
user_profile=user_profile,
|
||||||
|
already_recommended_ids=list(already_recommended_ids or []),
|
||||||
|
touched_or_viewed_ids=list(touched_or_viewed_ids or []),
|
||||||
|
k=int(k_i),
|
||||||
|
now=effective_now,
|
||||||
|
locale=effective_locale,
|
||||||
|
)
|
||||||
|
return result.model_dump()
|
||||||
|
|
||||||
|
|
||||||
def _run_reco_sync(
|
def _run_reco_sync(
|
||||||
*,
|
*,
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
|
|||||||
@@ -53,10 +53,19 @@ celery_app.conf.beat_schedule = {
|
|||||||
},
|
},
|
||||||
"push-generate-daily-schedule": {
|
"push-generate-daily-schedule": {
|
||||||
"task": "tasks.push.generate_daily_schedule",
|
"task": "tasks.push.generate_daily_schedule",
|
||||||
"schedule": crontab(minute=10, hour=0),
|
# 由“每天一次”调整为“每 2 小时一次”(UTC)
|
||||||
|
"schedule": crontab(minute=10, hour="*/2"),
|
||||||
"kwargs": {"max_users": 5000},
|
"kwargs": {"max_users": 5000},
|
||||||
"options": {"queue": f"{prefix}:celery"},
|
"options": {"queue": f"{prefix}:celery"},
|
||||||
}
|
}
|
||||||
|
,
|
||||||
|
# 补偿:每 5 分钟扫描一次 overdue scheduled 并重投递
|
||||||
|
"push-requeue-overdue": {
|
||||||
|
"task": "tasks.push.requeue_overdue",
|
||||||
|
"schedule": crontab(minute="*/5"),
|
||||||
|
"kwargs": {"grace_seconds": 300, "limit": 200},
|
||||||
|
"options": {"queue": f"{prefix}:celery"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# 自动发现任务(约定:导入 app.tasks 触发其内部对子模块的显式导入)
|
# 自动发现任务(约定:导入 app.tasks 触发其内部对子模块的显式导入)
|
||||||
|
|||||||
@@ -89,9 +89,9 @@
|
|||||||
- iOS 构建号已提升到 `2`,并将 `client/ios/client/Info.plist` 改为自动跟随 `MARKETING_VERSION` / `CURRENT_PROJECT_VERSION`
|
- iOS 构建号已提升到 `2`,并将 `client/ios/client/Info.plist` 改为自动跟随 `MARKETING_VERSION` / `CURRENT_PROJECT_VERSION`
|
||||||
- 推送 entitlements 的 `aps-environment` 已切到 `production`(用于 TestFlight/线上包)
|
- 推送 entitlements 的 `aps-environment` 已切到 `production`(用于 TestFlight/线上包)
|
||||||
- 清理未接入编译的 WidgetKit 骨架残留:移除磁盘上的 `client/ios/MindfulnessWidget/` 文件,并从 `client/ios/client.xcodeproj/project.pbxproj` 删除对应工程引用(避免 Xcode 显示幽灵文件)
|
- 清理未接入编译的 WidgetKit 骨架残留:移除磁盘上的 `client/ios/MindfulnessWidget/` 文件,并从 `client/ios/client.xcodeproj/project.pbxproj` 删除对应工程引用(避免 Xcode 显示幽灵文件)
|
||||||
- 修复 Xcode Archive 偶发显示 “Generic Xcode Archive”:在共享 scheme `Hey Mama` 的 Archive Post-actions 自动补齐 `.xcarchive/Info.plist` 的 `ApplicationProperties`,并在缺失时补齐 `Name`/`SchemeName` + 自检提示(根治 Organizer 无法识别主 App、无法分发/上传 TestFlight 的问题)
|
- 修复 Xcode Archive 偶发显示 “Generic Xcode Archive”:在共享 scheme `Dear Mama` 的 Archive Post-actions 自动补齐 `.xcarchive/Info.plist` 的 `ApplicationProperties`,并在缺失时补齐 `Name`/`SchemeName` + 自检提示(根治 Organizer 无法识别主 App、无法分发/上传 TestFlight 的问题)
|
||||||
- Widget 名称与描述支持多语言(TC/EN,默认 EN):Widget Extension 增加 `Localizable.strings`(`en.lproj` / `zh-Hant.lproj`),`EmotionWidget.swift` 使用本地化 key 作为 `.configurationDisplayName/.description`
|
- Widget 名称与描述支持多语言(TC/EN,默认 EN):Widget Extension 增加 `Localizable.strings`(`en.lproj` / `zh-Hant.lproj`),`EmotionWidget.swift` 使用本地化 key 作为 `.configurationDisplayName/.description`
|
||||||
- 个人主页弹窗:小工具入口**暂时隐藏**锁屏小工具说明;桌面小工具引导弹窗标题(繁中/TC)更新为“**如何加入小工具**”(并统一弹窗标题使用该文案);品牌文案“正念”改为 **Hey Mama**(含引导搜索词与 Widget 标题)
|
- 个人主页弹窗:小工具入口**暂时隐藏**锁屏小工具说明;桌面小工具引导弹窗标题(繁中/TC)更新为“**如何加入小工具**”(并统一弹窗标题使用该文案);品牌文案改为 **Dear Mama**(含引导搜索词与 Widget 标题)
|
||||||
|
|
||||||
## Text Wrap
|
## Text Wrap
|
||||||
|
|
||||||
@@ -202,6 +202,9 @@
|
|||||||
- 后端新增内置协议内容页:
|
- 后端新增内置协议内容页:
|
||||||
- `GET /v1/legal/privacy`
|
- `GET /v1/legal/privacy`
|
||||||
- `GET /v1/legal/terms`
|
- `GET /v1/legal/terms`
|
||||||
|
- `GET /v1/legal/support`(技术支持页面,提供审核可用的公开支持信息)
|
||||||
|
- 协议展示策略调整:`/v1/legal/privacy`、`/v1/legal/terms`、`/v1/legal/support` 在携带 `Accept-Language` 时按语言单语展示(EN/TC),缺省时展示 EN + TC(EN 在前、TC 在后)
|
||||||
|
- 协议页面语义修复:HTML 根节点 `lang` 属性不再写死,改为随页面实际语言输出(单语 en/zh-Hant;双语默认 en)
|
||||||
- 客户端新增协议接口封装 `client/src/services/legalApi.ts`
|
- 客户端新增协议接口封装 `client/src/services/legalApi.ts`
|
||||||
- 客户端工程化:新增统一 HTTP 封装 `client/src/utils/http.ts`(baseURL/超时/JSON/统一错误),并将 `legalApi.ts` / `recoApi.ts` 接入
|
- 客户端工程化:新增统一 HTTP 封装 `client/src/utils/http.ts`(baseURL/超时/JSON/统一错误),并将 `legalApi.ts` / `recoApi.ts` 接入
|
||||||
- 客户端接入两处入口:`app/(splash)/splash.tsx`、`components/home/ProfileModal.tsx`
|
- 客户端接入两处入口:`app/(splash)/splash.tsx`、`components/home/ProfileModal.tsx`
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
Hey Mama – Terms of Use
|
Dear Mama – Terms of Use
|
||||||
Last updated: February 2026
|
Last updated: February 2026
|
||||||
Welcome to Hey Mama (“the App,” “we,” or “us”).
|
Welcome to Dear Mama (“the App,” “we,” or “us”).
|
||||||
Please read these Terms of Use carefully before downloading, accessing, or using the App. By using the App, you agree to be bound by these Terms.
|
Please read these Terms of Use carefully before downloading, accessing, or using the App. By using the App, you agree to be bound by these Terms.
|
||||||
|
|
||||||
1. Intended Audience
|
1. Intended Audience
|
||||||
Hey Mama is intended for adults only.
|
Dear Mama is intended for adults only.
|
||||||
The App is not designed for children, and users must ensure they have the legal capacity to use the App under applicable laws.
|
The App is not designed for children, and users must ensure they have the legal capacity to use the App under applicable laws.
|
||||||
|
|
||||||
2. Services Provided
|
2. Services Provided
|
||||||
Hey Mama provides text-based content and features, including but not limited to:
|
Dear Mama provides text-based content and features, including but not limited to:
|
||||||
- Daily affirmations and mindfulness text
|
- Daily affirmations and mindfulness text
|
||||||
- User-configured reminders and push notifications
|
- User-configured reminders and push notifications
|
||||||
- Home screen widgets displaying affirmation text
|
- Home screen widgets displaying affirmation text
|
||||||
@@ -50,17 +50,17 @@ Updated versions will be made available within the App or related pages. Continu
|
|||||||
These Terms shall be governed by and construed in accordance with the applicable laws of our operating jurisdiction.
|
These Terms shall be governed by and construed in accordance with the applicable laws of our operating jurisdiction.
|
||||||
|
|
||||||
---
|
---
|
||||||
Hey Mama 使用條款
|
Dear Mama 使用條款
|
||||||
最後更新日期:2026 年 2 月
|
最後更新日期:2026 年 2 月
|
||||||
歡迎使用 Hey Mama(以下簡稱「本 App」、「我們」)。
|
歡迎使用 Dear Mama(以下簡稱「本 App」、「我們」)。
|
||||||
在下載、存取或使用本 App 前,請您仔細閱讀本使用條款。當您開始使用本 App,即表示您已閱讀、理解並同意遵守本條款。
|
在下載、存取或使用本 App 前,請您仔細閱讀本使用條款。當您開始使用本 App,即表示您已閱讀、理解並同意遵守本條款。
|
||||||
|
|
||||||
1. 服務對象與使用資格
|
1. 服務對象與使用資格
|
||||||
Hey Mama 僅供成年人使用(intended for adults)。
|
Dear Mama 僅供成年人使用(intended for adults)。
|
||||||
本 App 並非為兒童設計,使用者應確認自己具備依所在地法律使用本服務的完全行為能力。
|
本 App 並非為兒童設計,使用者應確認自己具備依所在地法律使用本服務的完全行為能力。
|
||||||
|
|
||||||
2. 服務內容
|
2. 服務內容
|
||||||
Hey Mama 提供以文字形式為主的內容與功能,包括但不限於:
|
Dear Mama 提供以文字形式為主的內容與功能,包括但不限於:
|
||||||
- 每日肯定語與正念文字內容
|
- 每日肯定語與正念文字內容
|
||||||
- 使用者設定的提醒與推送通知
|
- 使用者設定的提醒與推送通知
|
||||||
- 桌面小組件顯示肯定語文字
|
- 桌面小組件顯示肯定語文字
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
Hey Mama | Privacy Policy
|
Dear Mama | Privacy Policy
|
||||||
Last updated: February 2026
|
Last updated: February 2026
|
||||||
|
|
||||||
1. Introduction
|
1. Introduction
|
||||||
Welcome to Hey Mama (“the App,” “we,” “us”).
|
Welcome to Dear Mama (“the App,” “we,” “us”).
|
||||||
We respect your privacy and are committed to protecting your personal information. This Privacy Policy explains how we collect, use, store, and protect information when you use the App.
|
We respect your privacy and are committed to protecting your personal information. This Privacy Policy explains how we collect, use, store, and protect information when you use the App.
|
||||||
By downloading, accessing, or using the App, you acknowledge that you have read, understood, and agreed to this Privacy Policy.
|
By downloading, accessing, or using the App, you acknowledge that you have read, understood, and agreed to this Privacy Policy.
|
||||||
|
|
||||||
2. Data Controller and Scope
|
2. Data Controller and Scope
|
||||||
The App is operated and maintained by the Hey Mama team.
|
The App is operated and maintained by the Dear Mama team.
|
||||||
This Privacy Policy applies to information processing activities related to your use of the App.
|
This Privacy Policy applies to information processing activities related to your use of the App.
|
||||||
|
|
||||||
3. Information We Collect
|
3. Information We Collect
|
||||||
3.1 Information You Provide
|
3.1 Information You Provide
|
||||||
Hey Mama does not require account registration and does not require you to provide personally identifiable information.
|
Dear Mama does not require account registration and does not require you to provide personally identifiable information.
|
||||||
During your use of the App, you may optionally provide or generate the following information:
|
During your use of the App, you may optionally provide or generate the following information:
|
||||||
- Reminder settings (e.g., reminder frequency)
|
- Reminder settings (e.g., reminder frequency)
|
||||||
- Text content you view, save as favorites, or create within the App (if available)
|
- Text content you view, save as favorites, or create within the App (if available)
|
||||||
@@ -46,7 +46,7 @@ We do not:
|
|||||||
- Use your data for third-party advertising purposes
|
- Use your data for third-party advertising purposes
|
||||||
|
|
||||||
7. Third-Party Services
|
7. Third-Party Services
|
||||||
Hey Mama currently does not integrate third-party advertising or marketing services.
|
Dear Mama currently does not integrate third-party advertising or marketing services.
|
||||||
The App may rely on necessary operating system and app store services to provide functionality (for example, push notification delivery mechanisms).
|
The App may rely on necessary operating system and app store services to provide functionality (for example, push notification delivery mechanisms).
|
||||||
If we later integrate third-party analytics or technical services, we will update this Privacy Policy accordingly.
|
If we later integrate third-party analytics or technical services, we will update this Privacy Policy accordingly.
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ If we later integrate third-party analytics or technical services, we will updat
|
|||||||
We retain information only for as long as necessary to achieve the purposes described above. We implement reasonable technical and organizational measures to protect information against unauthorized access, disclosure, alteration, or loss.
|
We retain information only for as long as necessary to achieve the purposes described above. We implement reasonable technical and organizational measures to protect information against unauthorized access, disclosure, alteration, or loss.
|
||||||
|
|
||||||
9. Minors
|
9. Minors
|
||||||
Hey Mama is not designed for children, and we do not knowingly collect personal information from users under the age of 13.
|
Dear Mama is not designed for children, and we do not knowingly collect personal information from users under the age of 13.
|
||||||
If you are a minor, please use the App with the consent and supervision of a parent or guardian.
|
If you are a minor, please use the App with the consent and supervision of a parent or guardian.
|
||||||
|
|
||||||
10. Changes to This Privacy Policy
|
10. Changes to This Privacy Policy
|
||||||
@@ -62,17 +62,17 @@ We may update this Privacy Policy from time to time. The updated version will be
|
|||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
Hey Mama|隱私權政策
|
Dear Mama|隱私權政策
|
||||||
最後更新日期:2026 年 2 月
|
最後更新日期:2026 年 2 月
|
||||||
|
|
||||||
一、前言
|
一、前言
|
||||||
歡迎使用 Hey Mama(以下簡稱「本 App」、「我們」)。
|
歡迎使用 Dear Mama(以下簡稱「本 App」、「我們」)。
|
||||||
我們重視您的隱私,並致力於保護您的個人資料安全。本隱私權政策說明您在使用 Hey Mama 時,我們如何收集、使用、保存與保護相關資訊。
|
我們重視您的隱私,並致力於保護您的個人資料安全。本隱私權政策說明您在使用 Dear Mama 時,我們如何收集、使用、保存與保護相關資訊。
|
||||||
當您下載、存取或使用本 App,即表示您已閱讀、理解並同意本隱私權政策之內容。
|
當您下載、存取或使用本 App,即表示您已閱讀、理解並同意本隱私權政策之內容。
|
||||||
|
|
||||||
二、我們收集的資訊
|
二、我們收集的資訊
|
||||||
1. 使用者主動提供的資訊
|
1. 使用者主動提供的資訊
|
||||||
Hey Mama 不要求建立帳號,亦不強制使用者提供可識別個人身分的資料。
|
Dear Mama 不要求建立帳號,亦不強制使用者提供可識別個人身分的資料。
|
||||||
在使用過程中,您可能會選擇性提供或產生以下資訊:
|
在使用過程中,您可能會選擇性提供或產生以下資訊:
|
||||||
- 提醒設定(例如提醒頻率)
|
- 提醒設定(例如提醒頻率)
|
||||||
- 使用者在 App 內閱讀、收藏或建立的文字內容(如有)
|
- 使用者在 App 內閱讀、收藏或建立的文字內容(如有)
|
||||||
@@ -86,7 +86,7 @@ Hey Mama 不要求建立帳號,亦不強制使用者提供可識別個人身
|
|||||||
這些資訊無法直接識別您的身分,僅用於維持 App 穩定性與改善使用體驗。
|
這些資訊無法直接識別您的身分,僅用於維持 App 穩定性與改善使用體驗。
|
||||||
|
|
||||||
三、推送通知
|
三、推送通知
|
||||||
在取得您同意後,Hey Mama 可能會向您發送提醒推送,例如每日肯定語提示。
|
在取得您同意後,Dear Mama 可能會向您發送提醒推送,例如每日肯定語提示。
|
||||||
- 推送內容僅包含一般文字資訊
|
- 推送內容僅包含一般文字資訊
|
||||||
- 不包含任何敏感個人資料
|
- 不包含任何敏感個人資料
|
||||||
- 您可隨時於裝置系統設定中關閉通知功能
|
- 您可隨時於裝置系統設定中關閉通知功能
|
||||||
@@ -104,14 +104,14 @@ Hey Mama 不要求建立帳號,亦不強制使用者提供可識別個人身
|
|||||||
- 將資料用於第三方廣告投放
|
- 將資料用於第三方廣告投放
|
||||||
|
|
||||||
六、第三方服務
|
六、第三方服務
|
||||||
目前 Hey Mama 未整合第三方廣告或行銷服務。
|
目前 Dear Mama 未整合第三方廣告或行銷服務。
|
||||||
如未來整合第三方分析或技術服務,我們將於本政策中另行說明並更新。
|
如未來整合第三方分析或技術服務,我們將於本政策中另行說明並更新。
|
||||||
|
|
||||||
七、資料保存與安全
|
七、資料保存與安全
|
||||||
我們僅在達成上述目的所需期間內保存相關資訊,並採取合理的技術與管理措施,以防止資料遭未經授權存取、洩漏、竄改或遺失。
|
我們僅在達成上述目的所需期間內保存相關資訊,並採取合理的技術與管理措施,以防止資料遭未經授權存取、洩漏、竄改或遺失。
|
||||||
|
|
||||||
八、未成年人說明
|
八、未成年人說明
|
||||||
Hey Mama 並非專為兒童設計,亦不刻意收集未滿 13 歲使用者的個人資料。
|
Dear Mama 並非專為兒童設計,亦不刻意收集未滿 13 歲使用者的個人資料。
|
||||||
若您為未成年人,請在監護人同意與陪同下使用本 App。
|
若您為未成年人,請在監護人同意與陪同下使用本 App。
|
||||||
|
|
||||||
九、隱私權政策的變更
|
九、隱私權政策的變更
|
||||||
|
|||||||