Merge branch 'feat/build-flavors'
dev/staging/prod build flavors for client_app + mitra_app: Android product flavors, per-flavor Dart entrypoints + env files, per-flavor Firebase config (both platforms, 3 projects), customer iOS re-based to com.asc.hallobestie. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> # Conflicts: # mitra_app/lib/firebase/firebase_options_dev.dart
This commit is contained in:
184
client_app/BUILD_FLAVORS.md
Normal file
184
client_app/BUILD_FLAVORS.md
Normal file
@@ -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 <f>`, `-t lib/main_<f>.dart`, and
|
||||||
|
`--dart-define-from-file=env/<f>.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-<flavor>-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 <dev|staging|prod>` plus the matching
|
||||||
|
`-t lib/main_<flavor>.dart` and `--dart-define-from-file=env/<flavor>.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/<flavor>/google-services.json` — all 3, each containing a
|
||||||
|
client matching the flavor `applicationId`.
|
||||||
|
- `ios/config/<flavor>/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
|
||||||
@@ -24,7 +24,10 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
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"
|
applicationId = "com.mybestie"
|
||||||
// You can update the following values to match your application needs.
|
// You can update the following values to match your application needs.
|
||||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
@@ -34,6 +37,34 @@ android {
|
|||||||
versionName = flutter.versionName
|
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/<flavor>/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 {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
// TODO: Add your own signing config for the release build.
|
// TODO: Add your own signing config for the release build.
|
||||||
|
|||||||
@@ -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": {
|
"client_info": {
|
||||||
"mobilesdk_app_id": "1:1068156046511:android:8a873c8b7e64410ab8185a",
|
"mobilesdk_app_id": "1:1068156046511:android:8a873c8b7e64410ab8185a",
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
Phase 4 Stage 4 notif-gate via permission_handler. -->
|
Phase 4 Stage 4 notif-gate via permission_handler. -->
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
<application
|
<application
|
||||||
android:label="HaloBestie"
|
android:label="@string/app_name"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:usesCleartextTraffic="true">
|
android:usesCleartextTraffic="true">
|
||||||
|
|||||||
70
client_app/android/app/src/prod/google-services.json
Executable file
70
client_app/android/app/src/prod/google-services.json
Executable file
@@ -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"
|
||||||
|
}
|
||||||
36
client_app/android/app/src/prod/google-services.json.README
Normal file
36
client_app/android/app/src/prod/google-services.json.README
Normal file
@@ -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=<prod-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.
|
||||||
106
client_app/android/app/src/staging/google-services.json
Executable file
106
client_app/android/app/src/staging/google-services.json
Executable file
@@ -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"
|
||||||
|
}
|
||||||
@@ -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=<staging-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.
|
||||||
4
client_app/env/dev.json
vendored
Normal file
4
client_app/env/dev.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"API_BASE_URL": "http://192.168.88.247:3000",
|
||||||
|
"FLAVOR": "dev"
|
||||||
|
}
|
||||||
4
client_app/env/prod.json
vendored
Normal file
4
client_app/env/prod.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"API_BASE_URL": "https://api.halobestie.com",
|
||||||
|
"FLAVOR": "prod"
|
||||||
|
}
|
||||||
5
client_app/env/staging.json
vendored
Normal file
5
client_app/env/staging.json
vendored
Normal file
@@ -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)"
|
||||||
|
}
|
||||||
@@ -496,7 +496,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.mybestie;
|
PRODUCT_BUNDLE_IDENTIFIER = com.asc.hallobestie;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
@@ -513,7 +513,7 @@
|
|||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.mybestie.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.asc.hallobestie.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
@@ -531,7 +531,7 @@
|
|||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.mybestie.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.asc.hallobestie.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
@@ -547,7 +547,7 @@
|
|||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.mybestie.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.asc.hallobestie.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
@@ -679,7 +679,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.mybestie;
|
PRODUCT_BUNDLE_IDENTIFIER = com.asc.hallobestie;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
@@ -702,7 +702,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.mybestie;
|
PRODUCT_BUNDLE_IDENTIFIER = com.asc.hallobestie;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
|
|||||||
@@ -87,7 +87,7 @@
|
|||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleURLName</key>
|
<key>CFBundleURLName</key>
|
||||||
<string>com.mybestie</string>
|
<string>com.asc.hallobestie</string>
|
||||||
<key>CFBundleURLSchemes</key>
|
<key>CFBundleURLSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>halobestie</string>
|
<string>halobestie</string>
|
||||||
|
|||||||
34
client_app/ios/config/README.md
Normal file
34
client_app/ios/config/README.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# iOS per-flavor Firebase config — client_app
|
||||||
|
|
||||||
|
iOS has **no** automatic per-flavor resolution (unlike Android's
|
||||||
|
`android/app/src/<flavor>/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.
|
||||||
30
client_app/ios/config/dev/GoogleService-Info.plist
Executable file
30
client_app/ios/config/dev/GoogleService-Info.plist
Executable file
@@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>API_KEY</key>
|
||||||
|
<string>AIzaSyAQnB5hbj0T5tE4JQZQ9Tx6Whp_u15obMI</string>
|
||||||
|
<key>GCM_SENDER_ID</key>
|
||||||
|
<string>1068156046511</string>
|
||||||
|
<key>PLIST_VERSION</key>
|
||||||
|
<string>1</string>
|
||||||
|
<key>BUNDLE_ID</key>
|
||||||
|
<string>com.asc.hallobestie.dev</string>
|
||||||
|
<key>PROJECT_ID</key>
|
||||||
|
<string>halobestie-clone-dev</string>
|
||||||
|
<key>STORAGE_BUCKET</key>
|
||||||
|
<string>halobestie-clone-dev.firebasestorage.app</string>
|
||||||
|
<key>IS_ADS_ENABLED</key>
|
||||||
|
<false></false>
|
||||||
|
<key>IS_ANALYTICS_ENABLED</key>
|
||||||
|
<false></false>
|
||||||
|
<key>IS_APPINVITE_ENABLED</key>
|
||||||
|
<true></true>
|
||||||
|
<key>IS_GCM_ENABLED</key>
|
||||||
|
<true></true>
|
||||||
|
<key>IS_SIGNIN_ENABLED</key>
|
||||||
|
<true></true>
|
||||||
|
<key>GOOGLE_APP_ID</key>
|
||||||
|
<string>1:1068156046511:ios:bc9098ffc2c2913ab8185a</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
15
client_app/ios/config/dev/GoogleService-Info.plist.README
Normal file
15
client_app/ios/config/dev/GoogleService-Info.plist.README
Normal file
@@ -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.
|
||||||
36
client_app/ios/config/prod/GoogleService-Info.plist
Executable file
36
client_app/ios/config/prod/GoogleService-Info.plist
Executable file
@@ -0,0 +1,36 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CLIENT_ID</key>
|
||||||
|
<string>953866659887-bsb3c2a6ir10u47q8vcacre2tmnk59jb.apps.googleusercontent.com</string>
|
||||||
|
<key>REVERSED_CLIENT_ID</key>
|
||||||
|
<string>com.googleusercontent.apps.953866659887-bsb3c2a6ir10u47q8vcacre2tmnk59jb</string>
|
||||||
|
<key>ANDROID_CLIENT_ID</key>
|
||||||
|
<string>953866659887-5a62u0tdce92i0gmfo0gf3dt0dnlre42.apps.googleusercontent.com</string>
|
||||||
|
<key>API_KEY</key>
|
||||||
|
<string>AIzaSyA8guPSD87eDLeCsH6jVd1n2_SI4_MaGNE</string>
|
||||||
|
<key>GCM_SENDER_ID</key>
|
||||||
|
<string>953866659887</string>
|
||||||
|
<key>PLIST_VERSION</key>
|
||||||
|
<string>1</string>
|
||||||
|
<key>BUNDLE_ID</key>
|
||||||
|
<string>com.asc.hallobestie</string>
|
||||||
|
<key>PROJECT_ID</key>
|
||||||
|
<string>my-bestie-production</string>
|
||||||
|
<key>STORAGE_BUCKET</key>
|
||||||
|
<string>my-bestie-production.firebasestorage.app</string>
|
||||||
|
<key>IS_ADS_ENABLED</key>
|
||||||
|
<false></false>
|
||||||
|
<key>IS_ANALYTICS_ENABLED</key>
|
||||||
|
<false></false>
|
||||||
|
<key>IS_APPINVITE_ENABLED</key>
|
||||||
|
<true></true>
|
||||||
|
<key>IS_GCM_ENABLED</key>
|
||||||
|
<true></true>
|
||||||
|
<key>IS_SIGNIN_ENABLED</key>
|
||||||
|
<true></true>
|
||||||
|
<key>GOOGLE_APP_ID</key>
|
||||||
|
<string>1:953866659887:ios:159fd11b1d2f3633183eda</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
16
client_app/ios/config/prod/GoogleService-Info.plist.README
Normal file
16
client_app/ios/config/prod/GoogleService-Info.plist.README
Normal file
@@ -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.
|
||||||
36
client_app/ios/config/staging/GoogleService-Info.plist
Executable file
36
client_app/ios/config/staging/GoogleService-Info.plist
Executable file
@@ -0,0 +1,36 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CLIENT_ID</key>
|
||||||
|
<string>650461407929-fb4t48nmguaslfvis7ebsea8vndro7ac.apps.googleusercontent.com</string>
|
||||||
|
<key>REVERSED_CLIENT_ID</key>
|
||||||
|
<string>com.googleusercontent.apps.650461407929-fb4t48nmguaslfvis7ebsea8vndro7ac</string>
|
||||||
|
<key>ANDROID_CLIENT_ID</key>
|
||||||
|
<string>650461407929-3els6l9cegtiphe930vtjr80ffa2p1uv.apps.googleusercontent.com</string>
|
||||||
|
<key>API_KEY</key>
|
||||||
|
<string>AIzaSyC_BewS88iaNsc9QdwsPzkV0sf9wUs4i_4</string>
|
||||||
|
<key>GCM_SENDER_ID</key>
|
||||||
|
<string>650461407929</string>
|
||||||
|
<key>PLIST_VERSION</key>
|
||||||
|
<string>1</string>
|
||||||
|
<key>BUNDLE_ID</key>
|
||||||
|
<string>com.asc.hallobestie.staging</string>
|
||||||
|
<key>PROJECT_ID</key>
|
||||||
|
<string>my-bestie-876ec</string>
|
||||||
|
<key>STORAGE_BUCKET</key>
|
||||||
|
<string>my-bestie-876ec.firebasestorage.app</string>
|
||||||
|
<key>IS_ADS_ENABLED</key>
|
||||||
|
<false></false>
|
||||||
|
<key>IS_ANALYTICS_ENABLED</key>
|
||||||
|
<false></false>
|
||||||
|
<key>IS_APPINVITE_ENABLED</key>
|
||||||
|
<true></true>
|
||||||
|
<key>IS_GCM_ENABLED</key>
|
||||||
|
<true></true>
|
||||||
|
<key>IS_SIGNIN_ENABLED</key>
|
||||||
|
<true></true>
|
||||||
|
<key>GOOGLE_APP_ID</key>
|
||||||
|
<string>1:650461407929:ios:4ee79d479b69d688504968</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -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.
|
||||||
@@ -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
|
// ignore_for_file: type=lint
|
||||||
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
|
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
|
||||||
import 'package:flutter/foundation.dart'
|
import 'package:flutter/foundation.dart'
|
||||||
show defaultTargetPlatform, kIsWeb, TargetPlatform;
|
show defaultTargetPlatform, kIsWeb, TargetPlatform;
|
||||||
|
|
||||||
/// Default [FirebaseOptions] for use with your Firebase apps.
|
/// [FirebaseOptions] for the DEV environment (project halobestie-clone-dev).
|
||||||
///
|
class DevFirebaseOptions {
|
||||||
/// Example:
|
|
||||||
/// ```dart
|
|
||||||
/// import 'firebase_options.dart';
|
|
||||||
/// // ...
|
|
||||||
/// await Firebase.initializeApp(
|
|
||||||
/// options: DefaultFirebaseOptions.currentPlatform,
|
|
||||||
/// );
|
|
||||||
/// ```
|
|
||||||
class DefaultFirebaseOptions {
|
|
||||||
static FirebaseOptions get currentPlatform {
|
static FirebaseOptions get currentPlatform {
|
||||||
if (kIsWeb) {
|
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) {
|
switch (defaultTargetPlatform) {
|
||||||
case TargetPlatform.android:
|
case TargetPlatform.android:
|
||||||
@@ -48,7 +43,7 @@ class DefaultFirebaseOptions {
|
|||||||
|
|
||||||
static const FirebaseOptions android = FirebaseOptions(
|
static const FirebaseOptions android = FirebaseOptions(
|
||||||
apiKey: 'AIzaSyDFWlLSWytqwI7LSdUbVrO7J5De9L2LV2U',
|
apiKey: 'AIzaSyDFWlLSWytqwI7LSdUbVrO7J5De9L2LV2U',
|
||||||
appId: '1:1068156046511:android:f30784f6b0423131b8185a',
|
appId: '1:1068156046511:android:1f589ed358ccdad0b8185a',
|
||||||
messagingSenderId: '1068156046511',
|
messagingSenderId: '1068156046511',
|
||||||
projectId: 'halobestie-clone-dev',
|
projectId: 'halobestie-clone-dev',
|
||||||
storageBucket: 'halobestie-clone-dev.firebasestorage.app',
|
storageBucket: 'halobestie-clone-dev.firebasestorage.app',
|
||||||
@@ -56,21 +51,10 @@ class DefaultFirebaseOptions {
|
|||||||
|
|
||||||
static const FirebaseOptions ios = FirebaseOptions(
|
static const FirebaseOptions ios = FirebaseOptions(
|
||||||
apiKey: 'AIzaSyAQnB5hbj0T5tE4JQZQ9Tx6Whp_u15obMI',
|
apiKey: 'AIzaSyAQnB5hbj0T5tE4JQZQ9Tx6Whp_u15obMI',
|
||||||
appId: '1:1068156046511:ios:b781f67a57d6db7bb8185a',
|
appId: '1:1068156046511:ios:bc9098ffc2c2913ab8185a',
|
||||||
messagingSenderId: '1068156046511',
|
messagingSenderId: '1068156046511',
|
||||||
projectId: 'halobestie-clone-dev',
|
projectId: 'halobestie-clone-dev',
|
||||||
storageBucket: 'halobestie-clone-dev.firebasestorage.app',
|
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',
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
61
client_app/lib/firebase/firebase_options_prod.dart
Normal file
61
client_app/lib/firebase/firebase_options_prod.dart
Normal file
@@ -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',
|
||||||
|
);
|
||||||
|
}
|
||||||
61
client_app/lib/firebase/firebase_options_staging.dart
Normal file
61
client_app/lib/firebase/firebase_options_staging.dart
Normal file
@@ -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',
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -15,10 +15,23 @@ import 'core/chat/chat_notifier.dart';
|
|||||||
import 'core/notifications/notification_service.dart';
|
import 'core/notifications/notification_service.dart';
|
||||||
import 'core/pairing/pairing_notifier.dart';
|
import 'core/pairing/pairing_notifier.dart';
|
||||||
import 'core/theme/halo_theme.dart';
|
import 'core/theme/halo_theme.dart';
|
||||||
import 'firebase_options.dart';
|
import 'firebase/firebase_options_dev.dart';
|
||||||
import 'router.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/<flavor>.json` (see BUILD_FLAVORS.md).
|
||||||
|
Future<void> bootstrap({
|
||||||
|
required FirebaseOptions firebaseOptions,
|
||||||
|
required String flavor,
|
||||||
|
}) async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
// Pre-warm flutter_secure_storage. The first call triggers AndroidX
|
// 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.
|
// splash instead of paying it on the user's first interaction.
|
||||||
unawaited(TokenStorage().readRefreshToken());
|
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
|
// Enable GA4 collection. Fire-and-forget so it never adds to cold-start
|
||||||
// latency; the SDK queues events until collection is on.
|
// latency; the SDK queues events until collection is on.
|
||||||
@@ -42,6 +55,16 @@ void main() async {
|
|||||||
runApp(const ProviderScope(child: App()));
|
runApp(const ProviderScope(child: App()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void main() async {
|
||||||
|
// Bare `flutter run` (no `-t lib/main_<flavor>.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 {
|
class App extends ConsumerStatefulWidget {
|
||||||
const App({super.key});
|
const App({super.key});
|
||||||
|
|
||||||
|
|||||||
14
client_app/lib/main_dev.dart
Normal file
14
client_app/lib/main_dev.dart
Normal file
@@ -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',
|
||||||
|
);
|
||||||
|
}
|
||||||
14
client_app/lib/main_prod.dart
Normal file
14
client_app/lib/main_prod.dart
Normal file
@@ -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',
|
||||||
|
);
|
||||||
|
}
|
||||||
14
client_app/lib/main_staging.dart
Normal file
14
client_app/lib/main_staging.dart
Normal file
@@ -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',
|
||||||
|
);
|
||||||
|
}
|
||||||
121
mitra_app/BUILD_FLAVORS.md
Normal file
121
mitra_app/BUILD_FLAVORS.md
Normal file
@@ -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_<flavor>.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_<flavor>.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/<flavor>/google-services.json` — all 3, client matches the flavor applicationId.
|
||||||
|
- `ios/config/<flavor>/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/<flavor>/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`) |
|
||||||
@@ -21,7 +21,8 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
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"
|
applicationId = "com.mybestie.mitra"
|
||||||
// You can update the following values to match your application needs.
|
// You can update the following values to match your application needs.
|
||||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
@@ -31,6 +32,30 @@ android {
|
|||||||
versionName = flutter.versionName
|
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 {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
// TODO: Add your own signing config for the release build.
|
// TODO: Add your own signing config for the release build.
|
||||||
|
|||||||
@@ -61,6 +61,63 @@
|
|||||||
"other_platform_oauth_client": []
|
"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"
|
"configuration_version": "1"
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<application
|
<application
|
||||||
android:label="Mitra HaloBestie"
|
android:label="@string/app_name"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:usesCleartextTraffic="true">
|
android:usesCleartextTraffic="true">
|
||||||
|
|||||||
106
mitra_app/android/app/src/prod/google-services.json
Executable file
106
mitra_app/android/app/src/prod/google-services.json
Executable file
@@ -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"
|
||||||
|
}
|
||||||
29
mitra_app/android/app/src/prod/google-services.json.README
Normal file
29
mitra_app/android/app/src/prod/google-services.json.README
Normal file
@@ -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.
|
||||||
142
mitra_app/android/app/src/staging/google-services.json
Executable file
142
mitra_app/android/app/src/staging/google-services.json
Executable file
@@ -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"
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
4
mitra_app/env/dev.json
vendored
Normal file
4
mitra_app/env/dev.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"API_BASE_URL": "http://192.168.88.247:3000",
|
||||||
|
"FLAVOR": "dev"
|
||||||
|
}
|
||||||
4
mitra_app/env/prod.json
vendored
Normal file
4
mitra_app/env/prod.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"API_BASE_URL": "https://api.halobestie.com",
|
||||||
|
"FLAVOR": "prod"
|
||||||
|
}
|
||||||
4
mitra_app/env/staging.json
vendored
Normal file
4
mitra_app/env/staging.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"API_BASE_URL": "https://staging-api.halobestie.com",
|
||||||
|
"FLAVOR": "staging"
|
||||||
|
}
|
||||||
34
mitra_app/ios/config/README.md
Normal file
34
mitra_app/ios/config/README.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# iOS per-flavor Firebase config — mitra_app
|
||||||
|
|
||||||
|
iOS has **no** automatic per-flavor resolution (unlike Android's
|
||||||
|
`android/app/src/<flavor>/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.
|
||||||
30
mitra_app/ios/config/dev/GoogleService-Info.plist
Executable file
30
mitra_app/ios/config/dev/GoogleService-Info.plist
Executable file
@@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>API_KEY</key>
|
||||||
|
<string>AIzaSyAQnB5hbj0T5tE4JQZQ9Tx6Whp_u15obMI</string>
|
||||||
|
<key>GCM_SENDER_ID</key>
|
||||||
|
<string>1068156046511</string>
|
||||||
|
<key>PLIST_VERSION</key>
|
||||||
|
<string>1</string>
|
||||||
|
<key>BUNDLE_ID</key>
|
||||||
|
<string>com.mybestie.mitra.dev</string>
|
||||||
|
<key>PROJECT_ID</key>
|
||||||
|
<string>halobestie-clone-dev</string>
|
||||||
|
<key>STORAGE_BUCKET</key>
|
||||||
|
<string>halobestie-clone-dev.firebasestorage.app</string>
|
||||||
|
<key>IS_ADS_ENABLED</key>
|
||||||
|
<false></false>
|
||||||
|
<key>IS_ANALYTICS_ENABLED</key>
|
||||||
|
<false></false>
|
||||||
|
<key>IS_APPINVITE_ENABLED</key>
|
||||||
|
<true></true>
|
||||||
|
<key>IS_GCM_ENABLED</key>
|
||||||
|
<true></true>
|
||||||
|
<key>IS_SIGNIN_ENABLED</key>
|
||||||
|
<true></true>
|
||||||
|
<key>GOOGLE_APP_ID</key>
|
||||||
|
<string>1:1068156046511:ios:907b28451e22981db8185a</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
14
mitra_app/ios/config/dev/GoogleService-Info.plist.README
Normal file
14
mitra_app/ios/config/dev/GoogleService-Info.plist.README
Normal file
@@ -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.
|
||||||
36
mitra_app/ios/config/prod/GoogleService-Info.plist
Executable file
36
mitra_app/ios/config/prod/GoogleService-Info.plist
Executable file
@@ -0,0 +1,36 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CLIENT_ID</key>
|
||||||
|
<string>953866659887-i0atpahlpqt6r9id17hjeirt5j1uqu8k.apps.googleusercontent.com</string>
|
||||||
|
<key>REVERSED_CLIENT_ID</key>
|
||||||
|
<string>com.googleusercontent.apps.953866659887-i0atpahlpqt6r9id17hjeirt5j1uqu8k</string>
|
||||||
|
<key>ANDROID_CLIENT_ID</key>
|
||||||
|
<string>953866659887-5a62u0tdce92i0gmfo0gf3dt0dnlre42.apps.googleusercontent.com</string>
|
||||||
|
<key>API_KEY</key>
|
||||||
|
<string>AIzaSyA8guPSD87eDLeCsH6jVd1n2_SI4_MaGNE</string>
|
||||||
|
<key>GCM_SENDER_ID</key>
|
||||||
|
<string>953866659887</string>
|
||||||
|
<key>PLIST_VERSION</key>
|
||||||
|
<string>1</string>
|
||||||
|
<key>BUNDLE_ID</key>
|
||||||
|
<string>com.mybestie.mitra</string>
|
||||||
|
<key>PROJECT_ID</key>
|
||||||
|
<string>my-bestie-production</string>
|
||||||
|
<key>STORAGE_BUCKET</key>
|
||||||
|
<string>my-bestie-production.firebasestorage.app</string>
|
||||||
|
<key>IS_ADS_ENABLED</key>
|
||||||
|
<false></false>
|
||||||
|
<key>IS_ANALYTICS_ENABLED</key>
|
||||||
|
<false></false>
|
||||||
|
<key>IS_APPINVITE_ENABLED</key>
|
||||||
|
<true></true>
|
||||||
|
<key>IS_GCM_ENABLED</key>
|
||||||
|
<true></true>
|
||||||
|
<key>IS_SIGNIN_ENABLED</key>
|
||||||
|
<true></true>
|
||||||
|
<key>GOOGLE_APP_ID</key>
|
||||||
|
<string>1:953866659887:ios:cd8dd704842f3489183eda</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
16
mitra_app/ios/config/prod/GoogleService-Info.plist.README
Normal file
16
mitra_app/ios/config/prod/GoogleService-Info.plist.README
Normal file
@@ -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.
|
||||||
36
mitra_app/ios/config/staging/GoogleService-Info.plist
Executable file
36
mitra_app/ios/config/staging/GoogleService-Info.plist
Executable file
@@ -0,0 +1,36 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CLIENT_ID</key>
|
||||||
|
<string>650461407929-kuc6m53nehsa677geu57f3fmn9ql6lbg.apps.googleusercontent.com</string>
|
||||||
|
<key>REVERSED_CLIENT_ID</key>
|
||||||
|
<string>com.googleusercontent.apps.650461407929-kuc6m53nehsa677geu57f3fmn9ql6lbg</string>
|
||||||
|
<key>ANDROID_CLIENT_ID</key>
|
||||||
|
<string>650461407929-3els6l9cegtiphe930vtjr80ffa2p1uv.apps.googleusercontent.com</string>
|
||||||
|
<key>API_KEY</key>
|
||||||
|
<string>AIzaSyC_BewS88iaNsc9QdwsPzkV0sf9wUs4i_4</string>
|
||||||
|
<key>GCM_SENDER_ID</key>
|
||||||
|
<string>650461407929</string>
|
||||||
|
<key>PLIST_VERSION</key>
|
||||||
|
<string>1</string>
|
||||||
|
<key>BUNDLE_ID</key>
|
||||||
|
<string>com.mybestie.mitra.staging</string>
|
||||||
|
<key>PROJECT_ID</key>
|
||||||
|
<string>my-bestie-876ec</string>
|
||||||
|
<key>STORAGE_BUCKET</key>
|
||||||
|
<string>my-bestie-876ec.firebasestorage.app</string>
|
||||||
|
<key>IS_ADS_ENABLED</key>
|
||||||
|
<false></false>
|
||||||
|
<key>IS_ANALYTICS_ENABLED</key>
|
||||||
|
<false></false>
|
||||||
|
<key>IS_APPINVITE_ENABLED</key>
|
||||||
|
<true></true>
|
||||||
|
<key>IS_GCM_ENABLED</key>
|
||||||
|
<true></true>
|
||||||
|
<key>IS_SIGNIN_ENABLED</key>
|
||||||
|
<true></true>
|
||||||
|
<key>GOOGLE_APP_ID</key>
|
||||||
|
<string>1:650461407929:ios:b273bda6ad4045ca504968</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
15
mitra_app/ios/config/staging/GoogleService-Info.plist.README
Normal file
15
mitra_app/ios/config/staging/GoogleService-Info.plist.README
Normal file
@@ -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.
|
||||||
125
mitra_app/lib/bootstrap.dart
Normal file
125
mitra_app/lib/bootstrap.dart
Normal file
@@ -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<void> 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<App> createState() => _AppState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppState extends ConsumerState<App> 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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
// ignore_for_file: type=lint
|
||||||
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
|
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
|
||||||
import 'package:flutter/foundation.dart'
|
import 'package:flutter/foundation.dart'
|
||||||
show defaultTargetPlatform, kIsWeb, TargetPlatform;
|
show defaultTargetPlatform, kIsWeb, TargetPlatform;
|
||||||
|
|
||||||
/// Default [FirebaseOptions] for use with your Firebase apps.
|
/// [FirebaseOptions] for the DEV environment (project halobestie-clone-dev).
|
||||||
///
|
|
||||||
/// Example:
|
|
||||||
/// ```dart
|
|
||||||
/// import 'firebase_options.dart';
|
|
||||||
/// // ...
|
|
||||||
/// await Firebase.initializeApp(
|
|
||||||
/// options: DefaultFirebaseOptions.currentPlatform,
|
|
||||||
/// );
|
|
||||||
/// ```
|
|
||||||
class DefaultFirebaseOptions {
|
class DefaultFirebaseOptions {
|
||||||
static FirebaseOptions get currentPlatform {
|
static FirebaseOptions get currentPlatform {
|
||||||
if (kIsWeb) {
|
if (kIsWeb) {
|
||||||
throw UnsupportedError(
|
throw UnsupportedError(
|
||||||
'DefaultFirebaseOptions have not been configured for web - '
|
'DefaultFirebaseOptions have not been configured for web - '
|
||||||
'you can reconfigure this by running the FlutterFire CLI again.',
|
'you can reconfigure this by running the FlutterFire CLI again.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
switch (defaultTargetPlatform) {
|
switch (defaultTargetPlatform) {
|
||||||
case TargetPlatform.android:
|
case TargetPlatform.android:
|
||||||
@@ -51,7 +43,7 @@ class DefaultFirebaseOptions {
|
|||||||
|
|
||||||
static const FirebaseOptions android = FirebaseOptions(
|
static const FirebaseOptions android = FirebaseOptions(
|
||||||
apiKey: 'AIzaSyDFWlLSWytqwI7LSdUbVrO7J5De9L2LV2U',
|
apiKey: 'AIzaSyDFWlLSWytqwI7LSdUbVrO7J5De9L2LV2U',
|
||||||
appId: '1:1068156046511:android:4f8fe9a3c7c14c57b8185a',
|
appId: '1:1068156046511:android:f527c763dea3dc36b8185a',
|
||||||
messagingSenderId: '1068156046511',
|
messagingSenderId: '1068156046511',
|
||||||
projectId: 'halobestie-clone-dev',
|
projectId: 'halobestie-clone-dev',
|
||||||
storageBucket: 'halobestie-clone-dev.firebasestorage.app',
|
storageBucket: 'halobestie-clone-dev.firebasestorage.app',
|
||||||
@@ -59,10 +51,10 @@ class DefaultFirebaseOptions {
|
|||||||
|
|
||||||
static const FirebaseOptions ios = FirebaseOptions(
|
static const FirebaseOptions ios = FirebaseOptions(
|
||||||
apiKey: 'AIzaSyAQnB5hbj0T5tE4JQZQ9Tx6Whp_u15obMI',
|
apiKey: 'AIzaSyAQnB5hbj0T5tE4JQZQ9Tx6Whp_u15obMI',
|
||||||
appId: '1:1068156046511:ios:c7786cedb9101d34b8185a',
|
appId: '1:1068156046511:ios:907b28451e22981db8185a',
|
||||||
messagingSenderId: '1068156046511',
|
messagingSenderId: '1068156046511',
|
||||||
projectId: 'halobestie-clone-dev',
|
projectId: 'halobestie-clone-dev',
|
||||||
storageBucket: 'halobestie-clone-dev.firebasestorage.app',
|
storageBucket: 'halobestie-clone-dev.firebasestorage.app',
|
||||||
iosBundleId: 'com.mybestie',
|
iosBundleId: 'com.mybestie.mitra.dev',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
61
mitra_app/lib/firebase/firebase_options_prod.dart
Normal file
61
mitra_app/lib/firebase/firebase_options_prod.dart
Normal file
@@ -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',
|
||||||
|
);
|
||||||
|
}
|
||||||
61
mitra_app/lib/firebase/firebase_options_staging.dart
Normal file
61
mitra_app/lib/firebase/firebase_options_staging.dart
Normal file
@@ -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',
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,120 +1,15 @@
|
|||||||
import 'package:firebase_core/firebase_core.dart';
|
import 'bootstrap.dart';
|
||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
import 'firebase/firebase_options_dev.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';
|
|
||||||
|
|
||||||
void main() async {
|
/// Default entrypoint — delegates to the DEV flavor so a bare `flutter run`
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
/// (without -t) still works during local development. The `App` widget and the
|
||||||
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
/// shared startup logic now live in [bootstrap].
|
||||||
|
///
|
||||||
final messaging = FirebaseMessaging.instance;
|
/// For an explicit flavor, use the dedicated entrypoints instead:
|
||||||
await messaging.requestPermission();
|
/// - 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)
|
||||||
runApp(const ProviderScope(child: App()));
|
/// - lib/main_prod.dart (flutter run --flavor prod -t lib/main_prod.dart --dart-define-from-file=env/prod.json)
|
||||||
}
|
Future<void> main() => bootstrap(
|
||||||
|
firebaseOptions: DefaultFirebaseOptions.currentPlatform,
|
||||||
class App extends ConsumerStatefulWidget {
|
flavor: 'dev',
|
||||||
const App({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
ConsumerState<App> createState() => _AppState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AppState extends ConsumerState<App> 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,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
9
mitra_app/lib/main_dev.dart
Normal file
9
mitra_app/lib/main_dev.dart
Normal file
@@ -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<void> main() => bootstrap(
|
||||||
|
firebaseOptions: DefaultFirebaseOptions.currentPlatform,
|
||||||
|
flavor: 'dev',
|
||||||
|
);
|
||||||
9
mitra_app/lib/main_prod.dart
Normal file
9
mitra_app/lib/main_prod.dart
Normal file
@@ -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<void> main() => bootstrap(
|
||||||
|
firebaseOptions: DefaultFirebaseOptions.currentPlatform,
|
||||||
|
flavor: 'prod',
|
||||||
|
);
|
||||||
9
mitra_app/lib/main_staging.dart
Normal file
9
mitra_app/lib/main_staging.dart
Normal file
@@ -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<void> main() => bootstrap(
|
||||||
|
firebaseOptions: DefaultFirebaseOptions.currentPlatform,
|
||||||
|
flavor: 'staging',
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user