Add fallback copy/paste code flow for OAuth when deep link redirect fails#438
Add fallback copy/paste code flow for OAuth when deep link redirect fails#438devin-ai-integration[bot] wants to merge 11 commits intomasterfrom
Conversation
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Deploying maple with
|
| Latest commit: |
fe77fc8
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://16af247e.maple-ca8.pages.dev |
| Branch Preview URL: | https://devin-1772041598-oauth-copy.maple-ca8.pages.dev |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
📝 WalkthroughWalkthroughAdds a desktop (Tauri) OAuth copy-paste fallback: the callback encodes tokens into a base64 auth code and exposes a copy UI; login/signup gain a "paste-code" flow that decodes the code, stores tokens, and reloads the app when automatic native redirect fails. Changes
Sequence DiagramsequenceDiagram
actor User
participant Desktop as Desktop (Tauri)
participant OAuth as OAuth Provider
participant Callback as Callback Component
participant UI as Login/Signup UI
User->>Desktop: Initiate OAuth (GitHub/Google/Apple)
Desktop->>OAuth: Open external browser
OAuth->>OAuth: User authenticates
OAuth->>Callback: Redirect with tokens
Callback->>Callback: Encode tokens -> base64 authCode
Callback->>UI: Show copy UI / navigate to paste-code
UI->>User: Display authCode (Copy button after delay)
User->>UI: Paste code into paste-code input
UI->>UI: Decode authCode, store tokens
UI->>UI: Reload application
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@TestFlight build |
|
🚀 TestFlight deployment triggered! Check the Actions tab for progress. |
|
❌ TestFlight deployment failed. Check the workflow logs for details. |
…ails When desktop/mobile users authenticate via OAuth, the deep link redirect back to the app sometimes fails (e.g., opens in Safari instead). This adds a fallback mechanism: - Web side (auth callback): After 3 seconds, shows a 'Copy Code' button with a base64-encoded auth token that users can copy to clipboard - Desktop side (login/signup): Adds a 'Paste Login Code' option in Tauri environments where users can paste the copied code to complete auth The code is a base64-encoded JSON containing access_token and refresh_token. Co-Authored-By: unknown <>
The refresh_token may be an empty string when generated by the callback page (defaults to '' when absent from localStorage). The previous check treated empty string as falsy, rejecting valid codes. Now only access_token is required, and refresh_token is conditionally stored. Co-Authored-By: unknown <>
When pasting a login code with an empty refresh_token, the old refresh_token in localStorage was not being cleared, which could cause cross-session token mismatches on refresh. Co-Authored-By: unknown <>
…sktop - After clicking GitHub/Google/Apple OAuth on desktop, the app now automatically shows the paste-code screen with a spinner and 'Complete your [Provider] login in the browser' messaging - After 3 seconds, reveals the paste code input with 'Having trouble?' - Updated web callback fallback text to 'Trouble opening the app?' - Applied consistently to both login.tsx and signup.tsx Co-Authored-By: unknown <>
…h handlers Co-Authored-By: unknown <>
The paste-code screen is now only accessible via the auto-navigation after clicking an OAuth button on desktop. This simplifies the UI by removing the standalone buttons while keeping the fallback flow intact. Co-Authored-By: unknown <>
Use isTauriDesktop() instead of isTauri() for the paste-code screen auto-navigation after OAuth button click. The browser opening still happens on all Tauri platforms, but the paste-code fallback UI is only shown on desktop where deep link redirects are less reliable. Co-Authored-By: unknown <>
…te-code screen Co-Authored-By: unknown <>
Co-Authored-By: unknown <>
80c56fa to
0b2bd27
Compare
|
@TestFlight build |
|
🚀 TestFlight deployment triggered! Check the Actions tab for progress. |
|
✅ TestFlight deployment completed successfully! |
…roid) Co-Authored-By: unknown <>
…n Maple button and copy-code fallback Co-Authored-By: unknown <>
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
frontend/src/routes/auth.$provider.callback.tsx (1)
67-73:⚠️ Potential issue | 🟠 MajorSecurity risk: clipboard code still contains raw bearer tokens.
The fallback code is still just base64-encoded
{ access_token, refresh_token }, which is trivially reversible and replayable if copied/leaked. This was already flagged and remains unresolved.
🧹 Nitpick comments (1)
frontend/src/routes/login.tsx (1)
124-165: ExtracthandlePasteCodeinto a shared helper/hook used by login and signup.This logic is effectively duplicated in
frontend/src/routes/signup.tsx(decode, validate, persist tokens, clear billing token, redirect), which will drift over time.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/routes/login.tsx` around lines 124 - 165, Duplicate logic in handlePasteCode (in login.tsx and signup.tsx) should be extracted to a shared helper/hook (e.g., processAuthPaste or useAuthPaste) that decodes base64, JSON-parses the payload, validates access_token, persists access_token/refresh_token to localStorage, clears billing token via getBillingService().clearToken(), and performs the redirect via window.location.href = "/"; implement the helper to return a Promise that throws errors with the same messages so callers (handlePasteCode in login.tsx and the equivalent in signup.tsx) can reuse identical try/catch UI handling (setIsLoading, setError) while importing and invoking the shared function instead of duplicating decode/parse/persist/clear/redirect logic; ensure the helper exports a typed function and that callers pass pasteCodeValue and handle result/errors unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/routes/auth`.$provider.callback.tsx:
- Around line 64-73: Currently the code always builds a fallback auth code using
accessToken and refreshToken and calls setAuthCode(btoa(codePayload)) even when
access_token is missing; change the logic in this component (the block that
defines accessToken, refreshToken, codePayload and calls setAuthCode) to fail
fast when accessToken is falsy: if accessToken is missing, do not construct the
codePayload or call setAuthCode, instead set an error state or trigger the
error/redirect flow (e.g., call the existing error handler or navigate to an
error page) so the UI doesn't present a broken "successful" deep link; keep
refreshToken optional but ensure accessToken is required before creating the
deep link.
In `@frontend/src/routes/login.tsx`:
- Around line 149-150: The current hardcoded post-login reload
(window.location.href = "/") drops any active route intent (query params like
selected_plan or next), so change the reload to preserve the current URL +
query/hash (or use the router navigate) instead of forcing "/" — e.g., in the
login completion handler in frontend/src/routes/login.tsx (the block that sets
window.location.href = "/"), replace it with a redirect that retains
window.location.pathname + window.location.search + window.location.hash (or
call the router's navigate to the current location) so pasted-code flows like
selected_plan and next=/redeem?code=... are preserved.
In `@frontend/src/routes/signup.tsx`:
- Around line 160-161: Replace the unconditional reload (window.location.href =
"/") so the redirect preserves selected_plan and any redeem continuation
context; instead of hard-navigating to "/", read and reuse the current
location.search (or app routing state) when redirecting—e.g. navigate to "/"
plus window.location.search or use your router's navigate/replace with the
existing query params or saved continuation state; update the code that sets
window.location.href to construct the URL from window.location.pathname/ search
or call the router's navigation method so selected_plan and redeem context are
retained after paste-code signup completion.
---
Nitpick comments:
In `@frontend/src/routes/login.tsx`:
- Around line 124-165: Duplicate logic in handlePasteCode (in login.tsx and
signup.tsx) should be extracted to a shared helper/hook (e.g., processAuthPaste
or useAuthPaste) that decodes base64, JSON-parses the payload, validates
access_token, persists access_token/refresh_token to localStorage, clears
billing token via getBillingService().clearToken(), and performs the redirect
via window.location.href = "/"; implement the helper to return a Promise that
throws errors with the same messages so callers (handlePasteCode in login.tsx
and the equivalent in signup.tsx) can reuse identical try/catch UI handling
(setIsLoading, setError) while importing and invoking the shared function
instead of duplicating decode/parse/persist/clear/redirect logic; ensure the
helper exports a typed function and that callers pass pasteCodeValue and handle
result/errors unchanged.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
frontend/src/routes/auth.$provider.callback.tsxfrontend/src/routes/login.tsxfrontend/src/routes/signup.tsx
| const accessToken = localStorage.getItem("access_token") || ""; | ||
| const refreshToken = localStorage.getItem("refresh_token"); | ||
|
|
||
| // Generate the fallback auth code for copy/paste flow | ||
| const codePayload = JSON.stringify({ | ||
| access_token: accessToken, | ||
| refresh_token: refreshToken || "" | ||
| }); | ||
| setAuthCode(btoa(codePayload)); | ||
|
|
There was a problem hiding this comment.
Fail fast when OAuth tokens are missing before generating redirect/code.
If access_token is absent, this path still generates a deep link and copy code, leading to a broken "successful" flow.
🔧 Suggested guard
const accessToken = localStorage.getItem("access_token") || "";
const refreshToken = localStorage.getItem("refresh_token");
+ if (!accessToken) {
+ throw new Error("Missing access token in OAuth callback");
+ }
// Generate the fallback auth code for copy/paste flow
const codePayload = JSON.stringify({📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const accessToken = localStorage.getItem("access_token") || ""; | |
| const refreshToken = localStorage.getItem("refresh_token"); | |
| // Generate the fallback auth code for copy/paste flow | |
| const codePayload = JSON.stringify({ | |
| access_token: accessToken, | |
| refresh_token: refreshToken || "" | |
| }); | |
| setAuthCode(btoa(codePayload)); | |
| const accessToken = localStorage.getItem("access_token") || ""; | |
| const refreshToken = localStorage.getItem("refresh_token"); | |
| if (!accessToken) { | |
| throw new Error("Missing access token in OAuth callback"); | |
| } | |
| // Generate the fallback auth code for copy/paste flow | |
| const codePayload = JSON.stringify({ | |
| access_token: accessToken, | |
| refresh_token: refreshToken || "" | |
| }); | |
| setAuthCode(btoa(codePayload)); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/routes/auth`.$provider.callback.tsx around lines 64 - 73,
Currently the code always builds a fallback auth code using accessToken and
refreshToken and calls setAuthCode(btoa(codePayload)) even when access_token is
missing; change the logic in this component (the block that defines accessToken,
refreshToken, codePayload and calls setAuthCode) to fail fast when accessToken
is falsy: if accessToken is missing, do not construct the codePayload or call
setAuthCode, instead set an error state or trigger the error/redirect flow
(e.g., call the existing error handler or navigate to an error page) so the UI
doesn't present a broken "successful" deep link; keep refreshToken optional but
ensure accessToken is required before creating the deep link.
| // Reload the app to pick up the new tokens | ||
| window.location.href = "/"; |
There was a problem hiding this comment.
Preserve route intent instead of always redirecting to / after paste-code login.
Hardcoding / drops active flows like selected_plan and next === "/redeem" with code.
🔧 Suggested redirect preservation
- // Reload the app to pick up the new tokens
- window.location.href = "/";
+ // Reload the app to pick up the new tokens while preserving intent
+ const redirectTarget = selected_plan
+ ? `/pricing?selected_plan=${encodeURIComponent(selected_plan)}`
+ : next === "/redeem" && code
+ ? `/redeem?code=${encodeURIComponent(code)}`
+ : next || "/";
+ window.location.href = redirectTarget;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/routes/login.tsx` around lines 149 - 150, The current hardcoded
post-login reload (window.location.href = "/") drops any active route intent
(query params like selected_plan or next), so change the reload to preserve the
current URL + query/hash (or use the router navigate) instead of forcing "/" —
e.g., in the login completion handler in frontend/src/routes/login.tsx (the
block that sets window.location.href = "/"), replace it with a redirect that
retains window.location.pathname + window.location.search + window.location.hash
(or call the router's navigate to the current location) so pasted-code flows
like selected_plan and next=/redeem?code=... are preserved.
| // Reload the app to pick up the new tokens | ||
| window.location.href = "/"; |
There was a problem hiding this comment.
Avoid unconditional / reload after paste-code signup completion.
This drops selected_plan and redeem continuation context, unlike other auth completion paths.
🔧 Suggested redirect preservation
- // Reload the app to pick up the new tokens
- window.location.href = "/";
+ // Reload the app to pick up the new tokens while preserving intent
+ const redirectTarget = selected_plan
+ ? `/pricing?selected_plan=${encodeURIComponent(selected_plan)}`
+ : next === "/redeem" && code
+ ? `/redeem?code=${encodeURIComponent(code)}`
+ : next || "/";
+ window.location.href = redirectTarget;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Reload the app to pick up the new tokens | |
| window.location.href = "/"; | |
| // Reload the app to pick up the new tokens while preserving intent | |
| const redirectTarget = selected_plan | |
| ? `/pricing?selected_plan=${encodeURIComponent(selected_plan)}` | |
| : next === "/redeem" && code | |
| ? `/redeem?code=${encodeURIComponent(code)}` | |
| : next || "/"; | |
| window.location.href = redirectTarget; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/routes/signup.tsx` around lines 160 - 161, Replace the
unconditional reload (window.location.href = "/") so the redirect preserves
selected_plan and any redeem continuation context; instead of hard-navigating to
"/", read and reuse the current location.search (or app routing state) when
redirecting—e.g. navigate to "/" plus window.location.search or use your
router's navigate/replace with the existing query params or saved continuation
state; update the code that sets window.location.href to construct the URL from
window.location.pathname/ search or call the router's navigation method so
selected_plan and redeem context are retained after paste-code signup
completion.
Add fallback copy/paste code flow for OAuth deep link failures
Summary
When Tauri users (desktop, iOS, or Android) authenticate via OAuth (Google/GitHub/Apple), the app redirects to a browser, completes auth, then attempts a deep link redirect (
cloud.opensecret.maple://auth?...) back to the app. Sometimes this deep link fails — e.g., Safari doesn't redirect back, leaving the user stuck in the browser.This PR adds a manual fallback for all Tauri platforms (macOS, Windows, Linux, iOS, Android):
Web side (
auth.$provider.callback.tsx): After attempting the deep link redirect, an "Open Maple" button is shown immediately. After 3 seconds, a "Copy Code" fallback UI also appears below. The code is a base64-encoded JSON containingaccess_tokenandrefresh_token. The user can copy this to their clipboard.App side (
login.tsxandsignup.tsx): When the user clicks an OAuth button (GitHub/Google/Apple) on any Tauri platform, the app automatically navigates to the paste-code screen while the browser opens. This screen initially shows a spinner with "Logging in with [Provider] — Complete your login in the browser that just opened." After 3 seconds, it reveals the paste code input with "Having trouble? Paste the login code from the browser below."Updates since last revision
isTauriDesktop()back toisTauri(). iOS and Android users now also get the paste-code fallback flow, since deep link redirects can fail on mobile too (the user reported OAuth failures on iOS). Previously this was desktop-only.auth.$provider.callback.tsxto combine master's "Open Maple" button (for iOS Safari where auto-redirect is blocked) with the copy-code fallback. The callback page now shows: Open Maple button immediately → copy-code fallback after 3 seconds.Prior updates
setError(null)before navigating to the paste-code screen in all 6 OAuth handlers.Desktop app testing (Linux Tauri build)
The EGL fix on master enabled testing the actual Tauri desktop app on a headless Linux VM:
Review & Testing Checklist for Human
Notes
isTauri()(all Tauri platforms: macOS, Windows, Linux, iOS, Android) instead ofisTauriDesktop(). This was changed because the user reported OAuth failures on iOS. The browser-opening flow also usesisTauri()so it works on all platforms.oauthProviderstate) to show contextual messaging.handlePasteCodeis duplicated identically inlogin.tsxandsignup.tsx. Consider extracting into a shared hook. Not blocking but a maintenance concern.Link to Devin run: https://app.devin.ai/sessions/ec5b4b00cff6495a9f6f9ed5ab61664f
Requested by: @AnthonyRonning
Summary by CodeRabbit