Align Expo config, package.json, and lockfile root with release tag v1.2.0. Co-authored-by: Cursor <cursoragent@cursor.com>
219 lines
6.6 KiB
TypeScript
219 lines
6.6 KiB
TypeScript
import type { ConfigContext, ExpoConfig } from 'expo/config';
|
||
|
||
import zh from './locales/zh.json';
|
||
import en from './locales/en.json';
|
||
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> = {
|
||
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';
|
||
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,
|
||
name: 'Life Echo',
|
||
slug: 'life-echo',
|
||
version: '1.2.0',
|
||
orientation: 'portrait',
|
||
icon: './assets/images/icon.png',
|
||
scheme: 'lifeecho',
|
||
userInterfaceStyle: 'automatic',
|
||
ios: {
|
||
...config?.ios,
|
||
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: [
|
||
// CI/local release: android/app/keystore.properties + store file → release signing; -PversionName/-PversionCode
|
||
'./plugins/withAndroidReleaseSigning',
|
||
[
|
||
'./plugins/withAndroidCleartextTraffic',
|
||
{ enabled: ALLOW_ANDROID_CLEARTEXT_TRAFFIC },
|
||
],
|
||
'expo-router',
|
||
[
|
||
'expo-splash-screen',
|
||
{
|
||
// 与 android.adaptiveIcon.backgroundColor、品牌浅紫一致(见 scripts/generate-app-icon.sh,源图为 assets/logo.png)
|
||
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,
|
||
},
|
||
};
|
||
};
|