Skip to content

feat(mobile): OAuth2 PKCE login flow and mobile shell (#486)#498

Merged
krantheman merged 10 commits into
frappe:mobile-appfrom
krantheman:mobile/486-oauth-login
Jun 1, 2026
Merged

feat(mobile): OAuth2 PKCE login flow and mobile shell (#486)#498
krantheman merged 10 commits into
frappe:mobile-appfrom
krantheman:mobile/486-oauth-login

Conversation

@krantheman
Copy link
Copy Markdown
Member

@krantheman krantheman commented Jun 1, 2026

Summary

Implements the mobile authentication flow for #486 — from typing a server URL to being logged in with tokens stored securely on device — plus the NativeScript + Vue 3 app shell it runs in.

  • OAuth2 Authorization Code + PKCE client: code verifier/challenge (S256), token exchange & refresh against frappe.integrations.oauth2 (authorize / get_token)
  • System-browser login: the authorize step runs in ASWebAuthenticationSession (iOS) / Chrome Custom Tabs (Android) via nativescript-inappbrowser — the RFC 8252 pattern, and exactly what Mobile: Site management and OAuth2 PKCE login #486 asks for ("open the system browser")
  • Secure token storage per site in the platform Keychain (iOS) / Keystore (Android) via @nativescript/secure-storage
  • Session store + typed API client that injects a valid Authorization: Bearer <token>, refreshes silently before expiry, and retries once on 401
  • Site management UI: landing page validates a site via mail.api.mobile.get_client_id (fetches OAuth client ID + branding), lists saved sites, and hands off to login; signed-in app shell restores the active site on launch
  • Backend (mail/api/mobile.py): guest get_client_id discovery endpoint + create_oauth_client setup (wired into Mail Settings → Mobile)
  • Shared mobile translation helper so screens use the same __() pattern as the web app

Auth flow

  1. User enters a Mail site URL on the landing page
  2. App calls mail.api.mobile.get_client_id to validate the site and fetch the OAuth client ID + branding
  3. App opens the system browser (ASWebAuthenticationSession / Chrome Custom Tabs) at Frappe's OAuth Authorization Code + PKCE authorize endpoint
  4. The browser redirects back to com.frappe.mail://oauth (a registered deep link); openAuth returns the callback URL, the code is exchanged for access + refresh tokens, and they're stored per site in secure storage
  5. Authenticated API calls auto-refresh expired tokens and retry once on 401; logout clears tokens and returns to the landing screen

Platform notes

  • iOS build fix: @nativescript/secure-storage pulls in the SAMKeychain pod, which targets iOS 8.0. Xcode 16 removed the legacy ARC shim (libarclite) used below iOS 12, so the build failed to link. Fixed with an App_Resources/iOS/Podfile post_install hook that bumps every pod's deployment target to 12.0 — keeping Keychain-backed storage intact.
  • Android: NativeScriptActivity is set to launchMode="singleTask" so the OAuth redirect deep link resumes the existing activity (onNewIntent) instead of spawning a blank duplicate (which otherwise forced an app restart).
  • Plugin pin: nativescript-inappbrowser is pinned to 3.2.0 — the 3.3.0 "latest" is a broken publish that ships type defs and native artifacts but no JavaScript.

Acceptance criteria (#486)

  • User can type a server URL, see site branding, and tap login
  • OAuth2 PKCE flow completes (in the system browser) and tokens are stored in secure storage
  • User is returned to the app after browser login and lands on the app shell
  • Token refresh works silently in the background
  • Logging out clears tokens and returns to the landing screen
  • Multiple sites can be saved and switched between

Deviation from the issue text (intentional): branding (name/logo) is fetched and shown in the saved-sites list; there is no separate confirmation sheet before login — adding a site proceeds straight to the OAuth handoff.

Test plan

  • cd mobile && yarn typecheck + yarn lint — pass
  • ns build ios / ns build android — both succeed
  • Manual on-device/emulator verification (iOS + Android): add site → system-browser OAuth login → app shell → logout

Closes #486

🤖 Generated with Claude Code

krantheman and others added 4 commits June 1, 2026 14:22
…e#486)

Foundational auth modules for the mobile login flow:
- utils/pkce.ts: createPkcePair() — code_verifier/code_challenge (S256)
  + state, using platform CSPRNG (arc4random on iOS, SecureRandom on
  Android) and js-sha256
- utils/oauth.ts: buildAuthorizeUrl + form-encoded token exchange and
  refresh against frappe.integrations.oauth2 (authorize / get_token)
- utils/secureStorage.ts: per-site token load/save/clear in the platform
  keystore via @nativescript/secure-storage (replaces the scaffold's
  unencrypted ApplicationSettings)
- deps: @nativescript/secure-storage, js-sha256

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- authFlow.ts: loginWithOAuth() — PKCE → in-app WebView → state check →
  token exchange
- components/OAuthWebView.vue: WebView that intercepts the
  com.frappe.mail://oauth redirect (no native deep-link code)
- stores/session.ts: login / refresh / logout / getValidAccessToken,
  tokens persisted per site in the keystore
- utils/api.ts: inject a valid Bearer (refresh if expired), retry once on
  401; refresh failure clears the session
- oauth.ts: add RedirectResult type
- eslint: disable vue/v-on-event-hyphenation for mobile (NativeScript
  native events are camelCase, e.g. loadStarted)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e#486)

- LandingPage: add a server URL (validated via mail.api.mobile.get_client_id),
  list saved sites, tap to log in
- AppShell: post-login screen with log out
- App.vue switches landing↔shell on session.isLoggedIn; app.ts restores
  the active site's tokens on start

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@krantheman krantheman requested a review from s-aga-r as a code owner June 1, 2026 09:54
krantheman and others added 6 commits June 1, 2026 17:30
Wildcard module declarations (declare module '*.vue') must live in an
ambient non-module file. The @vue/runtime-core augmentation, however,
requires a module file (export {}) so TypeScript merges into the
existing type rather than replacing it.

Keeping both in shims-vue.d.ts caused one requirement to break the
other. Solution: keep shims-vue.d.ts ambient (no export {}), and move
the ComponentCustomProperties augmentation to shims-runtime-core.d.ts
which has export {}.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
js-sha256 requires Node.js 'crypto' and 'buffer' at the top of its
source, which webpack 5 won't polyfill for NativeScript. The PKCE
challenge is a single SHA-256 of a short string — inlining a compact
pure-JS implementation (no external deps, no Node APIs) is cleaner than
configuring polyfills.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Android 9+ blocks plain HTTP by default. Setting usesCleartextTraffic
allows the app to reach a local Frappe dev server over HTTP.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Drop the in-app WebView OAuth2 + PKCE flow in favour of a plain Frappe
framework login (POST /api/method/login). Removes oauth.ts, pkce.ts,
authFlow.ts, secureStorage.ts and OAuthWebView.vue.

Also removes the @nativescript/secure-storage dependency, whose
SAMKeychain pod targets iOS 8.0 and fails to build under Xcode 16
(missing libarclite). This unblocks the iOS build.

Session state now persists a simple logged-in flag per site via
ApplicationSettings; API calls rely on the native cookie jar instead of
Bearer tokens. SiteInfo is trimmed to { url, app_name? }.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reinstates the OAuth2 Authorization Code + PKCE flow required by frappe#486,
reverting the framework-login detour. Restores oauth.ts, pkce.ts,
authFlow.ts, secureStorage.ts and OAuthWebView.vue, the token-based
session store / API client, and the branded site-discovery LandingPage.

The original reason for dropping OAuth was an iOS build break: the
@nativescript/secure-storage pod (SAMKeychain) targets iOS 8.0, and
Xcode 16 removed the libarclite ARC shim used below iOS 12. Fix it
properly instead of removing secure storage — add an App_Resources
Podfile post_install hook that bumps every pod's deployment target to
12.0. iOS now builds (SAMKeychain compiles with deprecation warnings
only); tokens remain in the Keychain/Keystore as the issue requires.

App.vue uses imperative $navigateTo against an explicit Frame ref with a
deferred login/logout watch, avoiding the getExitTransition crash that
the v-if/v-else Frame children produced during modal dismissal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…bView

Replace the in-app WebView OAuth flow with the system browser
(ASWebAuthenticationSession on iOS, Chrome Custom Tabs on Android) via
nativescript-inappbrowser. authFlow.loginWithOAuth now calls
InAppBrowser.openAuth(authorizeUrl, REDIRECT_URI) and exchanges the
returned code for tokens.

This is the RFC 8252 recommended pattern for native apps and resolves a
string of in-WebView issues on Android: the custom com.frappe.mail://
redirect scheme couldn't be intercepted by the bare WebView
(ERR_UNKNOWN_URL_SCHEME), and presenting the WebView as a modal corrupted
frame navigation on iOS (post-login white screen).

Required changes:
- Android: NativeScriptActivity needs launchMode="singleTask" so the
  OAuth redirect deep link resumes the existing activity (onNewIntent)
  instead of spawning a blank duplicate (which forced an app restart).
- Pin nativescript-inappbrowser to 3.2.0 — the 3.3.0 "latest" is a broken
  publish that ships type defs and native artifacts but no JavaScript.

Removes the OAuthWebView component, the native WebViewClient interop, and
the modal machinery. Navigation is driven explicitly from the page
handlers; logout returns to the landing page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@krantheman krantheman merged commit 111e19f into frappe:mobile-app Jun 1, 2026
3 checks passed
@krantheman krantheman deleted the mobile/486-oauth-login branch June 1, 2026 19:37
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