Skip to content

Android app (APK)

The Android app is a thin shell around the live site: it opens https://alhudatravels.in/m — the phone layout of the ERP — full-screen in the phone's WebView. Every release of the web app is a release of the Android app — nothing to rebuild or reinstall. The service worker, the tour leader's outbox and the manifest cache (Tour leader app) work inside it as they do in Chrome. Rule PLT-060.

Package: in.alhudatravels.app. Built with Capacitor (capacitor.config.ts, android/).

Getting an APK

.github/workflows/android-apk.yml builds it. It is not part of CI (paid minutes):

  • By hand: Actions → Android APK → Run workflow (optionally type a version name).
  • By tag: git tag android-v1 && git push origin android-v1.

The run leaves two artifacts (Actions → the run → Artifacts):

Artifact What it is For
alhuda-travels-debug-<version> Debug APK, signed with a throwaway debug key Testing on a phone. Android asks to allow installs from unknown sources
alhuda-travels-release-<version> Release APK signed with the company keystore Staff phones; later the Play Store (as an AAB)

The release APK is built only when the signing secrets exist. Without them the run says so in its summary and stops at the debug APK. A release is never signed with a throwaway key: the phone would refuse the next update as "from a different developer".

versionCode is the workflow's run number, so it always rises; versionName is the package.json version unless one is typed.

Signing key — one-time setup (owner or IT)

The keystore is the app's identity. Lose it and no future build can update the installed app; leak it and anyone can publish as us. Keep it in the password manager, never in the repo or in chat.

keytool -genkeypair -v -keystore alhuda-android.keystore -alias alhuda \
  -keyalg RSA -keysize 2048 -validity 10000
base64 -i alhuda-android.keystore | pbcopy    # macOS: the base64 is now on the clipboard

Then GitHub → Settings → Secrets and variables → Actions:

Secret Value
ANDROID_KEYSTORE_BASE64 the base64 of the keystore file
ANDROID_KEYSTORE_PASSWORD the store password
ANDROID_KEY_ALIAS alhuda
ANDROID_KEY_PASSWORD the key password

Installing on a phone

  1. Download the APK on the phone (from the artifact zip, or a link staff share).
  2. Open it. Allow Install unknown apps for the browser or file manager if asked.
  3. Open Alhuda Travels. It loads the live site; sign in as usual.

The tour leader's location prompt (when they tap Attach my location) is the phone's normal permission dialog; the app declares ACCESS_FINE_LOCATION for it.

Notifications (Firebase) — one-time setup

The WebView has no Web Push, so the app uses Firebase Cloud Messaging. Without this set-up the app works; only alerts are off and the Notifications screen says so.

  1. Firebase console → create a project (any name) → Add app → Android, package name in.alhudatravels.app. Download google-services.json.
  2. Project settings → Service accounts → Generate new private key. Download the JSON.
  3. Secrets:
  4. GitHub → Actions secret GOOGLE_SERVICES_JSON = base64 -i google-services.json. The workflow writes the file before building. For a laptop build put the file at android/app/google-services.json (git-ignored).
  5. Supabase → supabase secrets set FCM_SERVICE_ACCOUNT_JSON="$(cat service-account.json)", then npx supabase functions deploy push-send.
  6. Rebuild the APK. On first open the app asks for notification permission (Android 13+) and stores its device token; the same push-send that reaches browsers now reaches the app.

Building on a laptop

Needs the Android SDK (Android Studio installs it) and Java 17+.

npx cap sync android                         # copies capacitor-web/ (a placeholder; the app opens the live site)
cd android && ./gradlew assembleDebug        # → app/build/outputs/apk/debug/app-debug.apk

npx cap open android opens the project in Android Studio.

Changing the app

  • Which page it opens: server.url in capacitor.config.ts (/m, the phone ERP). Sign-in, Supabase and the captcha are on allowNavigation; any other link opens in the browser.
  • Name, icon, colours: android/app/src/main/res/ (icons generated from public/pwa-512x512.png with @capacitor/assets; re-run npx @capacitor/assets generate --android after changing assets/icon.png).
  • Permissions: android/app/src/main/AndroidManifest.xml.

After any of these, run npx cap sync android and rebuild.

The native app

The native app (apps/mobile, Expo) is a different build from the Capacitor shell above: its own screens, its own package (in.alhudatravels.app too — do not install both on one phone with the same package name; the native app replaces the shell), built from .github/workflows/mobile-app.yml.

Checks run on every pull request that touches apps/mobile: npm ci, tsc --noEmit, expo lint, vitest. The debug APK is built only by hand (Actions → Mobile app → Run workflow) or on a push to main, to save runner minutes: expo prebuild + Gradle takes ten minutes or more. The artifact is alhuda-travels-app-debug-<run number>.

On a laptop (Android SDK, JDK 21):

cd apps/mobile && npm ci
npx expo prebuild --platform android --clean
cd android && ./gradlew assembleDebug        # → app/build/outputs/apk/debug/app-debug.apk

The APK for staff and travellers' phones is the release build — it carries its own JavaScript and takes over-the-air updates (below); the debug APK needs a developer's computer running Metro:

cd apps/mobile/android
ANDROID_HOME=$HOME/Library/Android/sdk JAVA_HOME=/opt/homebrew/opt/openjdk@21 \
  ./gradlew assembleRelease -PreactNativeArchitectures=arm64-v8a
# → app/build/outputs/apk/release/app-release.apk (about 58 MB)

The android/ folder is generated; do not edit it by hand — change app.json and rebuild.

Environment (apps/mobile/.env, all EXPO_PUBLIC_*, baked into the build):

Variable Value
EXPO_PUBLIC_SUPABASE_URL the project URL
EXPO_PUBLIC_SUPABASE_ANON_KEY the anon key
EXPO_PUBLIC_MOBILE_APP_KEY the app's key, the same value as the MOBILE_APP_KEY secret on auth-login

Secrets the owner sets (one-time):

Where Secret For
Supabase → auth-login MOBILE_APP_KEY the app identifies itself when signing in (supabase secrets set MOBILE_APP_KEY=…, then deploy auth-login)
Firebase → the Android app google-services.json push notifications on the phone. In CI: GitHub secret GOOGLE_SERVICES_JSON = base64 -i google-services.json, written to apps/mobile/google-services.json before prebuild. On a laptop: put the file there (git-ignored). Without it the workflow removes android.googleServicesFile from app.json before prebuild — prebuild fails on a missing file — and the app builds with notifications off
Supabase → push-send FCM_SERVICE_ACCOUNT_JSON delivering to FCM tokens (same secret as the Capacitor shell, above)
Supabase → extract-passport ANTHROPIC_API_KEY reading a photographed passport; without it the app still reads the machine-readable lines typed on the phone

Updates without a new APK

A fix that changes only JavaScript (screens, wording, logic in apps/mobile/src) reaches installed phones through EAS Update, without a reinstall (PLT-061; what the phone does: native app §10).

Setting Value Where
Expo account / project organization alhudatravels (company-owned since 01/10/2026; owner login alhuda-admin, admins ceo-alhuda and syedhamidali) / alhuda-travels, id fd4a2fa8-b795-4c3b-92d5-df73f4269e4c app.json → owner, extra.eas.projectId
Update URL https://u.expo.dev/<project id> app.json → updates.url
Runtime version policy appVersion: the version in app.json app.json → runtimeVersion
Channel an APK listens to production; APP_UPDATE_CHANNEL=preview at prebuild for a test phone app.config.js (a Gradle build has no EAS build profile, so the channel is baked in here)
When a phone checks on every cold start, without waiting; the update runs on the next cold start app.json → checkAutomatically: ON_LOAD, fallbackToCacheTimeout: 0

The project id is not a secret. The Expo login is: the Mac is signed in with npx expo login; npx expo whoami shows who.

Once: the APK that can take updates. Phones installed before expo-updates was added (app version 0.1.0) cannot receive updates. Build the release APK (above) from main and install it on every phone one last time. From then on, JavaScript fixes go over the air.

Publishing a fix (from the office Mac, on an up-to-date main, from apps/mobile):

git pull && npm ci
npm run update:preview -- "Fix: receipts list shows the payer"      # optional: a test phone on preview
npm run update:production -- "Fix: receipts list shows the payer"
npm run update:list                                                  # what production is serving
  • The bundle is built on the Mac and the EXPO_PUBLIC_* values in apps/mobile/.env are baked into it. npm run update:check (run first by both scripts) refuses to publish when any of the three is missing — a bundle without them would sign every phone out for good. Never publish from a checkout that lacks .env.
  • An update is served to APKs with the same version only. Publishing does not change version.
  • Phones get it on their next cold start and run it on the one after. Staff check More → About.
  • Merged to main first, published second: an update is a release to every phone, and follows the same rules as a web release (PLT-014).

A bad update. Serve the last good one again: npm run update:list shows each update's group id; npx eas-cli@latest update:republish --group <good group id> (or, on expo.dev, the project → Updates → the earlier update → Republish). Phones pick it up the same way. An update that crashes on start is rolled back on that phone to the bundle inside the APK by expo-updates itself.

Needs a new APK, not an update: a new native package or an Expo SDK upgrade, a new Android permission or prompt text, anything in app.json / app.config.js (name, icon, splash, plugins, Firebase). Raise version in app.json (for example 0.2.0 → 0.3.0) in the same change, build the release APK, install it on every phone. Publishing a JS update built against the new native code without raising version would send it to old APKs that lack that code, and they would crash.

Not set up: code signing of updates (expo-updates can verify a signature; the key would be one more secret to keep); iOS updates (no iPhone build yet); updates to the workflow's debug APK (a debug build never takes them). Changing to the company keystore later means one more reinstall — Android refuses an APK signed with a different key.

Release signing is not set up for the native app yet. Two ways, later: EAS Build (npx eas-cli build --platform android, credentials kept by Expo), or the same ANDROID_KEYSTORE_* secrets android-apk.yml uses, with a signingConfigs block added to the generated android/app/build.gradle through an Expo config plugin. The debug APK is for testing on a phone only.

Not done

  • Play Store listing. Needs the signed AAB (./gradlew bundleRelease), a developer account, the privacy policy URL (https://alhudatravels.in/privacy) and store assets. The Data safety form's account-deletion answer is in App Store (iPhone) §8: an in-app Request account deletion that the office completes within 30 days, and the web link https://alhudatravels.in/privacy#delete-account.
  • iOS. Capacitor can add it (npx cap add ios), but it needs a Mac with Xcode and an Apple developer account. The same is true of the native app (npx expo prebuild --platform ios).
  • Release signing for the native app (above).
  • App-links (opening alhudatravels.in links in the app) need assetlinks.json on the site; not set up.

The iOS app

The same Expo project (apps/mobile) builds the iPhone app: bundle id in.alhudatravels.app, the camera, photo-library, location and notification usage texts are in app.json, and eas.json carries three build profiles (development, preview, production).

What it needs, none of which the repository can supply:

Need Why Who
Apple Developer Program membership (₹8,000–9,000 a year) Signing, TestFlight, the App Store Owner
An Expo account signed in to EAS (npx eas-cli login) or a Mac with Xcode installed EAS builds in the cloud; Xcode builds on the Mac (the release Mac has only the command-line tools, not Xcode) Owner / IT
Push certificate (APNs key) uploaded to Firebase Notifications on iPhone go APNs → FCM; eas credentials creates the key once the Apple account exists IT

Build and distribute:

cd apps/mobile
npx eas-cli login                       # once
npx eas-cli build --platform ios --profile preview     # TestFlight-ready .ipa, signed by EAS
npx eas-cli submit --platform ios       # to TestFlight; testers install from the TestFlight app

A simulator build for a Mac that has Xcode: npx expo prebuild --platform ios && npx expo run:ios.

The App Store listing, enrolment, API key, review notes and privacy answers: App Store (iPhone).

Not done: no iOS build has been produced yet (no Apple account, no Xcode on the release Mac); the code is the same as the Android app and nothing in it is Android-only except the foreground-service notification for location sharing, which iOS replaces with its own "Always" location permission.