This document covers shipping new versions of Zingo to both stores, on both production and beta channels.
Zingo ships as two parallel apps from this single repo:
| Channel | iOS bundle ID | Android applicationId | Display name | App Store / Play presence |
|---|---|---|---|---|
| Production | org.ZingoLabs.Zingo |
org.ZingoLabs.Zingo |
"Zingo" | App Store / Play Production |
| Beta | org.ZingoLabs.Zingo.Beta |
org.ZingoLabs.Zingo.Beta |
"Zingo Beta" | TestFlight External / Play Open Testing |
Both apps share the same JS bundle (app/, components/, app/translations/),
the same Rust libs (libuniffi_zingo.so, Zingolib.xcframework), the same
keystores and certificates. They differ only in bundle ID, display name, and
app icon (Beta has a red BETA band). The channel is detected at runtime from
the native binary — there is no JS-side toggle.
Use release-prep to bump version/build numbers for one channel without
touching the other. The script is idempotent: running it with the current
values produces no diff.
yarn release:prod:prep <version> <build> # e.g. 2.0.19 308
yarn release:beta:prep <version> <build>What each subcommand touches:
| File | prod:prep |
beta:prep |
|---|---|---|
package.json version |
✓ | — |
ios/Zingo.xcodeproj/project.pbxproj (Debug + Release) |
✓ | — |
ios/Zingo.xcodeproj/project.pbxproj (Debug-Beta + Release-Beta) |
— | ✓ |
android/app/build.gradle.kts defaultConfig |
✓ | — |
android/app/build.gradle.kts flavor beta |
— | ✓ |
Prod and beta versionCodes/build numbers evolve independently. Same versionName is fine across channels.
After the bump, release-prep prints the suggested commit / push / tag
commands to copy-paste. Pushing the tag triggers the Android Release CI
workflow described below.
Pushing a release tag publishes a set of Android APKs as a GitHub Release. This is independent from the store uploads — it exists for sideloading, internal QA distribution, and reproducible per-commit archives.
| Channel | Tag format | GitHub Release |
|---|---|---|
| prod | zingo-<version>-<build> |
latest release, prerelease=false |
| beta | zingo-beta-<version>-<build> |
new release, prerelease=true |
The .github/workflows/android-release.yaml workflow:
-
Builds the rust native libs (4 ABIs in parallel) and the uniffi kotlin bindings from source on the tagged commit. No
actions/cacheis read or written for these outputs — all cross-job hand-off is viaupload-artifact, which is run-scoped. The maven dependency cache is used in read-only mode (artifacts are content-addressed and signed). -
Assembles
assembleProdReleaseorassembleBetaReleasewith-PsplitApk=true -PincludeUniversalApk=true. -
Publishes 5 APKs to the release:
<prefix>-armeabi-v7a-<version>-<build>.apk(32-bit ARM)<prefix>-arm64-v8a-<version>-<build>.apk(64-bit ARM, most modern phones)<prefix>-x86-<version>-<build>.apk(32-bit x86, emulators)<prefix>-x86_64-<version>-<build>.apk(64-bit x86, emulators)<prefix>-universal-<version>-<build>.apk(single APK with all 4 ABIs)
where
<prefix>iszingoorzingo-beta.
These APKs are signed with debug.keystore.
yarn release:prod:prep <ver> <build>and commit the diff.- Open
ios/Zingo.xcworkspacein Xcode. - Select scheme Zingo, destination Any iOS Device (arm64).
- Product → Archive.
- In the Organizer that opens: Distribute App → App Store Connect → Upload. Signing is Automatic — your account needs to be a member of the team that owns the App Store Connect record.
- Wait ~10-20 min for "build processed" email from Apple.
- App Store Connect → app "Zingo" → fill What's New for the version and submit for App Store Review.
yarn release:prod:prep <ver> <build>(use the same numbers as the matching iOS bump for consistency, even though they're independent under the hood).- Android Studio → Build → Generate Signed App Bundle / APK → AAB.
- Point the wizard at the release keystore and enter its passwords (see First-time dev setup below for where they live).
- Build variant: prodRelease.
- Output AAB lands under
android/app/prod/release/. - Play Console → app "Zingo" → Production → Create new release → upload AAB → fill release notes → Review release → Roll out.
CLI equivalents (without local signing config in place, gradle falls back to the debug keystore — fine for local smoke testing, not for store upload):
yarn build:release # universal APK
yarn build:release:split # per-ABI APKs
cd android && ./gradlew bundleProdRelease # AAByarn release:beta:prep <ver> <build>and commit.- Xcode scheme Zingo Beta → Product → Archive → Distribute App → App Store Connect → Upload.
- After "build processed", in App Store Connect → app "Zingo Beta" →
TestFlight:
- Click the new build → Manage → mark Missing Compliance (encryption
declaration carries over from
Info.plist, just acknowledge it once per build). - External Testing → Zingo Beta Public group → Add Build → select the uploaded build.
- Fill Beta App Review info (description, what to test, "No login required" notes) → Submit for Review.
- Click the new build → Manage → mark Missing Compliance (encryption
declaration carries over from
- Apple's review of the first build of each major version takes 24-48h. Subsequent builds of the same major usually auto-approve.
- After approval, toggle on the Public Link in the External group settings.
The URL is
testflight.apple.com/join/XXXXXXXX— share with testers.
yarn release:beta:prep <ver> <build>and commit.- Android Studio → Build → Generate Signed App Bundle / APK → AAB → variant betaRelease.
- AAB at
android/app/beta/release/app-beta-release.aab. - Play Console → app "Zingo Beta" → Testing → Open testing → Create new release → upload AAB.
- Fill Release notes (EN at minimum) → Save → Review release → Roll out.
- First release of an Open testing track needs Google's content review (typically 1-3 days). After approval, share the opt-in URL from the Open testing → Testers tab.
CLI equivalents:
yarn build:beta:release # universal APK
yarn build:beta:release:split # per-ABI APKs
yarn build:beta:release:aab # AAB (what Play wants)- Get the release keystore and its credentials from a teammate. Both the
keystore file and the properties file are gitignored — drop them under
android/at the paths gradle expects (seeandroid/app/build.gradle.ktsfor the exact filenames). - Without them,
gradle assembleProdReleaseandassembleBetaReleasefall back todebug.keystore— the resulting AAB is not Play-uploadable but is usable for local install / smoke testing.
Sign into the team's Apple ID in Xcode. Automatic Signing provisions both
org.ZingoLabs.Zingo and org.ZingoLabs.Zingo.Beta on the first Archive of
each variant.
Debug,Release→ prodDebug-Beta,Release-Beta→ beta- Per-config overrides:
PRODUCT_BUNDLE_IDENTIFIER,INFOPLIST_KEY_CFBundleDisplayName,ASSETCATALOG_COMPILER_APPICON_NAME. release-prepdistinguishes channels usingASSETCATALOG_COMPILER_APPICON_NAMEas the anchor (prod =AppIcon, beta ="AppIcon-Beta").- Two shared schemes:
Zingo.xcscheme,Zingo Beta.xcscheme.
- Flavors
prodandbetaon dimensionchannel. beta:applicationIdSuffix = ".Beta",versionCodeoverride,resValue("string", "app_name", "Zingo Beta").- Beta resource overlay:
android/app/src/beta/res/mipmap-*/(icons with BETA band). - Hardcoded
app_nameremoved fromstrings.xml— provided per-flavor viaresValue.
app/utils/zingoVersion.ts derives the displayed version from
react-native-device-info at runtime. The displayed string is always
consistent with the binary the user installed, regardless of which channel
the shared JS bundle was prepped against.
- Bundle ID case sensitivity: both iOS and Android use
.Beta(capital B). TheapplicationIdSuffixinbuild.gradle.ktsis intentionally capital to match what's registered in App Store Connect and Play Console. - AAB / APK never committed: gitignored (
*.aab,*.apk,android/app/beta/release/,android/app/release/). - UniFFI bindings refresh:
rust/lib/src/uniffi/zingo/zingo.ktis the source of truth;yarn rust:androidcopies it intoandroid/app/build/generated/source/uniffi/{debug,release}/.... If yougradle cleanwithout re-runningyarn rust:android, you'll seeUnresolved reference 'initLogging'errors at Kotlin compile — re-runyarn rust:androidto refresh.