Skip to content

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

CommandDescription
bun buildBuild everything (server binaries + mobile apps)
bun build:mobileBuild just the mobile app (AAB + IPA)
bun build:serverBuild just the server binaries
bun release:mobileFull release pipeline (test, bump, build, commit, push)
bun test:contractsRun contract tests only (server ↔ mobile)
bun commitServer 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 upload

Keep 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.jks

3. 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

  1. Go to https://play.google.com/console
  2. Create a new application
  3. Fill in the store listing (title, description, screenshots)
  4. 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

  1. Go to Apple Developer > Certificates, Identifiers & Profiles
  2. Create an App ID with bundle identifier com.safecall.safecallNav
  3. Create a Distribution provisioning profile

3. Configure Xcode signing

  1. Open mobile/ios/Runner.xcworkspace in Xcode
  2. Select the Runner target
  3. 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

  1. Go to https://appstoreconnect.apple.com
  2. Create a new app
  3. Fill in the store listing (name, description, screenshots, privacy policy URL)
  4. 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-interactive

This runs the following pipeline:

  1. Server tests (bun test in server/)
  2. Contract tests (server ↔ mobile model validation)
  3. Flutter analyze (lint check)
  4. Flutter tests (unit/widget tests)
  5. Version bump in pubspec.yaml
  6. Build Android AAB
  7. Build iOS IPA (macOS only, requires ExportOptions.plist)
  8. Git commit, tag (mobile-v{version}), push

After the script completes, it prints a store upload checklist.

Uploading to Google Play Store

  1. Open https://play.google.com/console
  2. Select the SafeCall Nav app
  3. Go to Production > Create new release
  4. Upload releases/safecall-nav.aab
  5. Add release notes
  6. 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_PASSWORD

Option B: Transporter app

Open the Transporter app (from Mac App Store), drag in the IPA, and deliver.

Then in App Store Connect:

  1. Open https://appstoreconnect.apple.com
  2. Select the SafeCall Nav app
  3. Create a new version, select the uploaded build
  4. Add release notes and screenshots
  5. 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:contracts

This runs two phases:

  1. Phase 1 (Bun): Starts an in-memory test server, seeds navigation data, captures real API responses, writes them to mobile/test/fixtures/*.json
  2. Phase 2 (Dart): Reads the fixtures, deserializes with Dart models, verifies round-trip consistency

Files Reference

FilePurpose
scripts/release/mobile.tsMobile release pipeline
scripts/build/mobile.shMobile build only (AAB + IPA)
scripts/build/all.shFull build (server + mobile)
scripts/test/contracts.tsContract test orchestrator
mobile/test/contract_test.dartDart-side contract validation
mobile/test/fixtures/Generated fixture JSON (gitignored)