diff --git a/client_app/BUILD_FLAVORS.md b/client_app/BUILD_FLAVORS.md new file mode 100644 index 0000000..84068a5 --- /dev/null +++ b/client_app/BUILD_FLAVORS.md @@ -0,0 +1,184 @@ +# Build Flavors — client_app (Android + iOS identity) + +Three build flavors map each environment to its own app ID, backend URL, app +display name, and Firebase config. This lets dev / staging / prod builds sit +**side-by-side** on one device (distinct package / bundle IDs). + +> Scope: **Android flavors + Dart + env files are fully wired.** On iOS, only +> the **bundle identity** is set (see the iOS section below); the iOS **Xcode +> build-configuration + scheme wiring** (per-flavor bundle suffixing, +> per-scheme `GoogleService-Info.plist`) is a separate follow-up that needs a +> Mac and is NOT done here. + +## Flavor table + +| Flavor | applicationId | App name | API_BASE_URL | +|----------|-------------------------|--------------------|-------------------------------------------| +| dev | `com.mybestie.dev` | HaloBestie Dev | `http://192.168.88.247:3000` | +| staging | `com.mybestie.staging` | HaloBestie Staging | `https://staging-api.halobestie.com` ⚠️\* | +| prod | `com.mybestie` | HaloBestie | `https://api.halobestie.com` | + +\* staging URL is a **PLACEHOLDER** — confirm the real staging host and update +`env/staging.json` (see the `_TODO` key there). + +The `applicationId` is set in `android/app/build.gradle.kts` via +`applicationIdSuffix` (dev/staging) on top of the base `com.mybestie`; prod has +no suffix. The app name comes from a per-flavor `resValue("string","app_name",…)` +consumed by `android:label="@string/app_name"` in `AndroidManifest.xml`. + +## iOS bundle identity — DIFFERENT base from Android ⚠️ + +The customer app already exists on the App Store under bundle ID +**`com.asc.hallobestie`** (published by a prior vendor — note the `asc` prefix +and `hallobestie` spelling). **Decision: v2 ships as an _update_ to that existing +listing**, so the iOS **prod** bundle ID must stay `com.asc.hallobestie` (you +cannot change a published app's bundle ID without it becoming a new app). The +iOS flavors therefore suffix **that** base, NOT the Android `com.mybestie` base: + +| Flavor | Android `applicationId` | iOS bundle ID | +|---------|-------------------------|--------------------------------| +| dev | `com.mybestie.dev` | `com.asc.hallobestie.dev` | +| staging | `com.mybestie.staging` | `com.asc.hallobestie.staging` | +| prod | `com.mybestie` | `com.asc.hallobestie` | + +A bundle ID differing across platforms is normal. What's done so far on iOS: +- `ios/Runner.xcodeproj/project.pbxproj` — base `PRODUCT_BUNDLE_IDENTIFIER` set + to `com.asc.hallobestie` (+ `com.asc.hallobestie.RunnerTests`). Once iOS + schemes are wired, dev/staging suffix off this base. +- `firebase_options_{dev,staging,prod}.dart` — `iosBundleId` set to the table + above. dev's iOS appId/apiKey are now PLACEHOLDERs (the old `com.mybestie` + iOS app no longer matches) — register `com.asc.hallobestie.dev` in the dev + project and paste the values. +- `ios/Runner/Info.plist` — `CFBundleURLName` aligned to `com.asc.hallobestie` + (the `halobestie://` deeplink scheme itself is unchanged). + +Still TODO on iOS (the deferred Mac/Xcode work): +- The existing `ios/Runner/GoogleService-Info.plist` still lists `com.mybestie` + — replace per flavor once the iOS apps are registered. +- Xcode build configs + schemes to actually apply the `.dev`/`.staging` bundle + suffixes per build, with a copy-script selecting the right `GoogleService-Info.plist`. +- You need access to the **`asc` Apple Developer account** + its signing + certs/provisioning profiles to release the prod update. + +> **mitra app is unaffected** — it's a brand-new app (`com.mybestie.mitra` + +> suffixes) on both platforms; only the customer iOS app inherits the legacy +> `com.asc.hallobestie` identity. + +## How the pieces fit + +| Concern | dev | staging | prod | +|----------------------|---------------------------------------|--------------------------------------------|-----------------------------------------| +| Dart entrypoint | `lib/main_dev.dart` | `lib/main_staging.dart` | `lib/main_prod.dart` | +| Firebase Dart opts | `lib/firebase/firebase_options_dev.dart` | `…_staging.dart` (PLACEHOLDER) | `…_prod.dart` (PLACEHOLDER) | +| Android Firebase cfg | `android/app/src/dev/google-services.json` | `android/app/src/staging/…` (MISSING) | `android/app/src/prod/…` (MISSING) | +| Env / dart-define | `env/dev.json` | `env/staging.json` | `env/prod.json` | + +A bare `flutter run` (no `-t`) still works and defaults to **dev**: +`lib/main.dart`'s `main()` delegates to `bootstrap(flavor: 'dev', …)`. + +## Run / build commands + +Each command needs three things: `--flavor `, `-t lib/main_.dart`, and +`--dart-define-from-file=env/.json`. + +### Run on a connected device/emulator +```bash +# dev +flutter run --flavor dev -t lib/main_dev.dart --dart-define-from-file=env/dev.json + +# staging +flutter run --flavor staging -t lib/main_staging.dart --dart-define-from-file=env/staging.json + +# prod +flutter run --flavor prod -t lib/main_prod.dart --dart-define-from-file=env/prod.json +``` + +### Build a release APK +```bash +# dev +flutter build apk --flavor dev -t lib/main_dev.dart --dart-define-from-file=env/dev.json + +# staging +flutter build apk --flavor staging -t lib/main_staging.dart --dart-define-from-file=env/staging.json + +# prod +flutter build apk --flavor prod -t lib/main_prod.dart --dart-define-from-file=env/prod.json +``` + +### Build an App Bundle (Play release) +```bash +flutter build appbundle --flavor prod -t lib/main_prod.dart --dart-define-from-file=env/prod.json +``` + +Output APKs land in `build/app/outputs/flutter-apk/app--release.apk`. + +## ⚠️ CRITICAL warnings + +### (a) Every build/install/run command now needs `--flavor` +Once product flavors exist, a bare `flutter build apk` / `flutter install` / +`flutter run` **with no `--flavor`** FAILS (Gradle can't pick a flavor). All +build, install, and run invocations — CI scripts, local muscle memory, IDE +launch configs — must pass `--flavor ` plus the matching +`-t lib/main_.dart` and `--dart-define-from-file=env/.json`. + +### (b) The dev applicationId is now `com.mybestie.dev`, not `com.mybestie` +Anything that references the package name by string must be updated for dev: + +- **GA4 DebugView:** + ```bash + adb shell setprop debug.firebase.analytics.app com.mybestie.dev + ``` +- **adb commands** (force-stop, pm clear, am start, grant, etc.): + ```bash + adb shell am force-stop com.mybestie.dev + adb shell pm clear com.mybestie.dev + ``` +- **Maestro flows:** any `appId: com.mybestie` must become `appId: com.mybestie.dev` + for the dev build. Check `.maestro/` configs and per-flow `appId` headers. + +Staging uses `com.mybestie.staging`; prod stays `com.mybestie`. + +## Firebase config — STATUS: configured ✅ (2026-06-04) + +All apps are registered and config files are in place for **both platforms**, +across 3 projects (one Firebase project per env): + +| Env | Firebase project | Android applicationId | iOS bundle ID | +|---------|------------------------|------------------------|-------------------------------| +| dev | `halobestie-clone-dev` | `com.mybestie.dev` | `com.asc.hallobestie.dev` | +| staging | `my-bestie-876ec` | `com.mybestie.staging` | `com.asc.hallobestie.staging` | +| prod | `my-bestie-production` | `com.mybestie` | `com.asc.hallobestie` | + +In place and verified: +- `android/app/src//google-services.json` — all 3, each containing a + client matching the flavor `applicationId`. +- `ios/config//GoogleService-Info.plist` — all 3, bundle IDs verified. +- `lib/firebase/firebase_options_{dev,staging,prod}.dart` — real android + iOS + values (regenerated from the native files above; no placeholders left). + +### Regenerating after any ID / key change +Either re-run flutterfire per flavor, or re-download the native files and +re-extract. Project IDs: +```bash +flutterfire configure --project=halobestie-clone-dev --out=lib/firebase/firebase_options_dev.dart +flutterfire configure --project=my-bestie-876ec --out=lib/firebase/firebase_options_staging.dart +flutterfire configure --project=my-bestie-production --out=lib/firebase/firebase_options_prod.dart +``` + +### Still TODO — iOS only (Mac/Xcode) +- [ ] iOS **Xcode schemes + build-phase copy script** to apply per-flavor bundle + suffixes and select the right `GoogleService-Info.plist` at build time. + Until then, iOS bundles only `ios/Runner/GoogleService-Info.plist`. +- [ ] Replace `ios/Runner/GoogleService-Info.plist` (still the legacy + `com.mybestie` dev file) — or let the copy script overwrite it per build. +- [ ] Prod iOS release needs the **`asc` Apple Developer account** + signing. + +## Files in this setup + +- `android/app/build.gradle.kts` — `flavorDimensions` + `productFlavors` +- `android/app/src/main/AndroidManifest.xml` — `android:label="@string/app_name"` +- `lib/main.dart` — `bootstrap()` + dev-default `main()` +- `lib/main_dev.dart` / `lib/main_staging.dart` / `lib/main_prod.dart` +- `lib/firebase/firebase_options_{dev,staging,prod}.dart` +- `env/{dev,staging,prod}.json` +- `android/app/src/{dev,staging,prod}/` Firebase config source sets diff --git a/client_app/android/app/build.gradle.kts b/client_app/android/app/build.gradle.kts index 313c989..5edeaa9 100644 --- a/client_app/android/app/build.gradle.kts +++ b/client_app/android/app/build.gradle.kts @@ -24,7 +24,10 @@ android { } defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + // Base application ID. Per-flavor suffixes below produce the final IDs: + // dev -> com.mybestie.dev + // staging -> com.mybestie.staging + // prod -> com.mybestie applicationId = "com.mybestie" // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. @@ -34,6 +37,34 @@ android { versionName = flutter.versionName } + // Build flavors for the three environments. Each flavor: + // - sets its final applicationId (via suffix, except prod) + // - injects an `app_name` string resource consumed by + // AndroidManifest.xml's android:label="@string/app_name" + // - selects its own Firebase config via the matching source set + // (android/app/src//google-services.json) + // NOTE: once these flavors exist, a bare `flutter build apk` (no --flavor) + // fails. All build/install/run commands MUST pass --flavor. See + // BUILD_FLAVORS.md. + flavorDimensions += "env" + productFlavors { + create("dev") { + dimension = "env" + applicationIdSuffix = ".dev" + resValue("string", "app_name", "HaloBestie Dev") + } + create("staging") { + dimension = "env" + applicationIdSuffix = ".staging" + resValue("string", "app_name", "HaloBestie Staging") + } + create("prod") { + dimension = "env" + // No applicationIdSuffix -> final applicationId stays "com.mybestie". + resValue("string", "app_name", "HaloBestie") + } + } + buildTypes { release { // TODO: Add your own signing config for the release build. diff --git a/mitra_app/android/app/google-services.json b/client_app/android/app/src/dev/google-services.json similarity index 81% rename from mitra_app/android/app/google-services.json rename to client_app/android/app/src/dev/google-services.json index d743396..d77e081 100755 --- a/mitra_app/android/app/google-services.json +++ b/client_app/android/app/src/dev/google-services.json @@ -62,6 +62,25 @@ } } }, + { + "client_info": { + "mobilesdk_app_id": "1:1068156046511:android:1f589ed358ccdad0b8185a", + "android_client_info": { + "package_name": "com.mybestie.dev" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyDFWlLSWytqwI7LSdUbVrO7J5De9L2LV2U" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + }, { "client_info": { "mobilesdk_app_id": "1:1068156046511:android:8a873c8b7e64410ab8185a", diff --git a/client_app/android/app/src/main/AndroidManifest.xml b/client_app/android/app/src/main/AndroidManifest.xml index 1e3102b..205ce2b 100644 --- a/client_app/android/app/src/main/AndroidManifest.xml +++ b/client_app/android/app/src/main/AndroidManifest.xml @@ -3,7 +3,7 @@ Phase 4 Stage 4 notif-gate via permission_handler. --> diff --git a/client_app/android/app/src/prod/google-services.json b/client_app/android/app/src/prod/google-services.json new file mode 100755 index 0000000..946ffc5 --- /dev/null +++ b/client_app/android/app/src/prod/google-services.json @@ -0,0 +1,70 @@ +{ + "project_info": { + "project_number": "953866659887", + "project_id": "my-bestie-production", + "storage_bucket": "my-bestie-production.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:953866659887:android:55dfbf97ac7c26e7183eda", + "android_client_info": { + "package_name": "com.mybestie" + } + }, + "oauth_client": [ + { + "client_id": "953866659887-5a62u0tdce92i0gmfo0gf3dt0dnlre42.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.mybestie", + "certificate_hash": "3c43db3c9ac7f6d7e2fa03b8dbcaf7e5d12c97f3" + } + }, + { + "client_id": "953866659887-kebg3eijcomtv97q6v03fm8i30kj7r9r.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.mybestie", + "certificate_hash": "7119b6cf7091074759450c899191905a5a4d0369" + } + }, + { + "client_id": "953866659887-vvur2mnmbu8ljmnmmg01hqsrj0ocssu9.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.mybestie", + "certificate_hash": "6e23f0164af04ddf200f769c460caac3ee2b91ac" + } + }, + { + "client_id": "953866659887-9ro36er68qupv00rgokjignnu2hs85v8.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyAxAp8hcXO-P7HwwVsS3vFe0OX5ZkIyyWI" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "953866659887-9ro36er68qupv00rgokjignnu2hs85v8.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "953866659887-bsb3c2a6ir10u47q8vcacre2tmnk59jb.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.asc.hallobestie" + } + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/client_app/android/app/src/prod/google-services.json.README b/client_app/android/app/src/prod/google-services.json.README new file mode 100644 index 0000000..1e7b542 --- /dev/null +++ b/client_app/android/app/src/prod/google-services.json.README @@ -0,0 +1,36 @@ +PROD google-services.json — NOT YET PRESENT +=========================================== + +The Gradle google-services plugin reads this flavor's Firebase config from: + + android/app/src/prod/google-services.json <-- MISSING + +This file is deliberately a README, NOT a fabricated google-services.json. +A wrong/placeholder google-services.json compiles fine but SILENTLY breaks +Firebase Analytics and FCM at runtime (no build error, no crash — just no +events / no push). Do not invent one. + +How to produce the real file +---------------------------- +1. In the Firebase Console for the PRODUCTION project, register an Android app + with package name: + + com.mybestie + + (no suffix — prod is the un-suffixed applicationId). Use the SEPARATE prod + Firebase console, NOT halobestie-clone-dev. See BUILD_FLAVORS.md. + +2. Download the generated google-services.json. + +3. Drop it here, replacing this README: + + android/app/src/prod/google-services.json + +4. Also generate the Dart options: + + flutterfire configure --project= \ + --out=lib/firebase/firebase_options_prod.dart + + (firebase_options_prod.dart currently holds PLACEHOLDER values.) + +Until both files hold real values, do NOT release a production build. diff --git a/client_app/android/app/src/staging/google-services.json b/client_app/android/app/src/staging/google-services.json new file mode 100755 index 0000000..b9e5faa --- /dev/null +++ b/client_app/android/app/src/staging/google-services.json @@ -0,0 +1,106 @@ +{ + "project_info": { + "project_number": "650461407929", + "project_id": "my-bestie-876ec", + "storage_bucket": "my-bestie-876ec.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:650461407929:android:92d95eb766802bcf504968", + "android_client_info": { + "package_name": "com.mybestie" + } + }, + "oauth_client": [ + { + "client_id": "650461407929-ofuff9cv8d4fj79efj9e2ovt2nj9b83a.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyAOPkPJkHXLFzo9ICOHyjee2Vn_EUqt1Pc" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "650461407929-ofuff9cv8d4fj79efj9e2ovt2nj9b83a.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "650461407929-2o7eo5ts2k389pa3l16sq26l3107b52f.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.example.halloBestie" + } + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:650461407929:android:05754df9552e0529504968", + "android_client_info": { + "package_name": "com.mybestie.staging" + } + }, + "oauth_client": [ + { + "client_id": "650461407929-3els6l9cegtiphe930vtjr80ffa2p1uv.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.mybestie.staging", + "certificate_hash": "a46c19a615b3c21b529240dabc8f1cd68bcbd449" + } + }, + { + "client_id": "650461407929-8lo71sr668gvvj0ntpjjemoqrkr82uid.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.mybestie.staging", + "certificate_hash": "6e23f0164af04ddf200f769c460caac3ee2b91ac" + } + }, + { + "client_id": "650461407929-lmj6n5jt818fkdjhjpbhabdd19g82f48.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.mybestie.staging", + "certificate_hash": "937ecfa181a695a5f1fb5d04df15e490c174caea" + } + }, + { + "client_id": "650461407929-ofuff9cv8d4fj79efj9e2ovt2nj9b83a.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyAOPkPJkHXLFzo9ICOHyjee2Vn_EUqt1Pc" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "650461407929-ofuff9cv8d4fj79efj9e2ovt2nj9b83a.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "650461407929-2o7eo5ts2k389pa3l16sq26l3107b52f.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.example.halloBestie" + } + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/client_app/android/app/src/staging/google-services.json.README b/client_app/android/app/src/staging/google-services.json.README new file mode 100644 index 0000000..397dc76 --- /dev/null +++ b/client_app/android/app/src/staging/google-services.json.README @@ -0,0 +1,37 @@ +STAGING google-services.json — NOT YET PRESENT +============================================== + +The Gradle google-services plugin reads this flavor's Firebase config from: + + android/app/src/staging/google-services.json <-- MISSING + +This file is deliberately a README, NOT a fabricated google-services.json. +A wrong/placeholder google-services.json compiles fine but SILENTLY breaks +Firebase Analytics and FCM at runtime (no build error, no crash — just no +events / no push). Do not invent one. + +How to produce the real file +---------------------------- +1. In the Firebase Console for the STAGING project, register an Android app + with package name: + + com.mybestie.staging + + (this is the dev/staging Firebase project today — see BUILD_FLAVORS.md for + the "register com.mybestie.staging in halobestie-clone-dev" decision, or a + dedicated staging project if/when one exists). + +2. Download the generated google-services.json. + +3. Drop it here, replacing this README: + + android/app/src/staging/google-services.json + +4. Also generate the Dart options: + + flutterfire configure --project= \ + --out=lib/firebase/firebase_options_staging.dart + + (firebase_options_staging.dart currently holds PLACEHOLDER values.) + +Until both files hold real values, do NOT distribute a staging build. diff --git a/client_app/env/dev.json b/client_app/env/dev.json new file mode 100644 index 0000000..b78deca --- /dev/null +++ b/client_app/env/dev.json @@ -0,0 +1,4 @@ +{ + "API_BASE_URL": "http://192.168.88.247:3000", + "FLAVOR": "dev" +} diff --git a/client_app/env/prod.json b/client_app/env/prod.json new file mode 100644 index 0000000..474e2c2 --- /dev/null +++ b/client_app/env/prod.json @@ -0,0 +1,4 @@ +{ + "API_BASE_URL": "https://api.halobestie.com", + "FLAVOR": "prod" +} diff --git a/client_app/env/staging.json b/client_app/env/staging.json new file mode 100644 index 0000000..65c5c9e --- /dev/null +++ b/client_app/env/staging.json @@ -0,0 +1,5 @@ +{ + "API_BASE_URL": "https://staging-api.halobestie.com", + "FLAVOR": "staging", + "_TODO": "confirm staging URL — https://staging-api.halobestie.com is a PLACEHOLDER (JSON forbids comments; this key documents the TODO and is ignored by the app)" +} diff --git a/client_app/ios/Runner.xcodeproj/project.pbxproj b/client_app/ios/Runner.xcodeproj/project.pbxproj index 960b9b0..de95e4c 100644 --- a/client_app/ios/Runner.xcodeproj/project.pbxproj +++ b/client_app/ios/Runner.xcodeproj/project.pbxproj @@ -496,7 +496,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.mybestie; + PRODUCT_BUNDLE_IDENTIFIER = com.asc.hallobestie; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -513,7 +513,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.mybestie.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.asc.hallobestie.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -531,7 +531,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.mybestie.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.asc.hallobestie.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; @@ -547,7 +547,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.mybestie.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.asc.hallobestie.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; @@ -679,7 +679,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.mybestie; + PRODUCT_BUNDLE_IDENTIFIER = com.asc.hallobestie; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -702,7 +702,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.mybestie; + PRODUCT_BUNDLE_IDENTIFIER = com.asc.hallobestie; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/client_app/ios/Runner/Info.plist b/client_app/ios/Runner/Info.plist index fea3909..61387c1 100644 --- a/client_app/ios/Runner/Info.plist +++ b/client_app/ios/Runner/Info.plist @@ -87,7 +87,7 @@ CFBundleURLName - com.mybestie + com.asc.hallobestie CFBundleURLSchemes halobestie diff --git a/client_app/ios/config/README.md b/client_app/ios/config/README.md new file mode 100644 index 0000000..ceced79 --- /dev/null +++ b/client_app/ios/config/README.md @@ -0,0 +1,34 @@ +# iOS per-flavor Firebase config — client_app + +iOS has **no** automatic per-flavor resolution (unlike Android's +`android/app/src//google-services.json`). Stage the three +`GoogleService-Info.plist` files here, one per flavor: + +``` +ios/config/ + dev/GoogleService-Info.plist → bundle com.asc.hallobestie.dev (dev project) + staging/GoogleService-Info.plist → bundle com.asc.hallobestie.staging (staging/nonprod) + prod/GoogleService-Info.plist → bundle com.asc.hallobestie (prod project, live App Store app) +``` + +> The customer iOS bundle base is **`com.asc.hallobestie`** (the existing App +> Store app), NOT the Android `com.mybestie`. See `../../BUILD_FLAVORS.md`. + +## Wiring (Xcode — Mac follow-up, not done yet) + +The active plist Xcode bundles is `ios/Runner/GoogleService-Info.plist`. To make +it per-flavor, add a **Run Script** build phase to the Runner target, placed +**before** "Compile Sources": + +```bash +cp "${PROJECT_DIR}/config/${FLAVOR}/GoogleService-Info.plist" \ + "${PROJECT_DIR}/Runner/GoogleService-Info.plist" +``` + +`${FLAVOR}` is a per-scheme/configuration build setting you define when creating +the dev/staging/prod schemes (e.g. `FLAVOR = dev` in the dev configuration). + +### Single-env shortcut +If you only need one env working (e.g. dev) before the full scheme setup, just +drop that env's plist directly at `ios/Runner/GoogleService-Info.plist` — no +script needed for a single environment. diff --git a/client_app/ios/config/dev/GoogleService-Info.plist b/client_app/ios/config/dev/GoogleService-Info.plist new file mode 100755 index 0000000..1d4e8b4 --- /dev/null +++ b/client_app/ios/config/dev/GoogleService-Info.plist @@ -0,0 +1,30 @@ + + + + + API_KEY + AIzaSyAQnB5hbj0T5tE4JQZQ9Tx6Whp_u15obMI + GCM_SENDER_ID + 1068156046511 + PLIST_VERSION + 1 + BUNDLE_ID + com.asc.hallobestie.dev + PROJECT_ID + halobestie-clone-dev + STORAGE_BUCKET + halobestie-clone-dev.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:1068156046511:ios:bc9098ffc2c2913ab8185a + + \ No newline at end of file diff --git a/client_app/ios/config/dev/GoogleService-Info.plist.README b/client_app/ios/config/dev/GoogleService-Info.plist.README new file mode 100644 index 0000000..e903d53 --- /dev/null +++ b/client_app/ios/config/dev/GoogleService-Info.plist.README @@ -0,0 +1,15 @@ +Place the DEV GoogleService-Info.plist here, named exactly: + + GoogleService-Info.plist + +Register this iOS app first: + • Firebase project: halobestie-clone-dev (the DEV project) + • iOS bundle ID: com.asc.hallobestie.dev +Then download its GoogleService-Info.plist into this folder (delete this .README). + +Also paste its appId/apiKey into lib/firebase/firebase_options_dev.dart +(the iOS section currently holds PLACEHOLDER values). + +How it gets used: a build-phase "Run Script" (added in Xcode — Mac follow-up) +copies the per-flavor file into ios/Runner/GoogleService-Info.plist at build +time. See ../README.md and BUILD_FLAVORS.md. diff --git a/client_app/ios/config/prod/GoogleService-Info.plist b/client_app/ios/config/prod/GoogleService-Info.plist new file mode 100755 index 0000000..22b1691 --- /dev/null +++ b/client_app/ios/config/prod/GoogleService-Info.plist @@ -0,0 +1,36 @@ + + + + + CLIENT_ID + 953866659887-bsb3c2a6ir10u47q8vcacre2tmnk59jb.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.953866659887-bsb3c2a6ir10u47q8vcacre2tmnk59jb + ANDROID_CLIENT_ID + 953866659887-5a62u0tdce92i0gmfo0gf3dt0dnlre42.apps.googleusercontent.com + API_KEY + AIzaSyA8guPSD87eDLeCsH6jVd1n2_SI4_MaGNE + GCM_SENDER_ID + 953866659887 + PLIST_VERSION + 1 + BUNDLE_ID + com.asc.hallobestie + PROJECT_ID + my-bestie-production + STORAGE_BUCKET + my-bestie-production.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:953866659887:ios:159fd11b1d2f3633183eda + + \ No newline at end of file diff --git a/client_app/ios/config/prod/GoogleService-Info.plist.README b/client_app/ios/config/prod/GoogleService-Info.plist.README new file mode 100644 index 0000000..aed52bb --- /dev/null +++ b/client_app/ios/config/prod/GoogleService-Info.plist.README @@ -0,0 +1,16 @@ +Place the PROD GoogleService-Info.plist here, named exactly: + + GoogleService-Info.plist + +Register this iOS app first: + • Firebase project: the PRODUCTION project + • iOS bundle ID: com.asc.hallobestie (the EXISTING App Store app — v2 ships + as an update to it; needs the `asc` Apple Developer account) +Then download its GoogleService-Info.plist into this folder (delete this .README). + +Also paste its values into lib/firebase/firebase_options_prod.dart +(currently all PLACEHOLDER). + +How it gets used: a build-phase "Run Script" (added in Xcode — Mac follow-up) +copies the per-flavor file into ios/Runner/GoogleService-Info.plist at build +time. See ../README.md and BUILD_FLAVORS.md. diff --git a/client_app/ios/config/staging/GoogleService-Info.plist b/client_app/ios/config/staging/GoogleService-Info.plist new file mode 100755 index 0000000..2f58352 --- /dev/null +++ b/client_app/ios/config/staging/GoogleService-Info.plist @@ -0,0 +1,36 @@ + + + + + CLIENT_ID + 650461407929-fb4t48nmguaslfvis7ebsea8vndro7ac.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.650461407929-fb4t48nmguaslfvis7ebsea8vndro7ac + ANDROID_CLIENT_ID + 650461407929-3els6l9cegtiphe930vtjr80ffa2p1uv.apps.googleusercontent.com + API_KEY + AIzaSyC_BewS88iaNsc9QdwsPzkV0sf9wUs4i_4 + GCM_SENDER_ID + 650461407929 + PLIST_VERSION + 1 + BUNDLE_ID + com.asc.hallobestie.staging + PROJECT_ID + my-bestie-876ec + STORAGE_BUCKET + my-bestie-876ec.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:650461407929:ios:4ee79d479b69d688504968 + + \ No newline at end of file diff --git a/client_app/ios/config/staging/GoogleService-Info.plist.README b/client_app/ios/config/staging/GoogleService-Info.plist.README new file mode 100644 index 0000000..0903663 --- /dev/null +++ b/client_app/ios/config/staging/GoogleService-Info.plist.README @@ -0,0 +1,15 @@ +Place the STAGING GoogleService-Info.plist here, named exactly: + + GoogleService-Info.plist + +Register this iOS app first: + • Firebase project: the STAGING / nonprod project + • iOS bundle ID: com.asc.hallobestie.staging +Then download its GoogleService-Info.plist into this folder (delete this .README). + +Also paste its values into lib/firebase/firebase_options_staging.dart +(currently all PLACEHOLDER). + +How it gets used: a build-phase "Run Script" (added in Xcode — Mac follow-up) +copies the per-flavor file into ios/Runner/GoogleService-Info.plist at build +time. See ../README.md and BUILD_FLAVORS.md. diff --git a/mitra_app/lib/firebase_options.dart b/client_app/lib/firebase/firebase_options_dev.dart similarity index 66% rename from mitra_app/lib/firebase_options.dart rename to client_app/lib/firebase/firebase_options_dev.dart index 596aa14..7963328 100644 --- a/mitra_app/lib/firebase_options.dart +++ b/client_app/lib/firebase/firebase_options_dev.dart @@ -1,23 +1,18 @@ -// File generated by FlutterFire CLI. +// File generated by FlutterFire CLI (regenerated from the registered +// dev Firebase apps — project halobestie-clone-dev). // ignore_for_file: type=lint import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; import 'package:flutter/foundation.dart' show defaultTargetPlatform, kIsWeb, TargetPlatform; -/// Default [FirebaseOptions] for use with your Firebase apps. -/// -/// Example: -/// ```dart -/// import 'firebase_options.dart'; -/// // ... -/// await Firebase.initializeApp( -/// options: DefaultFirebaseOptions.currentPlatform, -/// ); -/// ``` -class DefaultFirebaseOptions { +/// [FirebaseOptions] for the DEV environment (project halobestie-clone-dev). +class DevFirebaseOptions { static FirebaseOptions get currentPlatform { if (kIsWeb) { - return web; + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for web - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); } switch (defaultTargetPlatform) { case TargetPlatform.android: @@ -48,7 +43,7 @@ class DefaultFirebaseOptions { static const FirebaseOptions android = FirebaseOptions( apiKey: 'AIzaSyDFWlLSWytqwI7LSdUbVrO7J5De9L2LV2U', - appId: '1:1068156046511:android:f30784f6b0423131b8185a', + appId: '1:1068156046511:android:1f589ed358ccdad0b8185a', messagingSenderId: '1068156046511', projectId: 'halobestie-clone-dev', storageBucket: 'halobestie-clone-dev.firebasestorage.app', @@ -56,21 +51,10 @@ class DefaultFirebaseOptions { static const FirebaseOptions ios = FirebaseOptions( apiKey: 'AIzaSyAQnB5hbj0T5tE4JQZQ9Tx6Whp_u15obMI', - appId: '1:1068156046511:ios:b781f67a57d6db7bb8185a', + appId: '1:1068156046511:ios:bc9098ffc2c2913ab8185a', messagingSenderId: '1068156046511', projectId: 'halobestie-clone-dev', storageBucket: 'halobestie-clone-dev.firebasestorage.app', - iosBundleId: 'com.mybestie.mitra', + iosBundleId: 'com.asc.hallobestie.dev', ); - - static const FirebaseOptions web = FirebaseOptions( - apiKey: 'AIzaSyAvDQp6xLOZHSwhaj9Zk3DjcMvQyX0Y7Oc', - appId: '1:1068156046511:web:15b173b38aa563ceb8185a', - messagingSenderId: '1068156046511', - projectId: 'halobestie-clone-dev', - authDomain: 'halobestie-clone-dev.firebaseapp.com', - storageBucket: 'halobestie-clone-dev.firebasestorage.app', - measurementId: 'G-FK3V0LB3TT', - ); - -} \ No newline at end of file +} diff --git a/client_app/lib/firebase/firebase_options_prod.dart b/client_app/lib/firebase/firebase_options_prod.dart new file mode 100644 index 0000000..5c40614 --- /dev/null +++ b/client_app/lib/firebase/firebase_options_prod.dart @@ -0,0 +1,61 @@ +// File generated by FlutterFire CLI (regenerated from the registered +// prod Firebase apps — project my-bestie-production). +// ignore_for_file: type=lint +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// [FirebaseOptions] for the PROD environment (project my-bestie-production). +class ProdFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for web - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for macos - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.windows: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for windows - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyAxAp8hcXO-P7HwwVsS3vFe0OX5ZkIyyWI', + appId: '1:953866659887:android:55dfbf97ac7c26e7183eda', + messagingSenderId: '953866659887', + projectId: 'my-bestie-production', + storageBucket: 'my-bestie-production.firebasestorage.app', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyA8guPSD87eDLeCsH6jVd1n2_SI4_MaGNE', + appId: '1:953866659887:ios:159fd11b1d2f3633183eda', + messagingSenderId: '953866659887', + projectId: 'my-bestie-production', + storageBucket: 'my-bestie-production.firebasestorage.app', + iosClientId: '953866659887-bsb3c2a6ir10u47q8vcacre2tmnk59jb.apps.googleusercontent.com', + iosBundleId: 'com.asc.hallobestie', + ); +} diff --git a/client_app/lib/firebase/firebase_options_staging.dart b/client_app/lib/firebase/firebase_options_staging.dart new file mode 100644 index 0000000..2fba717 --- /dev/null +++ b/client_app/lib/firebase/firebase_options_staging.dart @@ -0,0 +1,61 @@ +// File generated by FlutterFire CLI (regenerated from the registered +// staging Firebase apps — project my-bestie-876ec). +// ignore_for_file: type=lint +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// [FirebaseOptions] for the STAGING environment (project my-bestie-876ec). +class StagingFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for web - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for macos - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.windows: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for windows - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyAOPkPJkHXLFzo9ICOHyjee2Vn_EUqt1Pc', + appId: '1:650461407929:android:05754df9552e0529504968', + messagingSenderId: '650461407929', + projectId: 'my-bestie-876ec', + storageBucket: 'my-bestie-876ec.firebasestorage.app', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyC_BewS88iaNsc9QdwsPzkV0sf9wUs4i_4', + appId: '1:650461407929:ios:4ee79d479b69d688504968', + messagingSenderId: '650461407929', + projectId: 'my-bestie-876ec', + storageBucket: 'my-bestie-876ec.firebasestorage.app', + iosClientId: '650461407929-fb4t48nmguaslfvis7ebsea8vndro7ac.apps.googleusercontent.com', + iosBundleId: 'com.asc.hallobestie.staging', + ); +} diff --git a/client_app/lib/main.dart b/client_app/lib/main.dart index 4b0ec5b..a47a12a 100644 --- a/client_app/lib/main.dart +++ b/client_app/lib/main.dart @@ -15,10 +15,23 @@ import 'core/chat/chat_notifier.dart'; import 'core/notifications/notification_service.dart'; import 'core/pairing/pairing_notifier.dart'; import 'core/theme/halo_theme.dart'; -import 'firebase_options.dart'; +import 'firebase/firebase_options_dev.dart'; import 'router.dart'; -void main() async { +/// Shared app bootstrap, parameterised per build flavor. +/// +/// The flavor entrypoints (`main_dev.dart`, `main_staging.dart`, +/// `main_prod.dart`) each call this with their environment's +/// [FirebaseOptions] and a [flavor] tag. The bare [main] below delegates to +/// dev so a plain `flutter run` (no `-t`) still launches the dev environment. +/// +/// `flavor` is currently informational (kept on hand for future flavor-gated +/// behaviour / analytics tagging); the API base URL is supplied separately via +/// `--dart-define-from-file=env/.json` (see BUILD_FLAVORS.md). +Future bootstrap({ + required FirebaseOptions firebaseOptions, + required String flavor, +}) async { WidgetsFlutterBinding.ensureInitialized(); // Pre-warm flutter_secure_storage. The first call triggers AndroidX @@ -28,7 +41,7 @@ void main() async { // splash instead of paying it on the user's first interaction. unawaited(TokenStorage().readRefreshToken()); - await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + await Firebase.initializeApp(options: firebaseOptions); // Enable GA4 collection. Fire-and-forget so it never adds to cold-start // latency; the SDK queues events until collection is on. @@ -42,6 +55,16 @@ void main() async { runApp(const ProviderScope(child: App())); } +void main() async { + // Bare `flutter run` (no `-t lib/main_.dart`) defaults to dev so + // local development works out of the box. Build-flavor APKs use the + // flavor-specific entrypoints instead. + await bootstrap( + firebaseOptions: DevFirebaseOptions.currentPlatform, + flavor: 'dev', + ); +} + class App extends ConsumerStatefulWidget { const App({super.key}); diff --git a/client_app/lib/main_dev.dart b/client_app/lib/main_dev.dart new file mode 100644 index 0000000..fa15f2e --- /dev/null +++ b/client_app/lib/main_dev.dart @@ -0,0 +1,14 @@ +import 'firebase/firebase_options_dev.dart'; +import 'main.dart'; + +/// DEV flavor entrypoint. +/// +/// Run/build with the matching flavor + env file: +/// flutter run --flavor dev -t lib/main_dev.dart \ +/// --dart-define-from-file=env/dev.json +void main() { + bootstrap( + firebaseOptions: DevFirebaseOptions.currentPlatform, + flavor: 'dev', + ); +} diff --git a/client_app/lib/main_prod.dart b/client_app/lib/main_prod.dart new file mode 100644 index 0000000..41b76f8 --- /dev/null +++ b/client_app/lib/main_prod.dart @@ -0,0 +1,14 @@ +import 'firebase/firebase_options_prod.dart'; +import 'main.dart'; + +/// PROD flavor entrypoint. +/// +/// Run/build with the matching flavor + env file: +/// flutter build apk --flavor prod -t lib/main_prod.dart \ +/// --dart-define-from-file=env/prod.json +void main() { + bootstrap( + firebaseOptions: ProdFirebaseOptions.currentPlatform, + flavor: 'prod', + ); +} diff --git a/client_app/lib/main_staging.dart b/client_app/lib/main_staging.dart new file mode 100644 index 0000000..bf877c6 --- /dev/null +++ b/client_app/lib/main_staging.dart @@ -0,0 +1,14 @@ +import 'firebase/firebase_options_staging.dart'; +import 'main.dart'; + +/// STAGING flavor entrypoint. +/// +/// Run/build with the matching flavor + env file: +/// flutter run --flavor staging -t lib/main_staging.dart \ +/// --dart-define-from-file=env/staging.json +void main() { + bootstrap( + firebaseOptions: StagingFirebaseOptions.currentPlatform, + flavor: 'staging', + ); +} diff --git a/mitra_app/BUILD_FLAVORS.md b/mitra_app/BUILD_FLAVORS.md new file mode 100644 index 0000000..411caa6 --- /dev/null +++ b/mitra_app/BUILD_FLAVORS.md @@ -0,0 +1,121 @@ +# Build Flavors — mitra_app (Android) + +The mitra_app has three Android build flavors: **dev**, **staging**, **prod**. +Each has its own `applicationId`, backend URL, app display name, Dart entrypoint, +Firebase Dart options, and `google-services.json` source set — so all three can +be installed side-by-side on one device. + +> Scope note: this is **Android + Dart + env-files** only. iOS Xcode schemes are +> a separate follow-up and are NOT set up yet. + +## Flavor matrix + +| Flavor | applicationId | API_BASE_URL | App name | Entrypoint | env file | +|---------|-----------------------------|-------------------------------------------|---------------------------|-----------------------|-------------------| +| dev | `com.mybestie.mitra.dev` | `http://192.168.88.247:3000` | Mitra HaloBestie Dev | `lib/main_dev.dart` | `env/dev.json` | +| staging | `com.mybestie.mitra.staging`| `https://staging-api.halobestie.com` ⚠️ | Mitra HaloBestie Staging | `lib/main_staging.dart`| `env/staging.json`| +| prod | `com.mybestie.mitra` | `https://api.halobestie.com` | Mitra HaloBestie | `lib/main_prod.dart` | `env/prod.json` | + +⚠️ The staging `API_BASE_URL` is a **placeholder** — confirm the real staging +host and update `env/staging.json` + `lib/firebase/firebase_options_staging.dart`. + +The `applicationId` suffix is applied in `android/app/build.gradle.kts` +(`applicationIdSuffix = ".dev"` / `".staging"`; prod has none). The app name is +emitted per flavor via `resValue("string", "app_name", "...")` and read by +`android/app/src/main/AndroidManifest.xml` through `android:label="@string/app_name"`. + +## Build / run commands + +Every command MUST pass `--flavor`, a matching `-t` entrypoint, and +`--dart-define-from-file` for the env. Examples: + +### Run (debug, on a device/emulator) +```bash +flutter run --flavor dev -t lib/main_dev.dart --dart-define-from-file=env/dev.json +flutter run --flavor staging -t lib/main_staging.dart --dart-define-from-file=env/staging.json +flutter run --flavor prod -t lib/main_prod.dart --dart-define-from-file=env/prod.json +``` + +### Build APK +```bash +flutter build apk --flavor dev -t lib/main_dev.dart --dart-define-from-file=env/dev.json +flutter build apk --flavor staging -t lib/main_staging.dart --dart-define-from-file=env/staging.json +flutter build apk --flavor prod -t lib/main_prod.dart --dart-define-from-file=env/prod.json +``` + +### Build App Bundle (Play Store) +```bash +flutter build appbundle --flavor prod -t lib/main_prod.dart --dart-define-from-file=env/prod.json +``` + +A bare `flutter run` (no `-t`) still works — `lib/main.dart` delegates to the +dev bootstrap — but it builds with no flavor selected on Android, so prefer the +explicit commands above. + +## ⚠️ CRITICAL warnings + +1. **`--flavor` is now mandatory for builds.** Once product flavors exist, a + bare `flutter build apk` (without `--flavor`) FAILS with a Gradle error + (no default flavor). Every build/run command must specify `--flavor` and the + matching `-t lib/main_.dart` entrypoint. + +2. **The dev applicationId changed to `com.mybestie.mitra.dev`.** Any tooling + that references the old package id must be updated when running the dev + flavor: + - `adb` commands: `adb shell pm clear com.mybestie.mitra.dev`, + `adb shell am start ... com.mybestie.mitra.dev/...`, etc. + - Maestro flows: `appId: com.mybestie.mitra.dev`. + - Any deeplink / FCM tooling keyed on the package name. + Prod keeps `com.mybestie.mitra`; staging is `com.mybestie.mitra.staging`. + +## Firebase config — STATUS: configured ✅ (2026-06-04) + +Firebase init is **Dart-side** (`Firebase.initializeApp(options:)` in +`lib/bootstrap.dart`), driven by the per-flavor +`lib/firebase/firebase_options_.dart`. The mitra app is a **brand-new +app** on both platforms (no legacy App Store identity), so the iOS bundle base +is `com.mybestie.mitra` — unlike the customer app, which inherits +`com.asc.hallobestie`. + +All apps registered + config in place across 3 projects (one per env): + +| Env | Firebase project | Android applicationId | iOS bundle ID | +|---------|------------------------|-------------------------------|------------------------------| +| dev | `halobestie-clone-dev` | `com.mybestie.mitra.dev` | `com.mybestie.mitra.dev` | +| staging | `my-bestie-876ec` | `com.mybestie.mitra.staging` | `com.mybestie.mitra.staging` | +| prod | `my-bestie-production` | `com.mybestie.mitra` | `com.mybestie.mitra` | + +In place and verified: +- `android/app/src//google-services.json` — all 3, client matches the flavor applicationId. +- `ios/config//GoogleService-Info.plist` — all 3, bundle IDs verified. +- `lib/firebase/firebase_options_{dev,staging,prod}.dart` — real android + iOS values, no placeholders. + +### Regenerating after any ID / key change +```bash +flutterfire configure --project=halobestie-clone-dev --out=lib/firebase/firebase_options_dev.dart +flutterfire configure --project=my-bestie-876ec --out=lib/firebase/firebase_options_staging.dart +flutterfire configure --project=my-bestie-production --out=lib/firebase/firebase_options_prod.dart +``` + +### Still TODO — iOS only (Mac/Xcode) +- [ ] iOS Xcode schemes + build-phase copy script to select the right + `GoogleService-Info.plist` per flavor (until then iOS bundles only + `ios/Runner/GoogleService-Info.plist`). See `ios/config/README.md`. + +> The Google Services Gradle plugin is **not** applied in this app — Android +> Firebase init is Dart-side. The `src//google-services.json` files are +> laid out for if/when that plugin is added. + +## File map + +| File | Purpose | +|---|---| +| `android/app/build.gradle.kts` | `flavorDimensions "env"` + `productFlavors` (id suffix + app_name) | +| `android/app/src/main/AndroidManifest.xml` | `android:label="@string/app_name"` | +| `android/app/src/dev/google-services.json` | dev Firebase config | +| `android/app/src/{staging,prod}/google-services.json.README` | placeholders — drop real json here | +| `lib/bootstrap.dart` | shared `bootstrap()` + `App` widget | +| `lib/main.dart` | bare entrypoint → delegates to dev | +| `lib/main_{dev,staging,prod}.dart` | per-flavor entrypoints | +| `lib/firebase/firebase_options_{dev,staging,prod}.dart` | per-flavor Dart Firebase options | +| `env/{dev,staging,prod}.json` | dart-define values (`API_BASE_URL`, `FLAVOR`) | diff --git a/mitra_app/android/app/build.gradle.kts b/mitra_app/android/app/build.gradle.kts index 3b4622e..9e0faa8 100644 --- a/mitra_app/android/app/build.gradle.kts +++ b/mitra_app/android/app/build.gradle.kts @@ -21,7 +21,8 @@ android { } defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + // Base Application ID. Per-flavor suffixes are applied below in + // productFlavors (dev → .dev, staging → .staging, prod → no suffix). applicationId = "com.mybestie.mitra" // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. @@ -31,6 +32,30 @@ android { versionName = flutter.versionName } + // Build flavors: dev / staging / prod. Each gets its own applicationId + // (so all three can be installed side-by-side) and its own app_name string + // resource (consumed by AndroidManifest's android:label="@string/app_name"). + // A bare `flutter build`/`flutter run` WITHOUT --flavor now fails — every + // command must pass --flavor and the matching -t entrypoint. + flavorDimensions += "env" + productFlavors { + create("dev") { + dimension = "env" + applicationIdSuffix = ".dev" + resValue("string", "app_name", "Mitra HaloBestie Dev") + } + create("staging") { + dimension = "env" + applicationIdSuffix = ".staging" + resValue("string", "app_name", "Mitra HaloBestie Staging") + } + create("prod") { + dimension = "env" + // No applicationIdSuffix — prod keeps the base com.mybestie.mitra. + resValue("string", "app_name", "Mitra HaloBestie") + } + } + buildTypes { release { // TODO: Add your own signing config for the release build. diff --git a/client_app/android/app/google-services.json b/mitra_app/android/app/src/dev/google-services.json similarity index 53% rename from client_app/android/app/google-services.json rename to mitra_app/android/app/src/dev/google-services.json index 1ec94b7..c85daaa 100755 --- a/client_app/android/app/google-services.json +++ b/mitra_app/android/app/src/dev/google-services.json @@ -61,6 +61,63 @@ "other_platform_oauth_client": [] } } + }, + { + "client_info": { + "mobilesdk_app_id": "1:1068156046511:android:1f589ed358ccdad0b8185a", + "android_client_info": { + "package_name": "com.mybestie.dev" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyDFWlLSWytqwI7LSdUbVrO7J5De9L2LV2U" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:1068156046511:android:8a873c8b7e64410ab8185a", + "android_client_info": { + "package_name": "com.mybestie.mitra" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyDFWlLSWytqwI7LSdUbVrO7J5De9L2LV2U" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:1068156046511:android:f527c763dea3dc36b8185a", + "android_client_info": { + "package_name": "com.mybestie.mitra.dev" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyDFWlLSWytqwI7LSdUbVrO7J5De9L2LV2U" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } } ], "configuration_version": "1" diff --git a/mitra_app/android/app/src/main/AndroidManifest.xml b/mitra_app/android/app/src/main/AndroidManifest.xml index 907059e..31f25e5 100644 --- a/mitra_app/android/app/src/main/AndroidManifest.xml +++ b/mitra_app/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ diff --git a/mitra_app/android/app/src/prod/google-services.json b/mitra_app/android/app/src/prod/google-services.json new file mode 100755 index 0000000..5f6b996 --- /dev/null +++ b/mitra_app/android/app/src/prod/google-services.json @@ -0,0 +1,106 @@ +{ + "project_info": { + "project_number": "953866659887", + "project_id": "my-bestie-production", + "storage_bucket": "my-bestie-production.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:953866659887:android:55dfbf97ac7c26e7183eda", + "android_client_info": { + "package_name": "com.mybestie" + } + }, + "oauth_client": [ + { + "client_id": "953866659887-5a62u0tdce92i0gmfo0gf3dt0dnlre42.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.mybestie", + "certificate_hash": "3c43db3c9ac7f6d7e2fa03b8dbcaf7e5d12c97f3" + } + }, + { + "client_id": "953866659887-kebg3eijcomtv97q6v03fm8i30kj7r9r.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.mybestie", + "certificate_hash": "7119b6cf7091074759450c899191905a5a4d0369" + } + }, + { + "client_id": "953866659887-vvur2mnmbu8ljmnmmg01hqsrj0ocssu9.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.mybestie", + "certificate_hash": "6e23f0164af04ddf200f769c460caac3ee2b91ac" + } + }, + { + "client_id": "953866659887-9ro36er68qupv00rgokjignnu2hs85v8.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyAxAp8hcXO-P7HwwVsS3vFe0OX5ZkIyyWI" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "953866659887-9ro36er68qupv00rgokjignnu2hs85v8.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "953866659887-bsb3c2a6ir10u47q8vcacre2tmnk59jb.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.asc.hallobestie" + } + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:953866659887:android:a4b99d675b0b0315183eda", + "android_client_info": { + "package_name": "com.mybestie.mitra" + } + }, + "oauth_client": [ + { + "client_id": "953866659887-9ro36er68qupv00rgokjignnu2hs85v8.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyAxAp8hcXO-P7HwwVsS3vFe0OX5ZkIyyWI" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "953866659887-9ro36er68qupv00rgokjignnu2hs85v8.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "953866659887-bsb3c2a6ir10u47q8vcacre2tmnk59jb.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.asc.hallobestie" + } + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/mitra_app/android/app/src/prod/google-services.json.README b/mitra_app/android/app/src/prod/google-services.json.README new file mode 100644 index 0000000..51400df --- /dev/null +++ b/mitra_app/android/app/src/prod/google-services.json.README @@ -0,0 +1,29 @@ +PROD flavor — google-services.json goes HERE +============================================ + +This directory is the `prod` flavor source set. The Google Services Gradle +plugin reads `android/app/src/prod/google-services.json` when you build the +prod flavor. It is intentionally MISSING right now. + +To make the prod flavor build: + +1. In the Firebase Console (the PRODUCTION project), add an Android app with + package name: + + com.mybestie.mitra + + (prod has NO applicationIdSuffix — it keeps the base id.) + +2. Download the generated `google-services.json`. + +3. Drop it in this folder, replacing this README: + + android/app/src/prod/google-services.json + +4. Also run `flutterfire configure` for the production project/package and paste + the generated Dart values into: + + lib/firebase/firebase_options_prod.dart + +DO NOT copy the dev google-services.json here and edit it by hand — the +mobilesdk_app_id / api_key must come from the real Firebase registration. diff --git a/mitra_app/android/app/src/staging/google-services.json b/mitra_app/android/app/src/staging/google-services.json new file mode 100755 index 0000000..8fa483f --- /dev/null +++ b/mitra_app/android/app/src/staging/google-services.json @@ -0,0 +1,142 @@ +{ + "project_info": { + "project_number": "650461407929", + "project_id": "my-bestie-876ec", + "storage_bucket": "my-bestie-876ec.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:650461407929:android:92d95eb766802bcf504968", + "android_client_info": { + "package_name": "com.mybestie" + } + }, + "oauth_client": [ + { + "client_id": "650461407929-ofuff9cv8d4fj79efj9e2ovt2nj9b83a.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyAOPkPJkHXLFzo9ICOHyjee2Vn_EUqt1Pc" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "650461407929-ofuff9cv8d4fj79efj9e2ovt2nj9b83a.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "650461407929-2o7eo5ts2k389pa3l16sq26l3107b52f.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.example.halloBestie" + } + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:650461407929:android:7571ae8d5036de5d504968", + "android_client_info": { + "package_name": "com.mybestie.mitra.staging" + } + }, + "oauth_client": [ + { + "client_id": "650461407929-ofuff9cv8d4fj79efj9e2ovt2nj9b83a.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyAOPkPJkHXLFzo9ICOHyjee2Vn_EUqt1Pc" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "650461407929-ofuff9cv8d4fj79efj9e2ovt2nj9b83a.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "650461407929-2o7eo5ts2k389pa3l16sq26l3107b52f.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.example.halloBestie" + } + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:650461407929:android:05754df9552e0529504968", + "android_client_info": { + "package_name": "com.mybestie.staging" + } + }, + "oauth_client": [ + { + "client_id": "650461407929-3els6l9cegtiphe930vtjr80ffa2p1uv.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.mybestie.staging", + "certificate_hash": "a46c19a615b3c21b529240dabc8f1cd68bcbd449" + } + }, + { + "client_id": "650461407929-8lo71sr668gvvj0ntpjjemoqrkr82uid.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.mybestie.staging", + "certificate_hash": "6e23f0164af04ddf200f769c460caac3ee2b91ac" + } + }, + { + "client_id": "650461407929-lmj6n5jt818fkdjhjpbhabdd19g82f48.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.mybestie.staging", + "certificate_hash": "937ecfa181a695a5f1fb5d04df15e490c174caea" + } + }, + { + "client_id": "650461407929-ofuff9cv8d4fj79efj9e2ovt2nj9b83a.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyAOPkPJkHXLFzo9ICOHyjee2Vn_EUqt1Pc" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "650461407929-ofuff9cv8d4fj79efj9e2ovt2nj9b83a.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "650461407929-2o7eo5ts2k389pa3l16sq26l3107b52f.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.example.halloBestie" + } + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/mitra_app/android/app/src/staging/google-services.json.README b/mitra_app/android/app/src/staging/google-services.json.README new file mode 100644 index 0000000..98d8031 --- /dev/null +++ b/mitra_app/android/app/src/staging/google-services.json.README @@ -0,0 +1,30 @@ +STAGING flavor — google-services.json goes HERE +================================================ + +This directory is the `staging` flavor source set. The Google Services Gradle +plugin reads `android/app/src/staging/google-services.json` when you build the +staging flavor. It is intentionally MISSING right now. + +To make the staging flavor build: + +1. In the Firebase Console (the staging / nonprod project), add an Android app + with package name: + + com.mybestie.mitra.staging + + (note the `.staging` suffix — set by `applicationIdSuffix` in + android/app/build.gradle.kts). + +2. Download the generated `google-services.json`. + +3. Drop it in this folder, replacing this README: + + android/app/src/staging/google-services.json + +4. Also run `flutterfire configure` for that project/package and paste the + generated Dart values into: + + lib/firebase/firebase_options_staging.dart + +DO NOT copy the dev google-services.json here and edit it by hand — the +mobilesdk_app_id / api_key must come from the real Firebase registration. diff --git a/mitra_app/env/dev.json b/mitra_app/env/dev.json new file mode 100644 index 0000000..b78deca --- /dev/null +++ b/mitra_app/env/dev.json @@ -0,0 +1,4 @@ +{ + "API_BASE_URL": "http://192.168.88.247:3000", + "FLAVOR": "dev" +} diff --git a/mitra_app/env/prod.json b/mitra_app/env/prod.json new file mode 100644 index 0000000..474e2c2 --- /dev/null +++ b/mitra_app/env/prod.json @@ -0,0 +1,4 @@ +{ + "API_BASE_URL": "https://api.halobestie.com", + "FLAVOR": "prod" +} diff --git a/mitra_app/env/staging.json b/mitra_app/env/staging.json new file mode 100644 index 0000000..9ab7a2d --- /dev/null +++ b/mitra_app/env/staging.json @@ -0,0 +1,4 @@ +{ + "API_BASE_URL": "https://staging-api.halobestie.com", + "FLAVOR": "staging" +} diff --git a/mitra_app/ios/config/README.md b/mitra_app/ios/config/README.md new file mode 100644 index 0000000..bcaf3f8 --- /dev/null +++ b/mitra_app/ios/config/README.md @@ -0,0 +1,34 @@ +# iOS per-flavor Firebase config — mitra_app + +iOS has **no** automatic per-flavor resolution (unlike Android's +`android/app/src//google-services.json`). Stage the three +`GoogleService-Info.plist` files here, one per flavor: + +``` +ios/config/ + dev/GoogleService-Info.plist → bundle com.mybestie.mitra.dev (dev project) + staging/GoogleService-Info.plist → bundle com.mybestie.mitra.staging (staging/nonprod) + prod/GoogleService-Info.plist → bundle com.mybestie.mitra (prod project) +``` + +> The mitra app is a **brand-new app** on both platforms — no legacy App Store +> identity (unlike the customer app, which inherits `com.asc.hallobestie`). + +## Wiring (Xcode — Mac follow-up, not done yet) + +The active plist Xcode bundles is `ios/Runner/GoogleService-Info.plist`. To make +it per-flavor, add a **Run Script** build phase to the Runner target, placed +**before** "Compile Sources": + +```bash +cp "${PROJECT_DIR}/config/${FLAVOR}/GoogleService-Info.plist" \ + "${PROJECT_DIR}/Runner/GoogleService-Info.plist" +``` + +`${FLAVOR}` is a per-scheme/configuration build setting you define when creating +the dev/staging/prod schemes (e.g. `FLAVOR = dev` in the dev configuration). + +### Single-env shortcut +If you only need one env working (e.g. dev) before the full scheme setup, just +drop that env's plist directly at `ios/Runner/GoogleService-Info.plist` — no +script needed for a single environment. diff --git a/mitra_app/ios/config/dev/GoogleService-Info.plist b/mitra_app/ios/config/dev/GoogleService-Info.plist new file mode 100755 index 0000000..7e82c78 --- /dev/null +++ b/mitra_app/ios/config/dev/GoogleService-Info.plist @@ -0,0 +1,30 @@ + + + + + API_KEY + AIzaSyAQnB5hbj0T5tE4JQZQ9Tx6Whp_u15obMI + GCM_SENDER_ID + 1068156046511 + PLIST_VERSION + 1 + BUNDLE_ID + com.mybestie.mitra.dev + PROJECT_ID + halobestie-clone-dev + STORAGE_BUCKET + halobestie-clone-dev.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:1068156046511:ios:907b28451e22981db8185a + + \ No newline at end of file diff --git a/mitra_app/ios/config/dev/GoogleService-Info.plist.README b/mitra_app/ios/config/dev/GoogleService-Info.plist.README new file mode 100644 index 0000000..cb3dc9c --- /dev/null +++ b/mitra_app/ios/config/dev/GoogleService-Info.plist.README @@ -0,0 +1,14 @@ +Place the DEV GoogleService-Info.plist here, named exactly: + + GoogleService-Info.plist + +Register this iOS app first: + • Firebase project: halobestie-clone-dev (the DEV project) + • iOS bundle ID: com.mybestie.mitra.dev +Then download its GoogleService-Info.plist into this folder (delete this .README). + +Also paste its appId/apiKey into lib/firebase/firebase_options_dev.dart. + +How it gets used: a build-phase "Run Script" (added in Xcode — Mac follow-up) +copies the per-flavor file into ios/Runner/GoogleService-Info.plist at build +time. See ../README.md and BUILD_FLAVORS.md. diff --git a/mitra_app/ios/config/prod/GoogleService-Info.plist b/mitra_app/ios/config/prod/GoogleService-Info.plist new file mode 100755 index 0000000..37942cd --- /dev/null +++ b/mitra_app/ios/config/prod/GoogleService-Info.plist @@ -0,0 +1,36 @@ + + + + + CLIENT_ID + 953866659887-i0atpahlpqt6r9id17hjeirt5j1uqu8k.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.953866659887-i0atpahlpqt6r9id17hjeirt5j1uqu8k + ANDROID_CLIENT_ID + 953866659887-5a62u0tdce92i0gmfo0gf3dt0dnlre42.apps.googleusercontent.com + API_KEY + AIzaSyA8guPSD87eDLeCsH6jVd1n2_SI4_MaGNE + GCM_SENDER_ID + 953866659887 + PLIST_VERSION + 1 + BUNDLE_ID + com.mybestie.mitra + PROJECT_ID + my-bestie-production + STORAGE_BUCKET + my-bestie-production.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:953866659887:ios:cd8dd704842f3489183eda + + \ No newline at end of file diff --git a/mitra_app/ios/config/prod/GoogleService-Info.plist.README b/mitra_app/ios/config/prod/GoogleService-Info.plist.README new file mode 100644 index 0000000..2e02dcb --- /dev/null +++ b/mitra_app/ios/config/prod/GoogleService-Info.plist.README @@ -0,0 +1,16 @@ +Place the PROD GoogleService-Info.plist here, named exactly: + + GoogleService-Info.plist + +Register this iOS app first: + • Firebase project: the PRODUCTION project + • iOS bundle ID: com.mybestie.mitra (brand-new app — mitra has no legacy + App Store identity, unlike the customer app) +Then download its GoogleService-Info.plist into this folder (delete this .README). + +Also paste its values into lib/firebase/firebase_options_prod.dart +(currently all PLACEHOLDER). + +How it gets used: a build-phase "Run Script" (added in Xcode — Mac follow-up) +copies the per-flavor file into ios/Runner/GoogleService-Info.plist at build +time. See ../README.md and BUILD_FLAVORS.md. diff --git a/mitra_app/ios/config/staging/GoogleService-Info.plist b/mitra_app/ios/config/staging/GoogleService-Info.plist new file mode 100755 index 0000000..0caa238 --- /dev/null +++ b/mitra_app/ios/config/staging/GoogleService-Info.plist @@ -0,0 +1,36 @@ + + + + + CLIENT_ID + 650461407929-kuc6m53nehsa677geu57f3fmn9ql6lbg.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.650461407929-kuc6m53nehsa677geu57f3fmn9ql6lbg + ANDROID_CLIENT_ID + 650461407929-3els6l9cegtiphe930vtjr80ffa2p1uv.apps.googleusercontent.com + API_KEY + AIzaSyC_BewS88iaNsc9QdwsPzkV0sf9wUs4i_4 + GCM_SENDER_ID + 650461407929 + PLIST_VERSION + 1 + BUNDLE_ID + com.mybestie.mitra.staging + PROJECT_ID + my-bestie-876ec + STORAGE_BUCKET + my-bestie-876ec.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:650461407929:ios:b273bda6ad4045ca504968 + + \ No newline at end of file diff --git a/mitra_app/ios/config/staging/GoogleService-Info.plist.README b/mitra_app/ios/config/staging/GoogleService-Info.plist.README new file mode 100644 index 0000000..40c3b13 --- /dev/null +++ b/mitra_app/ios/config/staging/GoogleService-Info.plist.README @@ -0,0 +1,15 @@ +Place the STAGING GoogleService-Info.plist here, named exactly: + + GoogleService-Info.plist + +Register this iOS app first: + • Firebase project: the STAGING / nonprod project + • iOS bundle ID: com.mybestie.mitra.staging +Then download its GoogleService-Info.plist into this folder (delete this .README). + +Also paste its values into lib/firebase/firebase_options_staging.dart +(currently all PLACEHOLDER). + +How it gets used: a build-phase "Run Script" (added in Xcode — Mac follow-up) +copies the per-flavor file into ios/Runner/GoogleService-Info.plist at build +time. See ../README.md and BUILD_FLAVORS.md. diff --git a/mitra_app/lib/bootstrap.dart b/mitra_app/lib/bootstrap.dart new file mode 100644 index 0000000..ee3d3e9 --- /dev/null +++ b/mitra_app/lib/bootstrap.dart @@ -0,0 +1,125 @@ +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'core/api/api_client_provider.dart'; +import 'core/auth/auth_notifier.dart'; +import 'core/chat/mitra_chat_notifier.dart'; +import 'core/status/status_notifier.dart'; +import 'core/chat/chat_request_notifier.dart'; +import 'core/chat/widgets/chat_request_overlay.dart'; +import 'core/notifications/notification_service.dart'; +import 'core/theme/halo_theme.dart'; +import 'router.dart'; + +/// Shared app bootstrap used by every flavor entrypoint +/// (main_dev / main_staging / main_prod). Each entrypoint passes the +/// flavor's own [FirebaseOptions] and a [flavor] tag. +Future bootstrap({ + required FirebaseOptions firebaseOptions, + required String flavor, +}) async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(options: firebaseOptions); + + final messaging = FirebaseMessaging.instance; + await messaging.requestPermission(); + + runApp(const ProviderScope(child: App())); +} + +class App extends ConsumerStatefulWidget { + const App({super.key}); + + @override + ConsumerState createState() => _AppState(); +} + +class _AppState extends ConsumerState with WidgetsBindingObserver { + bool _fcmRegistered = false; + // Session the chat WS was on at the moment we backgrounded. Restored on + // resume so a backgrounded mitra reconnects to the same chat once they + // foreground the app. Mirrors the customer-app fix (main.dart on the + // client side) — backend's sendMessage checks recipient WS readyState + // before falling back to FCM, so leaving the WS open while paused makes + // FCM never fire and the mitra misses customer messages in background. + String? _pausedChatSessionId; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.paused || state == AppLifecycleState.detached) { + ref.read(onlineStatusProvider.notifier).onAppPaused(); + // Close the chat WS so backend `sendMessage` falls back to FCM when + // the customer sends a message. Stash the active session_id so we + // can rejoin it on resume. + final chatNotifier = ref.read(mitraChatProvider.notifier); + final sid = chatNotifier.connectedSessionId; + if (sid != null) { + _pausedChatSessionId = sid; + chatNotifier.disconnect(); + } + } else if (state == AppLifecycleState.resumed) { + ref.read(onlineStatusProvider.notifier).onAppResumed(); + // Reconnect to the chat we backgrounded out of, if any. + final saved = _pausedChatSessionId; + _pausedChatSessionId = null; + if (saved != null) { + // ignore: discarded_futures + ref.read(mitraChatProvider.notifier).connect(saved); + } + } + } + + void _registerFcmToken() { + if (_fcmRegistered) return; + _fcmRegistered = true; + Future(() async { + try { + final token = await FirebaseMessaging.instance.getToken(); + if (token != null) { + await ref.read(apiClientProvider).post('/api/shared/device-token', data: {'token': token}); + } + } catch (_) { + _fcmRegistered = false; + } + }); + } + + @override + Widget build(BuildContext context) { + // Listen for auth changes to load status and register FCM + ref.listen(mitraAuthProvider, (prev, next) { + final data = next.valueOrNull; + if (data is MitraAuthAuthenticatedData) { + ref.read(onlineStatusProvider.notifier).load(); + _registerFcmToken(); + } + }); + + final router = ref.watch(routerProvider); + NotificationService.initialize(router); + NotificationService.onChatRequestTapped = (sessionId) { + ref.read(chatRequestProvider.notifier).setIncomingFromNotification(sessionId); + }; + + return ChatRequestOverlay( + child: MaterialApp.router( + title: 'Halo Bestie Mitra', + theme: haloThemeData(), + routerConfig: router, + ), + ); + } +} diff --git a/client_app/lib/firebase_options.dart b/mitra_app/lib/firebase/firebase_options_dev.dart similarity index 74% rename from client_app/lib/firebase_options.dart rename to mitra_app/lib/firebase/firebase_options_dev.dart index 79c3c92..2b2a163 100644 --- a/client_app/lib/firebase_options.dart +++ b/mitra_app/lib/firebase/firebase_options_dev.dart @@ -1,26 +1,18 @@ -// File generated by FlutterFire CLI. +// File generated by FlutterFire CLI (regenerated from the registered +// dev Firebase apps — project halobestie-clone-dev). // ignore_for_file: type=lint import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; import 'package:flutter/foundation.dart' show defaultTargetPlatform, kIsWeb, TargetPlatform; -/// Default [FirebaseOptions] for use with your Firebase apps. -/// -/// Example: -/// ```dart -/// import 'firebase_options.dart'; -/// // ... -/// await Firebase.initializeApp( -/// options: DefaultFirebaseOptions.currentPlatform, -/// ); -/// ``` +/// [FirebaseOptions] for the DEV environment (project halobestie-clone-dev). class DefaultFirebaseOptions { static FirebaseOptions get currentPlatform { if (kIsWeb) { - throw UnsupportedError( - 'DefaultFirebaseOptions have not been configured for web - ' - 'you can reconfigure this by running the FlutterFire CLI again.', - ); + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for web - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); } switch (defaultTargetPlatform) { case TargetPlatform.android: @@ -51,7 +43,7 @@ class DefaultFirebaseOptions { static const FirebaseOptions android = FirebaseOptions( apiKey: 'AIzaSyDFWlLSWytqwI7LSdUbVrO7J5De9L2LV2U', - appId: '1:1068156046511:android:4f8fe9a3c7c14c57b8185a', + appId: '1:1068156046511:android:f527c763dea3dc36b8185a', messagingSenderId: '1068156046511', projectId: 'halobestie-clone-dev', storageBucket: 'halobestie-clone-dev.firebasestorage.app', @@ -59,10 +51,10 @@ class DefaultFirebaseOptions { static const FirebaseOptions ios = FirebaseOptions( apiKey: 'AIzaSyAQnB5hbj0T5tE4JQZQ9Tx6Whp_u15obMI', - appId: '1:1068156046511:ios:c7786cedb9101d34b8185a', + appId: '1:1068156046511:ios:907b28451e22981db8185a', messagingSenderId: '1068156046511', projectId: 'halobestie-clone-dev', storageBucket: 'halobestie-clone-dev.firebasestorage.app', - iosBundleId: 'com.mybestie', + iosBundleId: 'com.mybestie.mitra.dev', ); } diff --git a/mitra_app/lib/firebase/firebase_options_prod.dart b/mitra_app/lib/firebase/firebase_options_prod.dart new file mode 100644 index 0000000..7df65b7 --- /dev/null +++ b/mitra_app/lib/firebase/firebase_options_prod.dart @@ -0,0 +1,61 @@ +// File generated by FlutterFire CLI (regenerated from the registered +// prod Firebase apps — project my-bestie-production). +// ignore_for_file: type=lint +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// [FirebaseOptions] for the PROD environment (project my-bestie-production). +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for web - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for macos - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.windows: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for windows - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyAxAp8hcXO-P7HwwVsS3vFe0OX5ZkIyyWI', + appId: '1:953866659887:android:a4b99d675b0b0315183eda', + messagingSenderId: '953866659887', + projectId: 'my-bestie-production', + storageBucket: 'my-bestie-production.firebasestorage.app', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyA8guPSD87eDLeCsH6jVd1n2_SI4_MaGNE', + appId: '1:953866659887:ios:cd8dd704842f3489183eda', + messagingSenderId: '953866659887', + projectId: 'my-bestie-production', + storageBucket: 'my-bestie-production.firebasestorage.app', + iosClientId: '953866659887-i0atpahlpqt6r9id17hjeirt5j1uqu8k.apps.googleusercontent.com', + iosBundleId: 'com.mybestie.mitra', + ); +} diff --git a/mitra_app/lib/firebase/firebase_options_staging.dart b/mitra_app/lib/firebase/firebase_options_staging.dart new file mode 100644 index 0000000..7beaad7 --- /dev/null +++ b/mitra_app/lib/firebase/firebase_options_staging.dart @@ -0,0 +1,61 @@ +// File generated by FlutterFire CLI (regenerated from the registered +// staging Firebase apps — project my-bestie-876ec). +// ignore_for_file: type=lint +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// [FirebaseOptions] for the STAGING environment (project my-bestie-876ec). +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for web - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for macos - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.windows: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for windows - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyAOPkPJkHXLFzo9ICOHyjee2Vn_EUqt1Pc', + appId: '1:650461407929:android:7571ae8d5036de5d504968', + messagingSenderId: '650461407929', + projectId: 'my-bestie-876ec', + storageBucket: 'my-bestie-876ec.firebasestorage.app', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyC_BewS88iaNsc9QdwsPzkV0sf9wUs4i_4', + appId: '1:650461407929:ios:b273bda6ad4045ca504968', + messagingSenderId: '650461407929', + projectId: 'my-bestie-876ec', + storageBucket: 'my-bestie-876ec.firebasestorage.app', + iosClientId: '650461407929-kuc6m53nehsa677geu57f3fmn9ql6lbg.apps.googleusercontent.com', + iosBundleId: 'com.mybestie.mitra.staging', + ); +} diff --git a/mitra_app/lib/main.dart b/mitra_app/lib/main.dart index 58037e9..23d3fe7 100644 --- a/mitra_app/lib/main.dart +++ b/mitra_app/lib/main.dart @@ -1,120 +1,15 @@ -import 'package:firebase_core/firebase_core.dart'; -import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'core/api/api_client_provider.dart'; -import 'core/auth/auth_notifier.dart'; -import 'core/chat/mitra_chat_notifier.dart'; -import 'core/status/status_notifier.dart'; -import 'core/chat/chat_request_notifier.dart'; -import 'core/chat/widgets/chat_request_overlay.dart'; -import 'core/notifications/notification_service.dart'; -import 'core/theme/halo_theme.dart'; -import 'firebase_options.dart'; -import 'router.dart'; +import 'bootstrap.dart'; +import 'firebase/firebase_options_dev.dart'; -void main() async { - WidgetsFlutterBinding.ensureInitialized(); - await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); - - final messaging = FirebaseMessaging.instance; - await messaging.requestPermission(); - - runApp(const ProviderScope(child: App())); -} - -class App extends ConsumerStatefulWidget { - const App({super.key}); - - @override - ConsumerState createState() => _AppState(); -} - -class _AppState extends ConsumerState with WidgetsBindingObserver { - bool _fcmRegistered = false; - // Session the chat WS was on at the moment we backgrounded. Restored on - // resume so a backgrounded mitra reconnects to the same chat once they - // foreground the app. Mirrors the customer-app fix (main.dart on the - // client side) — backend's sendMessage checks recipient WS readyState - // before falling back to FCM, so leaving the WS open while paused makes - // FCM never fire and the mitra misses customer messages in background. - String? _pausedChatSessionId; - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addObserver(this); - } - - @override - void dispose() { - WidgetsBinding.instance.removeObserver(this); - super.dispose(); - } - - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - if (state == AppLifecycleState.paused || state == AppLifecycleState.detached) { - ref.read(onlineStatusProvider.notifier).onAppPaused(); - // Close the chat WS so backend `sendMessage` falls back to FCM when - // the customer sends a message. Stash the active session_id so we - // can rejoin it on resume. - final chatNotifier = ref.read(mitraChatProvider.notifier); - final sid = chatNotifier.connectedSessionId; - if (sid != null) { - _pausedChatSessionId = sid; - chatNotifier.disconnect(); - } - } else if (state == AppLifecycleState.resumed) { - ref.read(onlineStatusProvider.notifier).onAppResumed(); - // Reconnect to the chat we backgrounded out of, if any. - final saved = _pausedChatSessionId; - _pausedChatSessionId = null; - if (saved != null) { - // ignore: discarded_futures - ref.read(mitraChatProvider.notifier).connect(saved); - } - } - } - - void _registerFcmToken() { - if (_fcmRegistered) return; - _fcmRegistered = true; - Future(() async { - try { - final token = await FirebaseMessaging.instance.getToken(); - if (token != null) { - await ref.read(apiClientProvider).post('/api/shared/device-token', data: {'token': token}); - } - } catch (_) { - _fcmRegistered = false; - } - }); - } - - @override - Widget build(BuildContext context) { - // Listen for auth changes to load status and register FCM - ref.listen(mitraAuthProvider, (prev, next) { - final data = next.valueOrNull; - if (data is MitraAuthAuthenticatedData) { - ref.read(onlineStatusProvider.notifier).load(); - _registerFcmToken(); - } - }); - - final router = ref.watch(routerProvider); - NotificationService.initialize(router); - NotificationService.onChatRequestTapped = (sessionId) { - ref.read(chatRequestProvider.notifier).setIncomingFromNotification(sessionId); - }; - - return ChatRequestOverlay( - child: MaterialApp.router( - title: 'Halo Bestie Mitra', - theme: haloThemeData(), - routerConfig: router, - ), +/// Default entrypoint — delegates to the DEV flavor so a bare `flutter run` +/// (without -t) still works during local development. The `App` widget and the +/// shared startup logic now live in [bootstrap]. +/// +/// For an explicit flavor, use the dedicated entrypoints instead: +/// - lib/main_dev.dart (flutter run --flavor dev -t lib/main_dev.dart --dart-define-from-file=env/dev.json) +/// - lib/main_staging.dart (flutter run --flavor staging -t lib/main_staging.dart --dart-define-from-file=env/staging.json) +/// - lib/main_prod.dart (flutter run --flavor prod -t lib/main_prod.dart --dart-define-from-file=env/prod.json) +Future main() => bootstrap( + firebaseOptions: DefaultFirebaseOptions.currentPlatform, + flavor: 'dev', ); - } -} diff --git a/mitra_app/lib/main_dev.dart b/mitra_app/lib/main_dev.dart new file mode 100644 index 0000000..92b05b5 --- /dev/null +++ b/mitra_app/lib/main_dev.dart @@ -0,0 +1,9 @@ +import 'bootstrap.dart'; +import 'firebase/firebase_options_dev.dart'; + +/// DEV flavor entrypoint. +/// Run: flutter run --flavor dev -t lib/main_dev.dart --dart-define-from-file=env/dev.json +Future main() => bootstrap( + firebaseOptions: DefaultFirebaseOptions.currentPlatform, + flavor: 'dev', + ); diff --git a/mitra_app/lib/main_prod.dart b/mitra_app/lib/main_prod.dart new file mode 100644 index 0000000..d77f5be --- /dev/null +++ b/mitra_app/lib/main_prod.dart @@ -0,0 +1,9 @@ +import 'bootstrap.dart'; +import 'firebase/firebase_options_prod.dart'; + +/// PROD flavor entrypoint. +/// Run: flutter run --flavor prod -t lib/main_prod.dart --dart-define-from-file=env/prod.json +Future main() => bootstrap( + firebaseOptions: DefaultFirebaseOptions.currentPlatform, + flavor: 'prod', + ); diff --git a/mitra_app/lib/main_staging.dart b/mitra_app/lib/main_staging.dart new file mode 100644 index 0000000..4df495e --- /dev/null +++ b/mitra_app/lib/main_staging.dart @@ -0,0 +1,9 @@ +import 'bootstrap.dart'; +import 'firebase/firebase_options_staging.dart'; + +/// STAGING flavor entrypoint. +/// Run: flutter run --flavor staging -t lib/main_staging.dart --dart-define-from-file=env/staging.json +Future main() => bootstrap( + firebaseOptions: DefaultFirebaseOptions.currentPlatform, + flavor: 'staging', + );