// The Gradle build resolves this config in a plain node process that does not // read .env, unlike `expo start` / `expo prebuild`. Without this the release // APK was written with the placeholder origin below and could not reach the // API at all. @expo/env is Expo's own loader — the same one the CLI uses. require("@expo/env").load(__dirname); // Dynamic Expo config. We use a JS config (rather than static app.json) so the // Android Google Maps API key can be pulled from EXPO_PUBLIC_GOOGLE_API_KEY // at build time without committing the key to the repo. // // react-native-maps renders blank tiles (just the Google logo, nothing else) // on Android when no Maps API key is set in the AndroidManifest. Expo's // prebuild reads `android.config.googleMaps.apiKey` and writes it to the // manifest as com.google.android.geo.API_KEY — that is what makes the map // actually draw. On iOS the default provider is Apple Maps, which needs no // key, so nothing is injected there. const googleMapsApiKey = process.env.EXPO_PUBLIC_GOOGLE_API_KEY; // Expo Router resolves relative API-route fetches ("/(api)/auth/login") against // this origin. In development it is overridden with the dev server URL, so the // placeholder never mattered; a release build has no such override and would // send every request to example.com. Point it at the same host that serves the // API routes. const serverOrigin = process.env.EXPO_PUBLIC_SERVER_URL || "https://example.com/"; // Adds the SYSTEM_ALERT_WINDOW permission to the AndroidManifest so the app // can request "display over other apps". The grant itself is a special // permission the user must toggle in system settings — it can't be requested // at runtime — but the manifest entry is what makes that system screen offer // the switch for our app. const { withAndroidManifest } = require("@expo/config-plugins"); // Release builds block cleartext HTTP: only src/debug/AndroidManifest.xml opts // in. A LAN test build talks to the dev server over http://, so allow it for // every build type. Drop this plugin once the API is served over https. const withCleartextTraffic = (config) => withAndroidManifest(config, (cfg) => { const application = cfg.modResults.manifest.application?.[0]; if (application) { application.$["android:usesCleartextTraffic"] = "true"; } return cfg; }); const withOverlayPermission = (config) => withAndroidManifest(config, (cfg) => { const manifest = cfg.modResults.manifest; manifest["uses-permission"] = manifest["uses-permission"] || []; const alreadyDeclared = manifest["uses-permission"].some( (entry) => entry.$ && entry.$["android:name"] === "android.permission.SYSTEM_ALERT_WINDOW", ); if (!alreadyDeclared) { manifest["uses-permission"].push({ $: { "android:name": "android.permission.SYSTEM_ALERT_WINDOW" }, }); } return cfg; }); // In-app WebRTC audio calls need the microphone. react-native-webrtc ships no // Expo config plugin, so both platforms' mic permissions are declared here: // the iOS Info.plist usage string lives in `ios.infoPlist` below, and the // Android RECORD_AUDIO / MODIFY_AUDIO_SETTINGS permissions are added to the // manifest at prebuild time — the grant itself is requested at runtime from // the call screen. const withMicPermission = (config) => withAndroidManifest(config, (cfg) => { const manifest = cfg.modResults.manifest; manifest["uses-permission"] = manifest["uses-permission"] || []; const needed = [ "android.permission.RECORD_AUDIO", "android.permission.MODIFY_AUDIO_SETTINGS", ]; for (const name of needed) { const exists = manifest["uses-permission"].some( (entry) => entry.$ && entry.$["android:name"] === name, ); if (!exists) { manifest["uses-permission"].push({ $: { "android:name": name } }); } } return cfg; }); // expo-image-picker's own plugin never declares CAMERA on Android — it only // blocks permissions when you ask it to. Without the declaration, // requestCameraPermissionsAsync() is auto-denied by the system and the driver // hits "allow camera access" with no way to allow it. READ_MEDIA_IMAGES is the // Android 13+ replacement for READ_EXTERNAL_STORAGE, needed for the gallery // option on the document scanners. const withCapturePermissions = (config) => withAndroidManifest(config, (cfg) => { const manifest = cfg.modResults.manifest; manifest["uses-permission"] = manifest["uses-permission"] || []; const needed = [ "android.permission.CAMERA", "android.permission.READ_MEDIA_IMAGES", ]; for (const name of needed) { const exists = manifest["uses-permission"].some( (entry) => entry.$ && entry.$["android:name"] === name, ); if (!exists) { manifest["uses-permission"].push({ $: { "android:name": name } }); } } return cfg; }); module.exports = ({ config }) => ({ ...config, name: "Waseel", description: "Find your perfect ride with Waseel.", githubUrl: "https://github.com/sanidhyy/uber-clone", slug: "waseel", version: "1.0.0", orientation: "portrait", icon: "./assets/images/icon.png", scheme: "waseel", userInterfaceStyle: "automatic", splash: { image: "./assets/images/splash.png", resizeMode: "contain", backgroundColor: "#2F80ED", }, ios: { supportsTablet: true, bundleIdentifier: "com.waseel.app", infoPlist: { NSMicrophoneUsageDescription: "Waseel uses the microphone for in-app calls with your driver.", }, }, android: { adaptiveIcon: { foregroundImage: "./assets/images/adaptive-icon.png", backgroundColor: "#ffffff", }, package: "com.waseel.app", // NOTE: minSdkVersion is NOT set here. `android.minSdkVersion` is not a // field Expo's config schema recognises, so prebuild silently ignored it // and generated a project defaulting to 23 — which the manifest merger // then rejected against react-native-webrtc's minSdk 24. It lives in the // expo-build-properties plugin below, which is the supported way to set // it and the only way it survives `prebuild --clean`. config: { googleMaps: { apiKey: googleMapsApiKey ?? "", }, }, }, web: { bundler: "metro", output: "server", favicon: "./assets/images/favicon.png", }, plugins: [ // react-native-webrtc declares minSdk 24, and the Android manifest merger // refuses to build an app that declares less than a library it links. // Expo's generated project defaults to 23, so this has to be raised // explicitly — and it has to be raised *here*, because a value written // into android/build.gradle or gradle.properties by hand is destroyed by // the next `prebuild --clean`. [ "expo-build-properties", { android: { minSdkVersion: 24, }, }, ], [ "expo-router", { origin: serverOrigin, }, ], // Drivers are tracked while they're online, and that has to survive the // screen going off — dispatch drops anyone whose last ping is over 60s // old. The foreground service is what keeps the updates flowing on // Android, and it declares the FOREGROUND_SERVICE_LOCATION permission and // the `location` service type that Android 14 requires. It also puts a // persistent notification in the shade, which is the honest way to run // background GPS: the driver can always see that it's on. [ "expo-location", { locationAlwaysAndWhenInUsePermission: "Waseel uses your location while you're online to match you with nearby riders and show them your car on the map.", isAndroidBackgroundLocationEnabled: true, isAndroidForegroundServiceEnabled: true, }, ], // Ride-offer alerts. The tint colour matches the app's primary so the // small status-bar icon isn't rendered in Android's default grey. [ "expo-notifications", { color: "#0286FF", }, ], // Driver onboarding photographs the licence, ID card and vehicle // registration so the details can be read off them and a reviewer can see // the document itself. The gallery is offered alongside the camera for // documents because drivers often already have a photo of their papers; // the profile selfie is camera-only and enforced in the component. // // Do NOT add `microphonePermission: false` here. It reads as "this picker // doesn't need the mic", but the plugin implements it as // withBlockedPermissions — which stamps tools:node="remove" on // RECORD_AUDIO and strips it from the *merged* manifest, taking // react-native-webrtc's in-app calls down with it. Leaving it unset lets // the picker declare RECORD_AUDIO harmlessly alongside the calls flow. [ "expo-image-picker", { cameraPermission: "Waseel uses the camera to take your driver photo and scan your licence and vehicle papers.", photosPermission: "Waseel needs your photo library so you can upload a picture of your driving licence and vehicle papers.", }, ], withOverlayPermission, withMicPermission, withCapturePermissions, withCleartextTraffic, ], experiments: { typedRoutes: true, }, extra: { router: { origin: serverOrigin, }, }, });