const fs = require("node:fs"); const path = require("node:path"); const pkg = require("./package.json"); const withFdroidAutolinking = require("./plugins/with-fdroid-autolinking"); const appVariant = process.env.APP_VARIANT ?? "production"; const isFdroidBuild = process.env.PASEO_FDROID_BUILD === "1"; const buildProfile = isFdroidBuild ? { androidPermissions: [ "RECORD_AUDIO", "android.permission.RECORD_AUDIO", "android.permission.MODIFY_AUDIO_SETTINGS", ], cameraPlugins: [], fdroidPlugins: [withFdroidAutolinking], notificationPlugins: [], updates: { enabled: false }, } : { androidPermissions: [ "RECORD_AUDIO", "android.permission.RECORD_AUDIO", "android.permission.MODIFY_AUDIO_SETTINGS", "CAMERA", "android.permission.CAMERA", ], cameraPlugins: [ [ "expo-camera", { cameraPermission: "Allow $(PRODUCT_NAME) to access your camera to scan pairing QR codes.", }, ], ], fdroidPlugins: [], notificationPlugins: [ [ "expo-notifications", { icon: "./assets/images/notification-icon.png", color: "#20744A", }, ], ], updates: {}, }; function getNativeBuildVersionCode(version) { const match = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(version); if (!match) { throw new Error(`Cannot derive Android versionCode from non-semver version: ${version}`); } const [, majorText, minorText, patchText] = match; const major = Number(majorText); const minor = Number(minorText); const patch = Number(patchText); if (minor > 999 || patch > 999) { throw new Error(`Cannot derive collision-free Android versionCode from version: ${version}`); } const versionCode = major * 1_000_000 + minor * 1_000 + patch; if (!Number.isSafeInteger(versionCode) || versionCode <= 0 || versionCode > 2_100_000_000) { throw new Error(`Derived Android versionCode is out of range: ${versionCode}`); } return versionCode; } function resolveSecretFile(params) { const fromEnv = process.env[params.envKey]; if (typeof fromEnv === "string" && fromEnv.trim().length > 0) { return fromEnv.trim(); } const fallbackAbsolutePath = path.resolve(__dirname, params.fallbackRelativePath); if (fs.existsSync(fallbackAbsolutePath)) { return params.fallbackRelativePath; } return undefined; } const variants = { production: { name: "Paseo", packageId: "sh.paseo", googleServicesFile: resolveSecretFile({ envKey: "GOOGLE_SERVICES_FILE_PROD", fallbackRelativePath: "./.secrets/google-services.prod.json", }), googleServiceInfoPlist: resolveSecretFile({ envKey: "GOOGLE_SERVICE_INFO_PLIST_PROD", fallbackRelativePath: "./.secrets/GoogleService-Info.prod.plist", }), }, development: { name: "Paseo Debug", packageId: "sh.paseo.debug", googleServicesFile: resolveSecretFile({ envKey: "GOOGLE_SERVICES_FILE_DEBUG", fallbackRelativePath: "./.secrets/google-services.debug.json", }), googleServiceInfoPlist: resolveSecretFile({ envKey: "GOOGLE_SERVICE_INFO_PLIST_DEBUG", fallbackRelativePath: "./.secrets/GoogleService-Info.debug.plist", }), }, }; const variant = variants[appVariant] ?? variants.production; const nativeBuildVersionCode = getNativeBuildVersionCode(pkg.version); export default { expo: { name: variant.name, slug: "voice-mobile", version: pkg.version, orientation: "portrait", icon: "./assets/images/icon.png", scheme: "paseo", userInterfaceStyle: "automatic", newArchEnabled: true, runtimeVersion: { policy: "appVersion", }, updates: { url: "https://u.expo.dev/0e7f65ce-0367-46c8-a238-2b65963d235a", ...buildProfile.updates, }, ios: { supportsTablet: true, infoPlist: { NSMicrophoneUsageDescription: "This app needs access to the microphone for voice commands.", ITSAppUsesNonExemptEncryption: false, }, bundleIdentifier: variant.packageId, ...(variant.googleServiceInfoPlist ? { googleServicesFile: variant.googleServiceInfoPlist } : {}), buildNumber: String(nativeBuildVersionCode), }, android: { adaptiveIcon: { backgroundColor: "#000000", foregroundImage: "./assets/images/android-icon-foreground.png", }, edgeToEdgeEnabled: true, predictiveBackGestureEnabled: false, softwareKeyboardLayoutMode: "resize", // Allow HTTP connections for local network hosts (required for release builds) usesCleartextTraffic: true, permissions: buildProfile.androidPermissions, package: variant.packageId, versionCode: nativeBuildVersionCode, ...(variant.googleServicesFile ? { googleServicesFile: variant.googleServicesFile } : {}), }, web: { output: "single", favicon: "./assets/images/favicon.png", }, autolinking: { searchPaths: ["../../node_modules", "./node_modules"], }, plugins: [ "expo-router", ...buildProfile.cameraPlugins, [ "expo-splash-screen", { image: "./assets/images/splash-icon.png", imageWidth: 200, resizeMode: "contain", backgroundColor: "#ffffff", dark: { backgroundColor: "#000000", }, }, ], ...buildProfile.notificationPlugins, "expo-audio", [ "expo-gradle-jvmargs", { xmx: "4096m", maxMetaspace: "1024m", }, ], [ "expo-build-properties", { android: { minSdkVersion: 29, kotlinVersion: "2.1.20", // Allow HTTP connections for local network hosts in release builds usesCleartextTraffic: true, }, }, ], ...buildProfile.fdroidPlugins, ], experiments: { typedRoutes: true, reactCompiler: true, autolinkingModuleResolution: true, }, extra: { fdroidBuild: isFdroidBuild, router: {}, eas: { projectId: "0e7f65ce-0367-46c8-a238-2b65963d235a", }, }, owner: "getpaseo", }, };