Files
picpeak/frontend/src/utils/inAppBrowser.ts
T
Paul Nothaft b1bfd4838e fix(gallery): unbreak password entry in Instagram in-app browser (#654)
Reporter @Duecki1 hit "Incorrect Password" on byte-correct input from
Instagram's iOS/Android IAB. Backend bcrypt compare is fine — the
frontend was handing it a mangled byte sequence because the password
Input lacked the autocaps/autocorrect/spellcheck/autocomplete defenses
Instagram's WKWebView keyboard bridge needs (the standard `type="password"`
WebKit defaults that suppress autocaps get overridden inside the IAB).

Three layers of defense:

1. **Explicit input attributes** on the gallery password field —
   `autoCapitalize="none"`, `autoCorrect="off"`, `spellCheck={false}`,
   `autoComplete="current-password"`. Stops iOS autocaps turning
   `wedding2026` into `Wedding2026`, stops predictive-text rewrites,
   nudges password managers to autofill the right credential rather
   than the IAB's stale saved-password store.

2. **Silent `.trim()` on submit** — Android Instagram IAB's predictive
   keyboard often appends a trailing space when the user taps the
   submit button. Event-gallery passwords don't legitimately carry
   leading/trailing whitespace (they're set by photographers, usually
   generated short strings), so trimming here is safe.

3. **Instagram IAB detection banner** — `frontend/src/utils/inAppBrowser.ts`
   detects the `Instagram` UA tag and surfaces a one-time advisory at
   the top of the password card with the right platform-specific
   "Open in external browser" instructions (⋯ menu copy for iOS,
   ⋮ for Android). Self-rescue path for users who hit it before we
   can close every keyboard mangling vector.

Scope is strictly Instagram per #654. Facebook IAB (`FBAV`/`FBAN`)
behaves identically and would benefit, but expanding the matcher is
a separate scope decision — the detector + i18n shape leaves room for
it without further refactor.

EN + DE i18n for the banner; 8 vitest cases on `detectInAppBrowser`
(iOS / Android Instagram UAs, plain Safari / Chrome / desktop UAs,
case-insensitive match, word-boundary defense against substring
collisions, SSR-safety when `navigator` is undefined). Lint + tsc
clean; pre-push Playwright smoke still expected green.

Closes #654.
2026-06-22 21:09:17 +02:00

52 lines
2.1 KiB
TypeScript

/**
* Detect when the page is loaded inside a known social-app in-app browser
* (#654). These WKWebView / WebView wrappers — Instagram's IAB especially —
* mangle password inputs in ways the host app can't override: keyboard
* autocapitalisation overrides, predictive-text-appended trailing spaces,
* stale saved-password autofill, smart-quote substitution. The result is
* "Incorrect Password" on a byte-correct user input, with no visible cause.
*
* The cheapest mitigation is to detect the IAB UA and surface a banner
* asking the user to open the link in their device's normal browser; the
* gallery password form behaves correctly once we're out of the IAB.
*
* Detection uses navigator.userAgent. UA spoofing is possible but irrelevant
* here — the banner is advisory; the legitimate IAB UA strings are stable.
*/
export type InAppBrowser = 'instagram';
export interface InAppBrowserDetection {
// The detected IAB family, or null when the UA doesn't match.
app: InAppBrowser | null;
// Best-effort guess at the host platform so we can show the right copy
// for the "open in browser" instructions (the menu lives in different
// places on iOS vs Android).
platform: 'ios' | 'android' | 'other';
}
/**
* Detect whether the current navigator.userAgent matches a known IAB the
* gallery password form has trouble with. SSR-safe (returns app: null
* when window is undefined).
*/
export function detectInAppBrowser(): InAppBrowserDetection {
if (typeof navigator === 'undefined' || typeof navigator.userAgent !== 'string') {
return { app: null, platform: 'other' };
}
const ua = navigator.userAgent;
let platform: InAppBrowserDetection['platform'] = 'other';
if (/iPhone|iPad|iPod/i.test(ua)) platform = 'ios';
else if (/Android/i.test(ua)) platform = 'android';
// Instagram's IAB tags its UA with `Instagram <version>` on both iOS and
// Android. Match case-insensitively so versioning quirks (`Instagram` vs
// `instagram`) don't slip past.
if (/\bInstagram\b/i.test(ua)) {
return { app: 'instagram', platform };
}
return { app: null, platform };
}