fix(certificates): keep portal navigations in the WebView; hide View in-app - #218
Merged
Conversation
…in-app Follow-up to the certificate view/download work, from testing on a real handset (RMX2061, Android 10). Two findings, both of which contradicted the earlier diagnosis. 1. THE ACTUAL CAUSE OF THE BROWSER EJECTION WAS NOT target="_blank". A bundled app runs on https://localhost, so every link to the portal API's own origin is a CROSS-ORIGIN navigation — and Capacitor hands those to the system browser by default, with or without a target attribute. Chrome holds none of the WebView's session cookies, so the request arrived unauthenticated and the portal answered 401 "Not authenticated. Please log in." (hasCookieHeader:false, cookieHeaderLength:0; hasSession:true with hasPortalUserId:false is the server minting a fresh empty session for the cookieless request, not the teacher's). Fixed with an allowNavigation entry DERIVED from VITE_API_BASE_URL, so a staging build allows staging and a production build allows production and neither can drift from the host actually being called. Returns [] when unset/unparseable rather than throwing — this runs at native build time and the value is legitimately absent in a web build. Download also loses target="_blank" in the native shell for the same reason. Web keeps it: the url returns a file, so a new tab preserves the teacher's place in the SPA. 2. VIEW AND DOWNLOAD ARE INDISTINGUISHABLE IN THE APP, so View is now web-only. Android's WebView ships no PDF viewer, so Content-Disposition: inline arrives somewhere that cannot render it and falls back to the download manager — the same visible outcome as attachment. Two buttons doing one thing imply a choice that does not exist. Gated on the existing isNativeApp() helper (the native shell, not screen size: a mobile browser renders PDFs fine). Delete the branch when the app gets a real viewer. Verified on the handset: one Download button, stays in the app, and the certificate opens in Android's PDF viewer — which also offers Print and Send file, serving the save-and-print case better than a silent download did.⚠️ capacitor.config.ts is a NATIVE-SHELL file. This reaches users only through a Play release, NOT via OTA or a Railway deploy. versionCode 1212. Diagnostic-only settings used to find this (hardcoded allowlist, loggingBehavior:'debug') are reverted and pinned by tests. Refs: bd-2676
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 #215, from testing on a real handset (RMX2061, Android 10). Both of my earlier conclusions were wrong, and the handset is what showed it.
1. The browser ejection was never
target="_blank"#215 claimed
target="_blank"caused the Capacitor WebView to hand off to Chrome. It doesn't.A bundled app runs on
https://localhost. Every link to the portal API's own origin is therefore a cross-origin navigation, and Capacitor hands those to the system browser with or without a target attribute. Chrome carries none of the WebView's session cookies, so the request arrived unauthenticated:{"success":false,"error":"Not authenticated. Please log in.","debug": {"hasSession":true,"hasPortalUserId":false, "hasCookieHeader":false,"cookieHeaderLength":0, "path":"/training/certificates/NIETE-20260802-HYZMH9/download"}}hasSession:true+hasPortalUserId:falseis the server minting a fresh empty session for the cookieless request — not the teacher's session.Fix: an
allowNavigationentry derived fromVITE_API_BASE_URL, so a staging build allows staging and a production build allows production, and the allowlist can never drift from the host actually being called. Returns[]when unset/unparseable rather than throwing — it runs at native build time and the value is legitimately absent in a web build.Download also loses
target="_blank"in the native shell for the same reason. Web keeps it: the url returns a file, so a new tab preserves the teacher's place in the SPA.2. View and Download are indistinguishable in the app
Android's WebView ships no PDF viewer.
Content-Disposition: inlinearrives somewhere that cannot render it and falls back to the download manager — the same visible outcome asattachment. Two buttons doing one thing imply a choice that isn't there.View is now web-only, gated on the existing
isNativeApp()helper — the native shell, not screen size (a mobile browser renders PDFs fine; it's the WebView that can't). Delete the branch when the app gets a real viewer.Verified on the handset
Screenshots of the 401-in-Chrome failure and the working render were reviewed with the operator.
capacitor.config.tsis a native-shell file; it compiles into the APK. Perniete-release§2 this reaches users only through a Play release — a Railway deploy or OTA will not carry it. The bot/dashboard/portal-src half of #215 still ships normally.versionCode 1212 (live is 1208; 1209–1211 were local diagnostic builds that never left this machine).
Tests
Red-first. The diagnostic-only settings used to find this (hardcoded allowlist,
loggingBehavior: 'debug') are reverted, and pinned by tests so they can't creep back:tests/portal/bd-2676-allow-navigation.test.js— derivation fromVITE_API_BASE_URL, hostname-not-url, https-only,[]on absent/malformed, plus no-hardcoded-host and production-logging guardstests/portal/bd-2676-certificate-view-and-download.test.js— View gated on!native, Download's conditional targetFull suite: 33 failed / 91 tests — identical to the
developbaseline (red for unrelated pre-existing reasons), 3054 passing. Zero new failures, verified by diff against a measured baseline.Refs: bd-2676
🤖 Generated with Claude Code