Repository and structure
How hoco-mobile is created and laid out: GitHub settings, the Dart pub workspace with Melos scripts, the package tree, application ids, flavors, Firebase projects, signing and the bootstrap commands.
hoco-mobile is the second repository of the project (ADR-001): one Flutter monorepo holding both apps and the shared hoco_* packages. It lives under Imad's GitHub account until it is transferred to an organisation before go-live (repositories and access). This page is the setup of MOB-01.
Create the repository
# Private repository under Imad's account, default branch dev
gh repo create hoco-mobile --private \
--description "HOCO B2B — reseller and delegate Flutter apps" --clone
cd hoco-mobile
git checkout -b master && git commit --allow-empty -m "chore: initial commit" && git push -u origin master
git checkout -b dev && git push -u origin dev
gh repo edit --default-branch dev --enable-squash-merge --enable-merge-commit \
--enable-rebase-merge=false --delete-branch-on-merge
# Collaborators (personal repositories give collaborators write access)
gh api -X PUT repos/{owner}/hoco-mobile/collaborators/AnisBetehi
gh api -X PUT repos/{owner}/hoco-mobile/collaborators/<akram-github-handle>
# Labels used by PR rules and triage
for l in contract reseller delegate design-qa pilot hotfix mobile-impact; do gh label create "$l"; done| Setting | Value | Why |
|---|---|---|
| Visibility | Private | Signing configuration, environment files and client code |
| Default branch | dev | PRs target integration by default, as in hoco |
| Merge buttons | Squash (default) and merge commit; rebase disabled | Squash into dev, merge commits into master (git workflow) |
| Branch protection | master and dev: PR only, CI checks required, no force push; dev needs one code-owner approval except the repository owner's bypass outside sensitive paths | Same rules as hoco — the table is on repositories and access |
| Plan | GitHub Pro on Imad's account | Protection and rulesets on private repositories need Pro, Team or Enterprise; Free gives 2 000 Actions minutes a month, Pro 3 000 |
| Secrets | Upload keystore and passwords, Firebase App Distribution and Play service accounts, Sentry auth token | Listed on repositories and access — never in the repository |
Branches follow the shared model: master (production) ← dev (integration) ← feature/<TASK-ID>-short-desc (for example feature/MDA-05-visit-flow), fix/<TASK-ID>-…, release/x.y.z, hotfix/x.y.z. .github/CODEOWNERS marks hoco_api, hoco_core, hoco_offline, the platform folders and .github as sensitive paths that wait for Anis's approval (the file is on repositories and access).
Toolchain and SDK constraints
| Tool | Version rule | Notes |
|---|---|---|
| Flutter | Latest stable on day 1, pinned with FVM (.fvmrc committed); upgraded deliberately in a chore(workspace) PR, never mid-feature | CI reads the same .fvmrc |
| Dart | The one bundled with the pinned Flutter; environment.sdk: ^3.10.0 in every pubspec.yaml (raise it to the bundled minor) | Pub workspaces need Dart 3.6 or later |
| Android | minSdk 24 (Android 7.0), compileSdk and targetSdk = latest API level (36 at kickoff) | Play requires targeting an API level released within the last year; Android 15+ targets also need 16 KB page-size compatible native libraries (build and release) |
| JDK | 17 (Temurin) | Required by the Android Gradle plugin |
| Node | 22 | Prism mock server, commitlint |
| Melos | 7.x, as a dev dependency of the workspace root | Melos 7 reads its configuration from the root pubspec.yaml |
| Xcode | Only if iOS is decided (D-10) | MOB-16 |
Workspace and Melos
One flutter pub get at the root resolves every package together (one lockfile, one version of each dependency). Each member declares resolution: workspace.
name: hoco_mobile
publish_to: none
environment:
sdk: ^3.10.0
workspace:
- apps/reseller
- apps/delegate
- apps/gallery
- packages/hoco_core
- packages/hoco_api
- packages/hoco_ui
- packages/hoco_l10n
- packages/hoco_offline
- packages/hoco_maps
- packages/hoco_catalog
dev_dependencies:
melos: ^7.0.0
melos:
scripts:
analyze:
description: Static analysis of the whole workspace; infos fail the build
run: flutter analyze --fatal-infos
format:
description: Format every Dart file
run: dart format .
format:check:
description: CI — fail when a file is not formatted
run: dart format --output=none --set-exit-if-changed .
test:
description: Unit, widget and golden tests of every package that has tests
run: flutter test --no-pub
exec:
concurrency: 2
failFast: true
packageFilters:
dirExists: test
test:coverage:
run: flutter test --no-pub --coverage
exec:
concurrency: 2
packageFilters:
dirExists: test
gen:
description: build_runner — freezed, json_serializable, riverpod, retrofit, drift
run: dart run build_runner build --delete-conflicting-outputs
exec:
concurrency: 1
packageFilters:
dependsOn: build_runner
api:gen:
description: Regenerate hoco_api from the pinned packages/hoco_api/openapi/hoco-v1.json
run: >-
melos exec --scope=hoco_api -- dart run swagger_parser &&
melos exec --scope=hoco_api -- dart run build_runner build --delete-conflicting-outputs
api:sync:
description: Download a contract version, pin it and regenerate (see API integration)
run: bash tool/api_sync.sh
l10n:
description: Enum labels from enums.json, then flutter gen-l10n
run: >-
dart run tool/gen_enum_l10n.dart &&
melos exec --scope=hoco_l10n -- flutter gen-l10n
tokens:
description: Akram's token JSON to Dart (hoco_ui)
run: dart run tool/gen_tokens.dart
mock:
description: Prism mock server of the pinned contract on port 4010
run: npx --yes @stoplight/prism-cli@5 mock packages/hoco_api/openapi/hoco-v1.json --host 0.0.0.0 --port 4010A member package, for example packages/hoco_core/pubspec.yaml:
name: hoco_core
publish_to: none
resolution: workspace
environment:
sdk: ^3.10.0
flutter: ">=3.38.0"
dependencies:
flutter:
sdk: flutter
dio: ^5.8.0
flutter_riverpod: ^3.0.0
riverpod_annotation: ^3.0.0
freezed_annotation: ^3.0.0
json_annotation: ^4.9.0
flutter_secure_storage: ^9.2.0
connectivity_plus: ^6.1.0
package_info_plus: ^8.3.0
sentry_flutter: ^9.0.0
sentry_dio: ^9.0.0
uuid: ^4.5.0
intl: any # pinned by flutter_localizations
logging: ^1.3.0
dev_dependencies:
build_runner: ^2.4.0
freezed: ^3.0.0
json_serializable: ^6.9.0
riverpod_generator: ^3.0.0
mocktail: ^1.0.4
very_good_analysis: ^7.0.0Version numbers above are floors to check on day 1, not a lockfile; pubspec.lock at the root is the truth and is committed.
Folder tree
Who may depend on whom — arrows point to the dependency; nothing depends on an app, and hoco_api is only imported by data/ layers:
The reseller app uses hoco_maps only for the coarse location of the WhatsApp region and the optional address pin; it does not embed a map in V1.
Application ids and flavors
Flavors decide identity (application id, launcher name and icon, Firebase project); the env/*.json file passed with --dart-define-from-file decides endpoints (API URL, Sentry environment). That keeps pre-production builds possible without a fourth flavor.
| Flavor | Reseller app id | Delegate app id | Launcher name | Icon | Firebase project | Env files |
|---|---|---|---|---|---|---|
dev | dz.hoco.reseller.dev | dz.hoco.delegate.dev | HOCO Rev DEV / HOCO Dél DEV | Akram's icon with a DEV ribbon | hoco-dev | dev.json (local server), mock.json (Prism) |
staging | dz.hoco.reseller.staging | dz.hoco.delegate.staging | HOCO Rev STG / HOCO Dél STG | STG ribbon | hoco-staging | staging.json, preprod.json (acceptance builds) |
prod | dz.hoco.reseller | dz.hoco.delegate | HOCO Revendeur / HOCO Délégué (Assumption — store names to confirm with HOCO) | Final icon (DQA-01) | hoco-prod | prod.json |
The three flavors install side by side on one phone.
{
"HOCO_ENV": "staging",
"API_BASE_URL": "https://api-staging.hoco.dz/v1",
"WEB_BASE_URL": "https://app-staging.hoco.dz",
"SENTRY_DSN": "https://examplePublicKey@o0.ingest.sentry.io/0",
"MOCK_BASE_URL": ""
}| Env file | API_BASE_URL | Notes |
|---|---|---|
mock.json | http://10.0.2.2:4010 | Prism on the host machine, seen from the Android emulator; on a USB phone run adb reverse tcp:4010 tcp:4010 and use http://127.0.0.1:4010 |
dev.json | http://10.0.2.2:3100/v1 | Local server from the hoco docker stack (port 3100) |
staging.json | https://api-staging.hoco.dz/v1 | Auto-deployed from dev of hoco |
preprod.json | Assumption — https://api-preprod.hoco.dz/v1, to confirm when Anis sets up pre-production (OPS-02) | Built with the staging flavor for HOCO's acceptance |
prod.json | https://api.hoco.dz/v1 | The hoco.dz domain is a working assumption |
Environment files hold no secrets: a Sentry DSN and URLs are public by nature. The Maps API key goes into the Android manifest from Gradle (below).
apps/reseller/android/app/build.gradle.kts (the delegate app is identical with its own ids):
import java.util.Properties
val keystoreProperties = Properties().apply {
val file = rootProject.file("key.properties")
if (file.exists()) file.inputStream().use { load(it) }
}
val secrets = Properties().apply {
val file = rootProject.file("secrets.properties") // MAPS_API_KEY_DEV=…, git-ignored, CI writes it
if (file.exists()) file.inputStream().use { load(it) }
}
android {
namespace = "dz.hoco.reseller"
compileSdk = flutter.compileSdkVersion
defaultConfig {
applicationId = "dz.hoco.reseller"
minSdk = 24
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
flavorDimensions += "env"
productFlavors {
create("dev") {
dimension = "env"
applicationIdSuffix = ".dev"
versionNameSuffix = "-dev"
resValue("string", "app_name", "HOCO Rev DEV")
manifestPlaceholders["mapsApiKey"] = secrets.getProperty("MAPS_API_KEY_DEV", "")
manifestPlaceholders["appLinkHost"] = "app-dev.hoco.dz"
}
create("staging") {
dimension = "env"
applicationIdSuffix = ".staging"
versionNameSuffix = "-stg"
resValue("string", "app_name", "HOCO Rev STG")
manifestPlaceholders["mapsApiKey"] = secrets.getProperty("MAPS_API_KEY_STAGING", "")
manifestPlaceholders["appLinkHost"] = "app-staging.hoco.dz"
}
create("prod") {
dimension = "env"
resValue("string", "app_name", "HOCO Revendeur")
manifestPlaceholders["mapsApiKey"] = secrets.getProperty("MAPS_API_KEY_PROD", "")
manifestPlaceholders["appLinkHost"] = "app.hoco.dz"
}
}
signingConfigs {
create("release") {
keyAlias = keystoreProperties.getProperty("keyAlias")
keyPassword = keystoreProperties.getProperty("keyPassword")
storeFile = keystoreProperties.getProperty("storeFile")?.let { file(it) }
storePassword = keystoreProperties.getProperty("storePassword")
}
}
buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = true
isShrinkResources = true
}
}
}The manifest uses android:label="@string/app_name". Per-flavor launcher icons come from flutter_launcher_icons with one config file per flavor (flutter_launcher_icons-dev.yaml, …) generating into src/<flavor>/res.
Cleartext for dev only. Android blocks plain HTTP since API 28. src/dev/res/xml/network_security_config.xml allows it for the emulator host and USB-reversed localhost, and src/dev/AndroidManifest.xml references it — staging and prod keep the default (HTTPS only):
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="false">10.0.2.2</domain>
<domain includeSubdomains="false">127.0.0.1</domain>
<domain includeSubdomains="false">localhost</domain>
</domain-config>
</network-security-config>Running a flavor:
cd apps/reseller
flutter run --flavor dev --dart-define-from-file=env/mock.json
flutter run --flavor staging --dart-define-from-file=env/staging.json
flutter build apk --flavor staging --release --dart-define-from-file=env/staging.json
flutter build appbundle --flavor prod --release --dart-define-from-file=env/prod.json \
--obfuscate --split-debug-info=build/symbolsFirebase projects
One Firebase project per environment, owned by HOCO's Google account, each registering both apps with the flavor's application id. A server environment and an app flavor must use the same project: FCM tokens are project-scoped, so the staging API sends with the hoco-staging service account to tokens produced by staging builds.
| Firebase project | Android apps | Server environment that sends | Used for |
|---|---|---|---|
hoco-dev | dz.hoco.reseller.dev, dz.hoco.delegate.dev | Local server | FCM tests |
hoco-staging | dz.hoco.reseller.staging, dz.hoco.delegate.staging | Staging and pre-production | FCM, App Distribution of test builds |
hoco-prod | dz.hoco.reseller, dz.hoco.delegate | Production | FCM |
Project ids are proposals; Google may append a suffix if they are taken. Generate the configuration per flavor with the FlutterFire CLI:
dart pub global activate flutterfire_cli
cd apps/reseller
flutterfire configure --project=hoco-dev --platforms=android \
--android-package-name=dz.hoco.reseller.dev \
--out=lib/firebase_options_dev.dart \
--android-out=android/app/src/dev/google-services.json
# repeat for staging and prod, then for apps/delegatebootstrap() picks DefaultFirebaseOptions of the flavor from HOCO_ENV. Push setup and routing are on notifications and deep links.
Signing
- Play App Signing holds the app signing key; the team only holds an upload key per app (
upload-reseller.jks,upload-delegate.jks), generated once by Imad:
keytool -genkeypair -v -keystore upload-reseller.jks -alias upload \
-keyalg RSA -keysize 2048 -validity 10000android/key.properties(git-ignored) points to it locally; CI rebuilds it from secrets (ANDROID_KEYSTORE_BASE64,ANDROID_KEYSTORE_PASSWORD,ANDROID_KEY_ALIAS,ANDROID_KEY_PASSWORD):
storeFile=C:/keys/hoco/upload-reseller.jks
storePassword=…
keyAlias=upload
keyPassword=…- The master copy of each keystore and its passwords lives in the shared password-manager vault (Mobile collection: Imad, Anis). A lost upload key is recoverable through Play support, but it blocks releases for days.
- Fingerprints to collect once the apps exist in Play Console: SHA-256 of the app signing key (for
assetlinks.json) and SHA-1 of the upload, app signing and debug keys (for the Maps API key restriction). Details in build and release.
Bootstrap commands
Toolchain. Install FVM and pin Flutter stable in the repository.
dart pub global activate fvm
fvm install stable
fvm use stable --pin # writes .fvmrc with the exact version
dart pub global activate melosApps and packages.
fvm flutter create --org dz.hoco --project-name reseller --platforms android,ios apps/reseller
fvm flutter create --org dz.hoco --project-name delegate --platforms android,ios apps/delegate
fvm flutter create --project-name gallery --platforms android,web apps/gallery
for p in hoco_core hoco_api hoco_ui hoco_l10n hoco_offline hoco_maps hoco_catalog; do
fvm flutter create --template=package "packages/$p"
doneThen add resolution: workspace to every member, write the root pubspec.yaml above and run fvm flutter pub get at the root.
Hooks and lints. analysis_options.yaml at the root (conventions); npm i -g lefthook (or Scoop on Windows), lefthook install; commitlint with the same configuration as hoco.
Flavors and environments. Gradle flavors as above, env/*.json, dev network security config, launcher icons per flavor. Check: the three flavors install side by side.
CI. .github/workflows/ci.yml running melos run format:check, melos run analyze, melos run test on every PR (quality and testing). Make it a required check once green.
Daily commands.
fvm flutter pub get # whole workspace
melos run gen # after changing a freezed / riverpod / drift / retrofit file
melos run l10n # after editing an ARB file or enums.json
melos run mock # Prism on :4010 (needs the pinned contract)
melos run api:sync # pull a new contract version, regenerate hoco_api
melos run analyze && melos run testMobile track
The two Flutter apps at a glance: what each does, Imad's plan, how the mobile work runs in parallel with the server contract, and the first-week checklist.
App architecture
How both Flutter apps are organised: feature-first layers, Riverpod 3 state, go_router routes for every RA and DA screen, guest mode and status gating, the facet-driven catalog query, errors, configuration, observability and performance rules.