feat(mobile): OAuth2 PKCE login flow and mobile shell (#486)#498
Merged
Conversation
…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>
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>
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.
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.
frappe.integrations.oauth2(authorize/get_token)ASWebAuthenticationSession(iOS) / Chrome Custom Tabs (Android) vianativescript-inappbrowser— the RFC 8252 pattern, and exactly what Mobile: Site management and OAuth2 PKCE login #486 asks for ("open the system browser")@nativescript/secure-storageAuthorization: Bearer <token>, refreshes silently before expiry, and retries once on 401mail.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 launchmail/api/mobile.py): guestget_client_iddiscovery endpoint +create_oauth_clientsetup (wired into Mail Settings → Mobile)__()pattern as the web appAuth flow
mail.api.mobile.get_client_idto validate the site and fetch the OAuth client ID + brandingASWebAuthenticationSession/ Chrome Custom Tabs) at Frappe's OAuth Authorization Code + PKCE authorize endpointcom.frappe.mail://oauth(a registered deep link);openAuthreturns the callback URL, the code is exchanged for access + refresh tokens, and they're stored per site in secure storagePlatform notes
@nativescript/secure-storagepulls 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 anApp_Resources/iOS/Podfilepost_installhook that bumps every pod's deployment target to 12.0 — keeping Keychain-backed storage intact.NativeScriptActivityis set tolaunchMode="singleTask"so the OAuth redirect deep link resumes the existing activity (onNewIntent) instead of spawning a blank duplicate (which otherwise forced an app restart).nativescript-inappbrowseris pinned to3.2.0— the3.3.0"latest" is a broken publish that ships type defs and native artifacts but no JavaScript.Acceptance criteria (#486)
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— passns build ios/ns build android— both succeedCloses #486
🤖 Generated with Claude Code