diff --git a/components/home/desktop-oauth/DesktopOAuthComplete.tsx b/components/home/desktop-oauth/DesktopOAuthComplete.tsx index 8659a7a1..62998f43 100644 --- a/components/home/desktop-oauth/DesktopOAuthComplete.tsx +++ b/components/home/desktop-oauth/DesktopOAuthComplete.tsx @@ -1,7 +1,8 @@ "use client"; import { useEffect, useState } from "react"; -import { useSearchParams } from "next/navigation"; +import { useSearchParams, useRouter } from "next/navigation"; +import { submitDesktopToken } from "@src/lib/utils/requests"; import ScriptioLogo from "@public/images/scriptio.svg"; import layout from "../../utils/Layout.module.css"; @@ -16,27 +17,33 @@ type Status = "working" | "done" | "error"; * After NextAuth has signed the user in (cookie set in this browser), we POST the * nonce to /api/desktop/token. The server reads the session, mints a NextAuth JWE, * and stows it under the nonce so the desktop poller can pick it up. + * + * The nonce comes from the URL for Google (callbackUrl cookie survives the redirect) + * or from sessionStorage for Apple (response_mode=form_post drops the cookie, so + * DesktopOAuthStart stored the nonce there before the handoff). If neither source + * has a nonce the visitor is a web user who signed in with Apple — send them to /projects. */ const DesktopOAuthComplete = () => { const searchParams = useSearchParams(); - const nonce = searchParams.get("nonce"); - const [status, setStatus] = useState(nonce ? "working" : "error"); + const router = useRouter(); + const urlNonce = searchParams.get("nonce"); + const [status, setStatus] = useState("working"); useEffect(() => { - if (!nonce) return; - (async () => { - try { - const res = await fetch("/api/desktop/token", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ nonce }), - }); - setStatus(res.ok ? "done" : "error"); - } catch { - setStatus("error"); - } - })(); - }, [nonce]); + const nonce = urlNonce ?? sessionStorage.getItem("desktop-oauth-nonce"); + + if (!nonce) { + // Web user signed in with Apple — no desktop bridge needed. + router.replace("/projects"); + return; + } + + sessionStorage.removeItem("desktop-oauth-nonce"); + + submitDesktopToken(nonce) + .then(res => setStatus(res.ok ? "done" : "error")) + .catch(() => setStatus("error")); + }, [urlNonce, router]); const message = status === "working" diff --git a/components/home/desktop-oauth/DesktopOAuthStart.tsx b/components/home/desktop-oauth/DesktopOAuthStart.tsx index c7ae4657..45705045 100644 --- a/components/home/desktop-oauth/DesktopOAuthStart.tsx +++ b/components/home/desktop-oauth/DesktopOAuthStart.tsx @@ -31,6 +31,9 @@ const DesktopOAuthStart = () => { // sessionToken.user.id !== getUserByAccount().id. Starting clean avoids that. (async () => { const callbackUrl = `/desktop-oauth/complete?nonce=${encodeURIComponent(nonce)}`; + // Persist the nonce in sessionStorage so /desktop-oauth/complete can recover + // it even when Apple's response_mode=form_post drops the callbackUrl cookie. + sessionStorage.setItem("desktop-oauth-nonce", nonce); await signOut({ redirect: false }); await signIn(provider, { callbackUrl }); })(); diff --git a/src/auth.ts b/src/auth.ts index bf0c5037..cf08a9d5 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -41,11 +41,13 @@ export const { handlers, auth, signIn, signOut } = NextAuth({ callbacks: { // Apple uses response_mode=form_post; the cross-site POST back from // appleid.apple.com drops the `callbackUrl` cookie (Auth.js promotes state/nonce - // to SameSite=None for form_post but not callbackUrl), so NextAuth otherwise falls - // back to baseUrl ("/") and the user lands on the homepage instead of where they - // started. Treat that fallback as "send them to the projects page." + // to SameSite=None for form_post but not callbackUrl), so NextAuth falls back to + // baseUrl. Send those cases to /desktop-oauth/complete, which recovers the nonce + // from sessionStorage (set by DesktopOAuthStart before the OAuth handoff) and + // finishes the desktop bridge. Web users who signed in with Apple and have no + // nonce are redirected to /projects from within that page. redirect: async ({ url, baseUrl }) => { - if (url === baseUrl || url === `${baseUrl}/`) return `${baseUrl}/projects`; + if (url === baseUrl || url === `${baseUrl}/`) return `${baseUrl}/desktop-oauth/complete`; if (url.startsWith("/")) return `${baseUrl}${url}`; try { if (new URL(url).origin === baseUrl) return url; diff --git a/src/lib/utils/requests.ts b/src/lib/utils/requests.ts index e1b59b34..48b1d9cd 100644 --- a/src/lib/utils/requests.ts +++ b/src/lib/utils/requests.ts @@ -151,6 +151,10 @@ export const requestMagicLink = (body: RequestMagicLinkBody) => { return request(`/api/auth/magic-link`, "POST", body); }; +export const submitDesktopToken = (nonce: string) => { + return request(`/api/desktop/token`, "POST", { nonce }); +}; + export const cancelStripeSubscription = async (): Promise => { const res = await request("/api/stripe/cancel", "POST"); return res.ok;