feat(app-expo): env variants, local iOS prebuild, and About diagnostics
Align staging/production builds with APP_VARIANT bundle IDs, allow staging HTTP on iOS, add ios-prebuild scripts for TestFlight, and show connected API URL on About for non-production builds. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,12 +1,34 @@
|
|||||||
# 复制为 .env.development / .env.staging / .env.production 后填写(勿提交含密钥的副本)。
|
# 复制为 .env.development / .env.staging / .env.production 后填写(勿提交含密钥的副本)。
|
||||||
|
# 仓库已提交三份模板:.env.development、.env.staging、.env.production。
|
||||||
# 本地:npm start 会通过 prestart 执行 `use-env development` 生成 .env;
|
# 本地:npm start 会通过 prestart 执行 `use-env development` 生成 .env;
|
||||||
# 或手动 `npm run use-env -- staging` / `npm run use-env -- production`。
|
# 或手动 `npm run use-env -- staging` / `npm run use-env -- production`。
|
||||||
# CI:GitHub Actions 在构建 APK 前会按分支调用 use-env(main → staging,tag → production)。
|
# CI:GitHub Actions 在构建 APK 前会按分支调用 use-env(main → staging,tag → production)。
|
||||||
|
# iOS 本机 TestFlight:npm run ios:prebuild:staging|production(见 scripts/ios-prebuild.sh)。
|
||||||
|
#
|
||||||
|
# APP_VARIANT / EXPO_PUBLIC_APP_VARIANT:控制 Bundle ID 与「关于」页是否显示后端地址
|
||||||
|
# development / staging → 显示版本 + API 地址;production → 仅版本号
|
||||||
#
|
#
|
||||||
# 变量在构建时注入;修改后需重新 prebuild/打包客户端。
|
# 变量在构建时注入;修改后需重新 prebuild/打包客户端。
|
||||||
#
|
#
|
||||||
# 助手朗读:无独立 EXPO_PUBLIC_* TTS 开关。会话页顶栏在每轮 WebSocket 中带 `tts_this_turn`;
|
# 助手朗读:无独立 EXPO_PUBLIC_* TTS 开关。会话页顶栏在每轮 WebSocket 中带 `tts_this_turn`;
|
||||||
# 服务端是否具备合成能力见 api/.env 中 ENABLE_TTS 等(模板见 api/.env.example)。
|
# 服务端是否具备合成能力见 api/.env 中 ENABLE_TTS 等(模板见 api/.env.example)。
|
||||||
|
|
||||||
EXPO_PUBLIC_API_URL=https://your-api.example.com
|
# --- development(本地)---
|
||||||
EXPO_PUBLIC_WS_URL=wss://your-api.example.com
|
# APP_VARIANT=development
|
||||||
|
# EXPO_PUBLIC_APP_VARIANT=development
|
||||||
|
# EXPO_PUBLIC_API_URL=http://127.0.0.1:8000
|
||||||
|
# EXPO_PUBLIC_WS_URL=ws://127.0.0.1:8000
|
||||||
|
|
||||||
|
# --- staging(预发 / TestFlight staging 包)---
|
||||||
|
# APP_VARIANT=staging
|
||||||
|
# EXPO_PUBLIC_APP_VARIANT=staging
|
||||||
|
# iOS Bundle ID: org.brighteng.lifecho.staging
|
||||||
|
# EXPO_PUBLIC_API_URL=http://your-staging-host:8000
|
||||||
|
# EXPO_PUBLIC_WS_URL=ws://your-staging-host:8000
|
||||||
|
|
||||||
|
# --- production(正式)---
|
||||||
|
# APP_VARIANT=production
|
||||||
|
# EXPO_PUBLIC_APP_VARIANT=production
|
||||||
|
# iOS Bundle ID: org.brighteng.lifecho
|
||||||
|
# EXPO_PUBLIC_API_URL=https://your-api.example.com
|
||||||
|
# EXPO_PUBLIC_WS_URL=wss://your-api.example.com
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
# 仅 API/WS 基址;TTS 每轮开关由运行时 WS payload 与服务端 ENABLE_TTS 控制(见 api/.env.example)。
|
# 正式:关于页仅显示版本号;iOS Bundle ID org.brighteng.lifecho
|
||||||
|
# TTS 每轮开关由运行时 WS payload 与服务端 ENABLE_TTS 控制(见 api/.env.example)。
|
||||||
|
APP_VARIANT=production
|
||||||
|
EXPO_PUBLIC_APP_VARIANT=production
|
||||||
EXPO_PUBLIC_API_URL=https://lifecho.worldsplats.com
|
EXPO_PUBLIC_API_URL=https://lifecho.worldsplats.com
|
||||||
EXPO_PUBLIC_WS_URL=wss://lifecho.worldsplats.com
|
EXPO_PUBLIC_WS_URL=wss://lifecho.worldsplats.com
|
||||||
|
|||||||
@@ -1,2 +1,5 @@
|
|||||||
EXPO_PUBLIC_API_URL=http://1.15.29.57:8000/
|
# 预发:关于页显示版本 + 后端地址;iOS Bundle ID org.brighteng.lifecho.staging
|
||||||
EXPO_PUBLIC_WS_URL=ws://1.15.29.57:8000/
|
APP_VARIANT=staging
|
||||||
|
EXPO_PUBLIC_APP_VARIANT=staging
|
||||||
|
EXPO_PUBLIC_API_URL=http://1.15.29.57:8000
|
||||||
|
EXPO_PUBLIC_WS_URL=ws://1.15.29.57:8000
|
||||||
|
|||||||
@@ -29,7 +29,28 @@ const LOCALES: Record<string, LocaleMessages> = {
|
|||||||
const SUPPORTED_LOCALES = ['zh', 'en'] as const;
|
const SUPPORTED_LOCALES = ['zh', 'en'] as const;
|
||||||
const PRIMARY_LOCALE = process.env.EXPO_PUBLIC_PRIMARY_LOCALE ?? 'zh';
|
const PRIMARY_LOCALE = process.env.EXPO_PUBLIC_PRIMARY_LOCALE ?? 'zh';
|
||||||
const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL ?? '';
|
const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL ?? '';
|
||||||
const ALLOW_ANDROID_CLEARTEXT_TRAFFIC = API_BASE_URL.startsWith('http://');
|
const ALLOW_INSECURE_HTTP = API_BASE_URL.startsWith('http://');
|
||||||
|
|
||||||
|
const APP_VARIANT =
|
||||||
|
process.env.APP_VARIANT ??
|
||||||
|
process.env.EXPO_PUBLIC_APP_VARIANT ??
|
||||||
|
'development';
|
||||||
|
const IS_STAGING = APP_VARIANT === 'staging';
|
||||||
|
const IS_PRODUCTION = APP_VARIANT === 'production';
|
||||||
|
|
||||||
|
const IOS_BUNDLE_IDENTIFIER = IS_STAGING
|
||||||
|
? 'org.brighteng.lifecho.staging'
|
||||||
|
: IS_PRODUCTION
|
||||||
|
? 'org.brighteng.lifecho'
|
||||||
|
: 'com.anonymous.app-expo';
|
||||||
|
|
||||||
|
const ANDROID_PACKAGE = IS_STAGING
|
||||||
|
? 'org.brighteng.lifecho.staging'
|
||||||
|
: IS_PRODUCTION
|
||||||
|
? 'org.brighteng.lifecho'
|
||||||
|
: 'com.anonymous.appexpo';
|
||||||
|
|
||||||
|
const APP_DISPLAY_NAME = IS_STAGING ? 'Life Echo (Staging)' : 'Life Echo';
|
||||||
|
|
||||||
const PERMISSION_FALLBACKS: Record<PermissionKey, string> = {
|
const PERMISSION_FALLBACKS: Record<PermissionKey, string> = {
|
||||||
microphone: 'Allow $(PRODUCT_NAME) to access your microphone.',
|
microphone: 'Allow $(PRODUCT_NAME) to access your microphone.',
|
||||||
@@ -106,7 +127,7 @@ export default ({ config }: ConfigContext): ExpoConfig => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...config,
|
...config,
|
||||||
name: 'Life Echo',
|
name: APP_DISPLAY_NAME,
|
||||||
slug: 'life-echo',
|
slug: 'life-echo',
|
||||||
version: '1.2.0',
|
version: '1.2.0',
|
||||||
orientation: 'portrait',
|
orientation: 'portrait',
|
||||||
@@ -116,7 +137,7 @@ export default ({ config }: ConfigContext): ExpoConfig => {
|
|||||||
ios: {
|
ios: {
|
||||||
...config?.ios,
|
...config?.ios,
|
||||||
icon: './assets/images/icon.png',
|
icon: './assets/images/icon.png',
|
||||||
bundleIdentifier: 'com.anonymous.app-expo',
|
bundleIdentifier: IOS_BUNDLE_IDENTIFIER,
|
||||||
config: {
|
config: {
|
||||||
usesNonExemptEncryption: false,
|
usesNonExemptEncryption: false,
|
||||||
},
|
},
|
||||||
@@ -140,7 +161,7 @@ export default ({ config }: ConfigContext): ExpoConfig => {
|
|||||||
*/
|
*/
|
||||||
softwareKeyboardLayoutMode: 'resize',
|
softwareKeyboardLayoutMode: 'resize',
|
||||||
// Reverse-DNS; no hyphens (Android package name rules). Matches iOS bundle id intent.
|
// Reverse-DNS; no hyphens (Android package name rules). Matches iOS bundle id intent.
|
||||||
package: 'com.anonymous.appexpo',
|
package: ANDROID_PACKAGE,
|
||||||
adaptiveIcon: {
|
adaptiveIcon: {
|
||||||
backgroundColor: '#E6F4FE',
|
backgroundColor: '#E6F4FE',
|
||||||
foregroundImage: './assets/images/android-icon-foreground.png',
|
foregroundImage: './assets/images/android-icon-foreground.png',
|
||||||
@@ -153,8 +174,9 @@ export default ({ config }: ConfigContext): ExpoConfig => {
|
|||||||
'./plugins/withAndroidReleaseSigning',
|
'./plugins/withAndroidReleaseSigning',
|
||||||
[
|
[
|
||||||
'./plugins/withAndroidCleartextTraffic',
|
'./plugins/withAndroidCleartextTraffic',
|
||||||
{ enabled: ALLOW_ANDROID_CLEARTEXT_TRAFFIC },
|
{ enabled: ALLOW_INSECURE_HTTP },
|
||||||
],
|
],
|
||||||
|
['./plugins/withIosInsecureHttp', { enabled: ALLOW_INSECURE_HTTP }],
|
||||||
'expo-router',
|
'expo-router',
|
||||||
[
|
[
|
||||||
'expo-splash-screen',
|
'expo-splash-screen',
|
||||||
|
|||||||
@@ -11,6 +11,9 @@
|
|||||||
"reset-project": "node ./scripts/reset-project.js",
|
"reset-project": "node ./scripts/reset-project.js",
|
||||||
"android": "npm run use-env -- development && expo run:android",
|
"android": "npm run use-env -- development && expo run:android",
|
||||||
"ios": "npm run use-env -- development && expo run:ios",
|
"ios": "npm run use-env -- development && expo run:ios",
|
||||||
|
"ios:prebuild": "bash scripts/ios-prebuild.sh staging",
|
||||||
|
"ios:prebuild:staging": "bash scripts/ios-prebuild.sh staging",
|
||||||
|
"ios:prebuild:production": "bash scripts/ios-prebuild.sh production",
|
||||||
"web": "npm run use-env -- development && expo start --web",
|
"web": "npm run use-env -- development && expo start --web",
|
||||||
"lint": "expo lint",
|
"lint": "expo lint",
|
||||||
"test": "jest --watch",
|
"test": "jest --watch",
|
||||||
|
|||||||
63
app-expo/plugins/withIosInsecureHttp.js
Normal file
63
app-expo/plugins/withIosInsecureHttp.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
// @ts-check
|
||||||
|
/**
|
||||||
|
* Allow HTTP / WS to staging API host via App Transport Security exception.
|
||||||
|
*
|
||||||
|
* Enabled when EXPO_PUBLIC_API_URL uses http:// (same rule as Android cleartext).
|
||||||
|
* Host is parsed from the URL so IP:port staging endpoints work without hard-coding.
|
||||||
|
*/
|
||||||
|
const { withInfoPlist } = require('@expo/config-plugins');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string | null}
|
||||||
|
*/
|
||||||
|
function getHttpExceptionHost() {
|
||||||
|
const raw = process.env.EXPO_PUBLIC_API_URL ?? '';
|
||||||
|
if (!raw.startsWith('http://')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new URL(raw).hostname;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('expo/config').ExpoConfig} config
|
||||||
|
* @param {{ enabled?: boolean }} props
|
||||||
|
*/
|
||||||
|
function withIosInsecureHttp(config, props = {}) {
|
||||||
|
const enabled = props.enabled ?? false;
|
||||||
|
|
||||||
|
return withInfoPlist(config, (mod) => {
|
||||||
|
if (!enabled) {
|
||||||
|
return mod;
|
||||||
|
}
|
||||||
|
|
||||||
|
const host = getHttpExceptionHost();
|
||||||
|
if (!host) {
|
||||||
|
console.warn(
|
||||||
|
'[withIosInsecureHttp] enabled but EXPO_PUBLIC_API_URL has no http host; skipping ATS exception.',
|
||||||
|
);
|
||||||
|
return mod;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = mod.modResults.NSAppTransportSecurity ?? {};
|
||||||
|
const existingDomains = existing.NSExceptionDomains ?? {};
|
||||||
|
|
||||||
|
mod.modResults.NSAppTransportSecurity = {
|
||||||
|
...existing,
|
||||||
|
NSExceptionDomains: {
|
||||||
|
...existingDomains,
|
||||||
|
[host]: {
|
||||||
|
NSExceptionAllowsInsecureHTTPLoads: true,
|
||||||
|
NSIncludesSubdomains: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return mod;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = withIosInsecureHttp;
|
||||||
67
app-expo/scripts/ios-prebuild.sh
Executable file
67
app-expo/scripts/ios-prebuild.sh
Executable file
@@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 本机 iOS Release:切换 env → expo prebuild → 打开 Xcode 打 Archive 上传 TestFlight。
|
||||||
|
#
|
||||||
|
# 用法(在仓库任意目录):
|
||||||
|
# app-expo/scripts/ios-prebuild.sh staging
|
||||||
|
# app-expo/scripts/ios-prebuild.sh production
|
||||||
|
#
|
||||||
|
# 或通过 npm(在 app-expo 目录):
|
||||||
|
# npm run ios:prebuild:staging
|
||||||
|
# npm run ios:prebuild:production
|
||||||
|
#
|
||||||
|
# Xcode 内后续步骤(脚本不会自动执行):
|
||||||
|
# 1. 选 Any iOS Device(或 Generic iOS Device)
|
||||||
|
# 2. Product → Archive
|
||||||
|
# 3. Distribute App → App Store Connect → Upload(进入 TestFlight)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ENV="${1:-}"
|
||||||
|
if [[ -z "$ENV" ]]; then
|
||||||
|
echo "Usage: $(basename "$0") <staging|production|development>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$ENV" in
|
||||||
|
staging | production | development) ;;
|
||||||
|
*)
|
||||||
|
echo "Unknown environment: $ENV (expected staging, production, or development)" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
echo "==> Switching to .env.${ENV}"
|
||||||
|
npm run use-env -- "$ENV"
|
||||||
|
|
||||||
|
echo "==> expo prebuild --platform ios --clean"
|
||||||
|
npx expo prebuild --platform ios --clean
|
||||||
|
|
||||||
|
shopt -s nullglob
|
||||||
|
WORKSPACES=(ios/*.xcworkspace)
|
||||||
|
shopt -u nullglob
|
||||||
|
|
||||||
|
if [[ ${#WORKSPACES[@]} -eq 0 ]]; then
|
||||||
|
echo "No ios/*.xcworkspace found after prebuild." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ${#WORKSPACES[@]} -gt 1 ]]; then
|
||||||
|
echo "Multiple workspaces found; opening the first: ${WORKSPACES[0]}" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> Opening ${WORKSPACES[0]}"
|
||||||
|
open "${WORKSPACES[0]}"
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
Done. In Xcode:
|
||||||
|
• Select "Any iOS Device" (or a connected device)
|
||||||
|
• Product → Archive
|
||||||
|
• Window → Organizer → Distribute App → App Store Connect
|
||||||
|
|
||||||
|
Environment: ${ENV}
|
||||||
|
API URL: $(grep -E '^EXPO_PUBLIC_API_URL=' .env 2>/dev/null | cut -d= -f2- || echo '(see .env)')
|
||||||
|
EOF
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
* 将 app-expo/.env.<name> 复制为 .env,供 Metro/Expo 读取 EXPO_PUBLIC_*。
|
* 将 app-expo/.env.<name> 复制为 .env,供 Metro/Expo 读取 EXPO_PUBLIC_*。
|
||||||
*
|
*
|
||||||
* 参数 name → 源文件:
|
* 参数 name → 源文件:
|
||||||
* development → .env.development(本地默认:npm start / prestart)
|
* development → .env.development(仓库已提交;npm start / prestart 默认)
|
||||||
* staging → .env.staging
|
* staging → .env.staging
|
||||||
* production → .env.production
|
* production → .env.production
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,25 +1,44 @@
|
|||||||
import Constants from 'expo-constants';
|
import Constants from 'expo-constants';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View } from 'react-native';
|
import { View } from 'react-native';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
import { Text } from '@/components/ui/text';
|
import { Text } from '@/components/ui/text';
|
||||||
import { ScreenHeader } from '@/components/screen-header';
|
import { ScreenHeader } from '@/components/screen-header';
|
||||||
|
import { config } from '@/core/config';
|
||||||
|
|
||||||
export default function AboutScreen() {
|
export default function AboutScreen() {
|
||||||
|
const { t } = useTranslation('profile');
|
||||||
const version = Constants.expoConfig?.version ?? '1.0.0';
|
const version = Constants.expoConfig?.version ?? '1.0.0';
|
||||||
|
const showBackend = config.showAboutBackendUrl;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="flex-1 bg-background">
|
<View className="flex-1 bg-background">
|
||||||
<ScreenHeader title="关于" />
|
<ScreenHeader title={t('about.title')} />
|
||||||
<SafeAreaView className="flex-1 items-center justify-center gap-4 px-6">
|
<SafeAreaView className="flex-1 items-center justify-center gap-4 px-6">
|
||||||
<Text variant="h2" className="text-foreground">
|
<Text variant="h2" className="text-foreground">
|
||||||
岁月时书
|
{t('about.appName')}
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="text-muted-foreground">Life Echo</Text>
|
<Text className="text-muted-foreground">{t('about.appSubtitle')}</Text>
|
||||||
<Text className="text-sm text-muted-foreground">版本 {version}</Text>
|
<Text className="text-sm text-muted-foreground">
|
||||||
|
{t('about.version', { version })}
|
||||||
|
</Text>
|
||||||
|
{showBackend ? (
|
||||||
|
<View className="w-full max-w-sm items-center gap-1">
|
||||||
|
<Text className="text-sm text-muted-foreground">
|
||||||
|
{t('about.backend')}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
selectable
|
||||||
|
className="text-center font-mono text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
|
{config.apiBaseUrl}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
<Text className="mt-8 text-center text-sm leading-5 text-muted-foreground">
|
<Text className="mt-8 text-center text-sm leading-5 text-muted-foreground">
|
||||||
记录你的人生故事,让回忆成书。
|
{t('about.tagline')}
|
||||||
</Text>
|
</Text>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -2,6 +2,26 @@ function trimTrailingSlashes(value: string): string {
|
|||||||
return value.replace(/\/+$/, '');
|
return value.replace(/\/+$/, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AppVariant = 'development' | 'staging' | 'production';
|
||||||
|
|
||||||
|
function resolveAppVariant(): AppVariant {
|
||||||
|
const raw = process.env.EXPO_PUBLIC_APP_VARIANT;
|
||||||
|
if (raw === 'development' || raw === 'staging' || raw === 'production') {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
if (__DEV__) {
|
||||||
|
return 'development';
|
||||||
|
}
|
||||||
|
return 'production';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Shown on About screen for dev/staging builds only. */
|
||||||
|
export function shouldShowAboutBackendUrl(variant: AppVariant = appVariant): boolean {
|
||||||
|
return variant === 'development' || variant === 'staging';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const appVariant = resolveAppVariant();
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
apiBaseUrl: trimTrailingSlashes(
|
apiBaseUrl: trimTrailingSlashes(
|
||||||
process.env.EXPO_PUBLIC_API_URL ?? 'http://192.168.10.151:8000',
|
process.env.EXPO_PUBLIC_API_URL ?? 'http://192.168.10.151:8000',
|
||||||
@@ -10,6 +30,8 @@ export const config = {
|
|||||||
process.env.EXPO_PUBLIC_WS_URL ?? 'ws://192.168.10.151:8000',
|
process.env.EXPO_PUBLIC_WS_URL ?? 'ws://192.168.10.151:8000',
|
||||||
),
|
),
|
||||||
isDebugMode: __DEV__,
|
isDebugMode: __DEV__,
|
||||||
|
appVariant,
|
||||||
|
showAboutBackendUrl: shouldShowAboutBackendUrl(),
|
||||||
|
|
||||||
api: {
|
api: {
|
||||||
timeoutMs: 30_000,
|
timeoutMs: 30_000,
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
{
|
{
|
||||||
"about": {
|
"about": {
|
||||||
"aboutUs": "About Us",
|
"aboutUs": "About Us",
|
||||||
"title": "About"
|
"appName": "Life Echo",
|
||||||
|
"appSubtitle": "岁月时书",
|
||||||
|
"backend": "API endpoint",
|
||||||
|
"tagline": "Capture your life story and turn memories into a book.",
|
||||||
|
"title": "About",
|
||||||
|
"version": "Version {{version}}"
|
||||||
},
|
},
|
||||||
"appExperience": {
|
"appExperience": {
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
{
|
{
|
||||||
"about": {
|
"about": {
|
||||||
"aboutUs": "关于我们",
|
"aboutUs": "关于我们",
|
||||||
"title": "关于"
|
"appName": "岁月时书",
|
||||||
|
"appSubtitle": "Life Echo",
|
||||||
|
"backend": "连接的后端",
|
||||||
|
"tagline": "记录你的人生故事,让回忆成书。",
|
||||||
|
"title": "关于",
|
||||||
|
"version": "版本 {{version}}"
|
||||||
},
|
},
|
||||||
"appExperience": {
|
"appExperience": {
|
||||||
"language": "语言",
|
"language": "语言",
|
||||||
|
|||||||
23
app-expo/tests/core/config.test.ts
Normal file
23
app-expo/tests/core/config.test.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import {
|
||||||
|
appVariant,
|
||||||
|
config,
|
||||||
|
shouldShowAboutBackendUrl,
|
||||||
|
type AppVariant,
|
||||||
|
} from '@/core/config';
|
||||||
|
|
||||||
|
describe('shouldShowAboutBackendUrl', () => {
|
||||||
|
it('shows backend URL for development and staging', () => {
|
||||||
|
expect(shouldShowAboutBackendUrl('development')).toBe(true);
|
||||||
|
expect(shouldShowAboutBackendUrl('staging')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hides backend URL for production', () => {
|
||||||
|
expect(shouldShowAboutBackendUrl('production')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches config.showAboutBackendUrl for current build', () => {
|
||||||
|
expect(config.showAboutBackendUrl).toBe(
|
||||||
|
shouldShowAboutBackendUrl(appVariant as AppVariant),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user