Files
life-echo/app-expo/app.config.ts

219 lines
6.6 KiB
TypeScript
Raw Normal View History

import type { ConfigContext, ExpoConfig } from 'expo/config';
import zh from './locales/zh.json';
import en from './locales/en.json';
2026-03-22 19:22:34 +08:00
import permissionsZh from './locales/permissions/zh.json';
import permissionsEn from './locales/permissions/en.json';
type PermissionKey =
| 'microphone'
| 'photos'
| 'camera'
| 'microphoneForVideo'
| 'faceId';
type LocaleMessages = {
permissions?: Partial<Record<PermissionKey, string>>;
};
type PrivacyAccessedAPIType = {
NSPrivacyAccessedAPIType: string;
NSPrivacyAccessedAPITypeReasons: string[];
};
const LOCALES: Record<string, LocaleMessages> = {
2026-03-22 19:22:34 +08:00
zh: { ...zh, permissions: permissionsZh },
en: { ...en, permissions: permissionsEn },
};
const SUPPORTED_LOCALES = ['zh', 'en'] as const;
const PRIMARY_LOCALE = process.env.EXPO_PUBLIC_PRIMARY_LOCALE ?? 'zh';
2026-05-09 16:16:48 +08:00
const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL ?? '';
const ALLOW_ANDROID_CLEARTEXT_TRAFFIC = API_BASE_URL.startsWith('http://');
const PERMISSION_FALLBACKS: Record<PermissionKey, string> = {
microphone: 'Allow $(PRODUCT_NAME) to access your microphone.',
photos: 'Allow $(PRODUCT_NAME) to access your photos.',
camera: 'Allow $(PRODUCT_NAME) to access your camera.',
microphoneForVideo: 'Allow $(PRODUCT_NAME) to access your microphone.',
faceId: 'Allow $(PRODUCT_NAME) to access Face ID.',
};
/**
* iOS privacy manifest required-reason APIs.
*
* These values are aggregated from the PrivacyInfo.xcprivacy files shipped by the
* Expo native modules currently used in this app, because Apple may not always
* correctly parse all static CocoaPods dependency manifests during submission.
*
* Sources checked in node_modules:
* - expo-task-manager
* - expo-media-library
* - expo-localization
* - expo-file-system
* - expo-system-ui
* - expo-device
* - expo-constants
*/
const REQUIRED_PRIVACY_ACCESSED_API_TYPES: PrivacyAccessedAPIType[] = [
{
NSPrivacyAccessedAPIType: 'NSPrivacyAccessedAPICategoryUserDefaults',
NSPrivacyAccessedAPITypeReasons: ['CA92.1'],
},
{
NSPrivacyAccessedAPIType: 'NSPrivacyAccessedAPICategoryFileTimestamp',
NSPrivacyAccessedAPITypeReasons: ['0A2A.1', '3B52.1'],
},
{
NSPrivacyAccessedAPIType: 'NSPrivacyAccessedAPICategoryDiskSpace',
NSPrivacyAccessedAPITypeReasons: ['E174.1', '85F4.1'],
},
{
NSPrivacyAccessedAPIType: 'NSPrivacyAccessedAPICategorySystemBootTime',
NSPrivacyAccessedAPITypeReasons: ['35F9.1'],
},
];
const localePermissions = LOCALES[PRIMARY_LOCALE]?.permissions ?? {};
function getPermissionMessage(key: PermissionKey): string {
return localePermissions[key] ?? PERMISSION_FALLBACKS[key];
}
function mergePrivacyAccessedApiTypes(
existing: PrivacyAccessedAPIType[] = [],
required: PrivacyAccessedAPIType[] = [],
): PrivacyAccessedAPIType[] {
const merged = new Map<string, Set<string>>();
for (const item of [...existing, ...required]) {
const reasons =
merged.get(item.NSPrivacyAccessedAPIType) ?? new Set<string>();
for (const reason of item.NSPrivacyAccessedAPITypeReasons) {
reasons.add(reason);
}
merged.set(item.NSPrivacyAccessedAPIType, reasons);
}
return Array.from(merged.entries()).map(([apiType, reasons]) => ({
NSPrivacyAccessedAPIType: apiType,
NSPrivacyAccessedAPITypeReasons: Array.from(reasons),
}));
}
export default ({ config }: ConfigContext): ExpoConfig => {
const existingPrivacyAccessedApiTypes = (config?.ios?.privacyManifests
?.NSPrivacyAccessedAPITypes ?? []) as PrivacyAccessedAPIType[];
return {
...config,
2026-03-23 11:33:02 +08:00
name: 'Life Echo',
slug: 'life-echo',
version: '1.2.0',
orientation: 'portrait',
icon: './assets/images/icon.png',
2026-03-23 11:33:02 +08:00
scheme: 'lifeecho',
userInterfaceStyle: 'automatic',
ios: {
...config?.ios,
2026-03-23 10:25:51 +08:00
icon: './assets/images/icon.png',
bundleIdentifier: 'com.anonymous.app-expo',
config: {
usesNonExemptEncryption: false,
},
infoPlist: {
...config?.ios?.infoPlist,
CFBundleAllowMixedLocalizations: true,
},
privacyManifests: {
...config?.ios?.privacyManifests,
NSPrivacyAccessedAPITypes: mergePrivacyAccessedApiTypes(
existingPrivacyAccessedApiTypes,
REQUIRED_PRIVACY_ACCESSED_API_TYPES,
),
},
},
android: {
...config?.android,
/**
* `resize` `adjustResize`
* `KeyboardAvoidingView` + `behavior="height"` Expo Keyboard
*/
softwareKeyboardLayoutMode: 'resize',
// Reverse-DNS; no hyphens (Android package name rules). Matches iOS bundle id intent.
package: 'com.anonymous.appexpo',
adaptiveIcon: {
backgroundColor: '#E6F4FE',
foregroundImage: './assets/images/android-icon-foreground.png',
monochromeImage: './assets/images/android-icon-monochrome.png',
},
predictiveBackGestureEnabled: false,
},
plugins: [
2026-03-20 17:25:42 +08:00
// CI/local release: android/app/keystore.properties + store file → release signing; -PversionName/-PversionCode
'./plugins/withAndroidReleaseSigning',
2026-05-09 16:16:48 +08:00
[
'./plugins/withAndroidCleartextTraffic',
{ enabled: ALLOW_ANDROID_CLEARTEXT_TRAFFIC },
],
'expo-router',
[
'expo-splash-screen',
{
// 与 android.adaptiveIcon.backgroundColor、品牌浅紫一致见 scripts/generate-app-icon.sh源图为 assets/logo.png
2026-03-23 10:25:51 +08:00
backgroundColor: '#E6F4FE',
image: './assets/images/splash-icon.png',
resizeMode: 'contain',
imageWidth: 200,
},
],
[
'expo-localization',
{
supportedLocales: {
ios: SUPPORTED_LOCALES,
android: SUPPORTED_LOCALES,
},
},
],
'expo-asset',
[
'expo-audio',
{
microphonePermission: getPermissionMessage('microphone'),
},
],
[
'expo-image-picker',
{
photosPermission: getPermissionMessage('photos'),
cameraPermission: getPermissionMessage('camera'),
microphonePermission: getPermissionMessage('microphoneForVideo'),
},
],
'expo-image',
'expo-sqlite',
[
'expo-secure-store',
{
configureAndroidBackup: true,
faceIDPermission: getPermissionMessage('faceId'),
},
],
'expo-font',
],
locales: {
zh: './locales/zh.json',
en: './locales/en.json',
},
extra: {
...config?.extra,
supportsRTL: true,
},
experiments: {
...config?.experiments,
typedRoutes: true,
reactCompiler: true,
},
};
};