Skip to content

fix(certificates): keep portal navigations in the WebView; hide View in-app - #218

Merged
hatafatif merged 1 commit into
developfrom
bd-2676-build
Aug 13, 2026
Merged

fix(certificates): keep portal navigations in the WebView; hide View in-app#218
hatafatif merged 1 commit into
developfrom
bd-2676-build

Conversation

@hatafatif

Copy link
Copy Markdown
Collaborator

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:false is the server minting a fresh empty session for the cookieless request — not the teacher's session.

Fix: an allowNavigation entry derived from VITE_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: 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 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

Result
Buttons on a certificate row One — Download (View correctly hidden)
Tap Download Stays in the app, no Chrome, no 401
Certificate Renders in Android's PDF viewer
Bonus That viewer offers Print and Send file — serving the save-and-print case better than the silent download did

Screenshots of the 401-in-Chrome failure and the working render were reviewed with the operator.

⚠️ Shipping path — this is NOT OTA

capacitor.config.ts is a native-shell file; it compiles into the APK. Per niete-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 from VITE_API_BASE_URL, hostname-not-url, https-only, [] on absent/malformed, plus no-hardcoded-host and production-logging guards
  • tests/portal/bd-2676-certificate-view-and-download.test.js — View gated on !native, Download's conditional target

Full suite: 33 failed / 91 tests — identical to the develop baseline (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

…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
@hatafatif
hatafatif merged commit e24e65a into develop Aug 13, 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