fix(android): OTA same-origin API + staging build type & workflow (bd-2554) - #184
Merged
Conversation
…-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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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, soisNativeApp()stays true →resolveApiBaseUrl()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:
The fix. The rule was never "native ⇒ absolute URL" — it's "no usable origin ⇒ absolute URL". A bundled app on
https://localhosthas 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
stagingbuild type (initWith buildTypes.release) +android-staging.ymlondevelop, so the artifact under test differs from the Play artifact in one variable: the backend.pk.edu.niete.debugpk.edu.niete.stagingpk.edu.nieteAll 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
So a staging build cannot replace the production app, and Play would reject it. The release key never enters a non-production workflow.
.aab, bd-2520 invariant intact.Still required before these workflows run
Repo variables:
NIETE_STAGING_API_BASE_URL(staging portal is live atportal-production-24e6.up.railway.app), optionalNIETE_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