Appearance
Build & Release
The SafeCall Nav mobile app is built with Flutter and supports both Android and iOS. Release is orchestrated by the monorepo scripts — you don't need to run Flutter commands directly.
Commands
| Command | Description |
|---|---|
bun build | Build everything (server binaries + mobile apps) |
bun build:mobile | Build just the mobile app (AAB + IPA) |
bun build:server | Build just the server binaries |
bun release:mobile | Full release pipeline (test, bump, build, commit, push) |
bun test:contracts | Run contract tests only (server ↔ mobile) |
bun commit | Server release pipeline (now includes contract tests) |
All build and release commands run the integration test gate (server tests + contract tests) before proceeding.
One-Time Setup
Prerequisites
- Flutter SDK (3.10+) installed and on PATH
- For Android: Android SDK with build-tools
- For iOS: macOS with Xcode 15+, Apple Developer Program membership
Android Setup
1. Create an upload keystore
bash
keytool -genkey -v \
-keystore android/upload-keystore.jks \
-storetype JKS \
-keyalg RSA \
-keysize 2048 \
-validity 10000 \
-alias uploadKeep this keystore safe. If lost, you cannot update the app on Google Play.
2. Create key.properties
Create mobile/android/key.properties (gitignored):
properties
storePassword=<your-store-password>
keyPassword=<your-key-password>
keyAlias=upload
storeFile=../upload-keystore.jks3. Configure signing in build.gradle
In mobile/android/app/build.gradle.kts, add the signing config. Flutter's default template already has a placeholder — replace the signingConfigs block:
kotlin
// Load keystore
val keystorePropertiesFile = rootProject.file("key.properties")
val keystoreProperties = java.util.Properties()
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(java.io.FileInputStream(keystorePropertiesFile))
}
android {
signingConfigs {
create("release") {
keyAlias = keystoreProperties["keyAlias"] as String?
keyPassword = keystoreProperties["keyPassword"] as String?
storeFile = keystoreProperties["storeFile"]?.let { file(it) }
storePassword = keystoreProperties["storePassword"] as String?
}
}
buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
}
}
}4. Create app listing in Google Play Console
- Go to https://play.google.com/console
- Create a new application
- Fill in the store listing (title, description, screenshots)
- Upload the first AAB manually via Production > Create new release
iOS Setup
1. Apple Developer Program
Ensure you have an active Apple Developer Program membership at https://developer.apple.com/account.
2. Create App ID & Provisioning Profile
- Go to Apple Developer > Certificates, Identifiers & Profiles
- Create an App ID with bundle identifier
com.safecall.safecallNav - Create a Distribution provisioning profile
3. Configure Xcode signing
- Open
mobile/ios/Runner.xcworkspacein Xcode - Select the Runner target
- In Signing & Capabilities, set:
- Team: your Apple Developer team
- Bundle Identifier:
com.safecall.safecallNav - Provisioning Profile: your distribution profile
4. Create ExportOptions.plist
Create mobile/ios/ExportOptions.plist for automated IPA export:
xml
<?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>method</key>
<string>app-store</string>
<key>teamID</key>
<string>YOUR_TEAM_ID</string>
<key>signingStyle</key>
<string>manual</string>
<key>provisioningProfiles</key>
<dict>
<key>com.safecall.safecallNav</key>
<string>YOUR_PROVISIONING_PROFILE_NAME</string>
</dict>
</dict>
</plist>5. Create app listing in App Store Connect
- Go to https://appstoreconnect.apple.com
- Create a new app
- Fill in the store listing (name, description, screenshots, privacy policy URL)
- Upload the first build manually
Release Process
Releasing a new mobile version
bash
bun release:mobile # interactive (prompts for bump type + message)
bun release:mobile patch # prompts for message only
bun release:mobile minor "Add QR scanning" # non-interactiveThis runs the following pipeline:
- Server tests (
bun testin server/) - Contract tests (server ↔ mobile model validation)
- Flutter analyze (lint check)
- Flutter tests (unit/widget tests)
- Version bump in
pubspec.yaml - Build Android AAB
- Build iOS IPA (macOS only, requires ExportOptions.plist)
- Git commit, tag (
mobile-v{version}), push
After the script completes, it prints a store upload checklist.
Uploading to Google Play Store
- Open https://play.google.com/console
- Select the SafeCall Nav app
- Go to Production > Create new release
- Upload
releases/safecall-nav.aab - Add release notes
- Review and Start rollout
Uploading to Apple App Store
Option A: Command line
bash
xcrun altool --upload-app \
-f releases/SafeCallNav.ipa \
-t ios \
-u YOUR_APPLE_ID \
-p YOUR_APP_SPECIFIC_PASSWORDOption B: Transporter app
Open the Transporter app (from Mac App Store), drag in the IPA, and deliver.
Then in App Store Connect:
- Open https://appstoreconnect.apple.com
- Select the SafeCall Nav app
- Create a new version, select the uploaded build
- Add release notes and screenshots
- Submit for review
Contract Tests
Contract tests verify that the mobile Dart models can parse real server API responses. They run automatically as part of every build and release command.
To run manually:
bash
bun test:contractsThis runs two phases:
- Phase 1 (Bun): Starts an in-memory test server, seeds navigation data, captures real API responses, writes them to
mobile/test/fixtures/*.json - Phase 2 (Dart): Reads the fixtures, deserializes with Dart models, verifies round-trip consistency
Files Reference
| File | Purpose |
|---|---|
scripts/release/mobile.ts | Mobile release pipeline |
scripts/build/mobile.sh | Mobile build only (AAB + IPA) |
scripts/build/all.sh | Full build (server + mobile) |
scripts/test/contracts.ts | Contract test orchestrator |
mobile/test/contract_test.dart | Dart-side contract validation |
mobile/test/fixtures/ | Generated fixture JSON (gitignored) |