Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 77 additions & 10 deletions frontend/src/routes/auth.$provider.callback.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createFileRoute, useNavigate, Link } from "@tanstack/react-router";
import { useEffect, useState, useRef } from "react";
import { useEffect, useState, useRef, useCallback } from "react";
import { useOpenSecret } from "@opensecret/react";
import { AlertDestructive } from "@/components/AlertDestructive";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
Expand Down Expand Up @@ -28,12 +28,14 @@ function formatProviderName(provider: string): string {
function OAuthCallback() {
const [isProcessing, setIsProcessing] = useState(true);
const [error, setError] = useState<string | null>(null);
const [redirectFailed, setRedirectFailed] = useState(false);
const [deepLinkUrl, setDeepLinkUrl] = useState<string>("");
const navigate = useNavigate();
const { handleGitHubCallback, handleGoogleCallback, handleAppleCallback } = useOpenSecret();
const processedRef = useRef(false);

// Helper functions for the callback process
const handleSuccessfulAuth = () => {
const handleSuccessfulAuth = useCallback(() => {
// Check if this is a Tauri app auth flow (desktop or mobile)
const isTauriAuth = localStorage.getItem("redirect-to-native") === "true";

Expand All @@ -45,15 +47,35 @@ function OAuthCallback() {
const accessToken = localStorage.getItem("access_token") || "";
const refreshToken = localStorage.getItem("refresh_token");

let deepLinkUrl = `cloud.opensecret.maple://auth?access_token=${encodeURIComponent(accessToken)}`;
let deepLink = `cloud.opensecret.maple://auth?access_token=${encodeURIComponent(accessToken)}`;

if (refreshToken) {
deepLinkUrl += `&refresh_token=${encodeURIComponent(refreshToken)}`;
deepLink += `&refresh_token=${encodeURIComponent(refreshToken)}`;
}

setTimeout(() => {
window.location.href = deepLinkUrl;
}, 1000);
// Store the deep link for manual fallback
setDeepLinkUrl(deepLink);

// Set a timeout to detect if the redirect failed
// If the user is still on this page after 5 seconds, show error
const redirectTimer = setTimeout(() => {
setRedirectFailed(true);
setIsProcessing(false);
}, 5000);
Comment on lines +61 to +64
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: The redirectTimer is never cleaned up if the redirect succeeds and the page unloads. If the deep link works, the timer will still fire 5 seconds later (potentially in the background), calling state setters on an unmounted component.

Suggested change
const redirectTimer = setTimeout(() => {
setRedirectFailed(true);
setIsProcessing(false);
}, 5000);
// Set a timeout to detect if the redirect failed
// If the user is still on this page after 5 seconds, show error
const redirectTimer = setTimeout(() => {
if (document.visibilityState === 'visible') {
setRedirectFailed(true);
setIsProcessing(false);
}
}, 5000);
Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/src/routes/auth.$provider.callback.tsx
Line: 61:64

Comment:
**logic:** The `redirectTimer` is never cleaned up if the redirect succeeds and the page unloads. If the deep link works, the timer will still fire 5 seconds later (potentially in the background), calling state setters on an unmounted component.

```suggestion
      // Set a timeout to detect if the redirect failed
      // If the user is still on this page after 5 seconds, show error
      const redirectTimer = setTimeout(() => {
        if (document.visibilityState === 'visible') {
          setRedirectFailed(true);
          setIsProcessing(false);
        }
      }, 5000);
```

How can I resolve this? If you propose a fix, please make it concise.


// Attempt the redirect
try {
window.location.href = deepLink;

// If we're still here after a brief moment, the redirect may have failed
// The timer above will catch this
} catch (error) {
// Clear the timer and show error immediately if redirect throws
clearTimeout(redirectTimer);
setRedirectFailed(true);
setIsProcessing(false);
console.error("Failed to redirect to app:", error);
}

return;
}
Expand All @@ -72,7 +94,7 @@ function OAuthCallback() {
navigate({ to: "/" });
}
}, 2000);
};
}, [navigate, setDeepLinkUrl, setIsProcessing, setRedirectFailed]);

const handleAuthError = (error: unknown) => {
console.error(`Authentication callback error:`, error);
Expand Down Expand Up @@ -150,10 +172,55 @@ function OAuthCallback() {
};

processCallback();
}, [handleGitHubCallback, handleGoogleCallback, handleAppleCallback, navigate, provider]);
}, [
handleGitHubCallback,
handleGoogleCallback,
handleAppleCallback,
handleSuccessfulAuth,
navigate,
provider
]);

// If this is a Tauri app auth flow (desktop or mobile), show a different UI
if (localStorage.getItem("redirect-to-native") === "true") {
if (localStorage.getItem("redirect-to-native") === "true" || redirectFailed) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: This condition checks localStorage on every render. Since localStorage.removeItem("redirect-to-native") is called on line 43, this will be false after the first execution, causing the UI to switch from the Tauri success screen back to the web flow screen before the 5-second timer completes.

Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/src/routes/auth.$provider.callback.tsx
Line: 185:185

Comment:
**logic:** This condition checks `localStorage` on every render. Since `localStorage.removeItem("redirect-to-native")` is called on line 43, this will be `false` after the first execution, causing the UI to switch from the Tauri success screen back to the web flow screen before the 5-second timer completes.

How can I resolve this? If you propose a fix, please make it concise.

if (redirectFailed) {
return (
<Card className="max-w-md mx-auto mt-20">
<CardHeader>
<CardTitle>Redirect Failed</CardTitle>
</CardHeader>
<CardContent>
<AlertDestructive
title="Unable to redirect to app"
description="The automatic redirect to the Maple app failed. Please try one of the options below."
/>
<div className="mt-4 space-y-3">
<p className="text-sm text-muted-foreground">
If the Maple app is installed, click the button below to manually open it:
</p>
<Button
onClick={() => {
window.location.href = deepLinkUrl;
}}
className="w-full"
>
Open Maple App
</Button>
<p className="text-sm text-muted-foreground">
Or copy this link and paste it into your browser:
</p>
<div className="bg-muted p-2 rounded text-xs break-all font-mono">{deepLinkUrl}</div>
<div className="pt-2">
<Button variant="outline" asChild className="w-full">
<Link to="/login">Back to Login</Link>
</Button>
</div>
</div>
</CardContent>
</Card>
);
}

return (
<Card className="max-w-md mx-auto mt-20">
<CardHeader>
Expand Down
Loading