Skip to content

fix(android): OTA same-origin API + staging build type & workflow (bd-2554) - #184

Merged
hatafatif merged 1 commit into
developfrom
bd-2554-23087
Aug 12, 2026
Merged

fix(android): OTA same-origin API + staging build type & workflow (bd-2554)#184
hatafatif merged 1 commit into
developfrom
bd-2554-23087

Conversation

@hatafatif

Copy link
Copy Markdown
Collaborator

Follow-up to #182. Two gaps — the first is a bug that would have bricked the app.

1. OTA would have white-screened every app 🚨

Remote-first OTA silently changed which bundle runs inside the native shell. It used to always be the app-mode bundle (absolute VITE_API_BASE_URL). Under OTA the WebView fetches whatever the portal serves the web — where that value is undefined. But Capacitor still injects its global, so isNativeApp() stays trueresolveApiBaseUrl() took the isNative branch, found no absolute URL, and threw at first render.

That's bd-2551's white screen through a new door, and worse: a Play rollback cannot fix a bundle served from the web. Verified by direct invocation against the pre-fix code:

Error: Native builds need an absolute API base URL...

The fix. The rule was never "native ⇒ absolute URL" — it's "no usable origin ⇒ absolute URL". A bundled app on https://localhost has no server (must stay loud, and does). A page served by a real https host can use the relative /api/portal, because there the page origin is the API's origin. isServedByRealHost() draws that line and fails closed on anything unrecognised.

Side effect: under OTA the API is same-origin, so the CORS allowlist stops being load-bearing for app traffic. Kept — the bundled fallback still needs it.

2. The staging build

A debug APK uses the debug build type — no minify/proguard/shrinking — so it never exercises the path that ships. Added a staging build type (initWith buildTypes.release) + android-staging.yml on develop, so the artifact under test differs from the Play artifact in one variable: the backend.

Build Package Backend Signed with
Debug pk.edu.niete.debug staging debug key
Staging pk.edu.niete.staging staging debug key
Release pk.edu.niete production NIETE release key

All three install side by side on one handset.

matchingFallbacks = ['release'] is required, not decoration — the Capacitor library modules publish only debug/release variants, so without it the build dies with "No matching variant of project :capacitor-android". Found by running gradle, not by reading it.

Verification — on the built APK, not the gradle source

package: name='pk.edu.niete.staging'  label='NIETE Staging'
Signer #1 DN: C=US, O=Android, CN=Android Debug
Signer #1 SHA-256: 493f04ed...   (NIETE release key: daa4a5fb... ✗ no match)

So a staging build cannot replace the production app, and Play would reject it. The release key never enters a non-production workflow.

  • Release build reverified unaffected: one versioned .aab, bd-2520 invariant intact.
  • Red-first: 2 failing for the OTA throw + 6 for the staging build type → 248 passing across 20 portal suites.

Still required before these workflows run

Repo variables: NIETE_STAGING_API_BASE_URL (staging portal is live at portal-production-24e6.up.railway.app), optional NIETE_PROD_API_BASE_URL (enables the staging-must-not-target-prod guard). Release secrets from #182 unchanged; the Play publisher service account still doesn't exist.

Closes: bd-2554

🤖 Generated with Claude Code

…-2554)

Two gaps in the bd-2553 Android infrastructure. The first is a bug that
would have bricked the app; the second is the staging build.

1. OTA WOULD HAVE WHITE-SCREENED EVERY APP.

Remote-first OTA silently changed which bundle runs inside the native
shell. It used to always be the app-mode bundle, carrying an absolute
VITE_API_BASE_URL. Under OTA the WebView fetches whatever the portal
serves the WEB — a bundle where that value is undefined. But Capacitor
still injects its global, so isNativeApp() stays TRUE, so
resolveApiBaseUrl() took the isNative branch, found no absolute URL, and
threw at first render before React mounted.

That is bd-2551's white screen arriving through a new door, and worse:
a Play rollback cannot fix a bundle served from the web. Verified by
direct invocation against the pre-fix code.

The rule was never "native => absolute URL"; it is "no usable origin =>
absolute URL". A bundled app sits on https://localhost where nothing is
listening — that must stay loud, and it does. A page SERVED BY a real
https host can use the relative /api/portal, because there the page
origin IS the API's origin. isServedByRealHost() draws that line and
fails closed on anything it does not recognise; runtime.ts threads
window.location.origin through. A side effect worth noting: under OTA the
API is same-origin, so the CORS allowlist stops being load-bearing for
app traffic (kept, since the bundled fallback still needs it).

2. NO STAGING BUILD.

A debug APK is built from a PR branch with the debug build type — no
minify, no proguard, no shrinking — so it never exercises the path that
ships. Added a `staging` build type (initWith buildTypes.release) and
android-staging.yml on develop, so the artifact under test differs from
the Play artifact in ONE variable: the backend.

`matchingFallbacks = ['release']` is required, not decoration: the
Capacitor library modules publish only debug/release variants, so without
it the build dies with "No matching variant of project
:capacitor-android". Found by running gradle, not by reading it.

Both safety properties verified on the BUILT APK rather than the gradle
source: package pk.edu.niete.staging (cannot replace the production app),
label "NIETE Staging", and signer CN=Android Debug (493f04ed...) — NOT
the inherited NIETE release key (daa4a5fb...), so Play would reject it.
The release key is never given to a non-production workflow. Release
build reverified unaffected: one versioned .aab, bd-2520 invariant intact.

Tests red-first: 2 failing for the OTA throw and 6 for the staging build
type before implementation; 248 passing across 20 portal suites after.

Closes: bd-2554
@hatafatif
hatafatif merged commit 6564369 into develop Aug 12, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant