Files
life-echo/app-expo/app.config.ts
Kevin 8af37e5e8e 修复:CI 部署环境与 ref 错配、迁移碎片化、图片意图 source_span、章节物化脏版式、会话历史与本地语音不一致
新增:TTS 上传 COS 与分片、章节 reading_segments 物化与快照、markdown 清洗、会话消息 repository、语音 store 重构与相关测试
2026-03-20 16:43:02 +08:00

224 lines
6.2 KiB
TypeScript

import type { ConfigContext, ExpoConfig } from 'expo/config';
import zh from './locales/zh.json';
import en from './locales/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,
en,
};
const SUPPORTED_LOCALES = ['zh', 'en'] as const;
const PRIMARY_LOCALE = process.env.EXPO_PUBLIC_PRIMARY_LOCALE ?? 'zh';
const WEB_SQLITE_HEADERS = {
'Cross-Origin-Embedder-Policy': 'credentialless',
'Cross-Origin-Opener-Policy': 'same-origin',
} as const;
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: 'app-expo',
slug: 'app-expo',
version: '1.0.0',
orientation: 'portrait',
icon: './assets/images/icon.png',
scheme: 'appexpo',
userInterfaceStyle: 'automatic',
ios: {
...config?.ios,
icon: './assets/expo.icon',
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,
// 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',
backgroundImage: './assets/images/android-icon-background.png',
monochromeImage: './assets/images/android-icon-monochrome.png',
},
predictiveBackGestureEnabled: false,
},
web: {
...config?.web,
bundler: 'metro',
output: 'static',
favicon: './assets/images/favicon.png',
},
plugins: [
// --- Web: Required for expo-sqlite on web ---
// COEP/COOP headers enable SharedArrayBuffer in browsers.
// Also configure metro.config.js (wasm + dev server headers).
[
'expo-router',
{
headers: WEB_SQLITE_HEADERS,
},
],
[
'expo-splash-screen',
{
backgroundColor: '#208AEF',
android: {
image: './assets/images/splash-icon.png',
imageWidth: 76,
},
},
],
[
'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,
},
};
};