From 6b3134f09f378a9fa52c91c7266b9552d82fb0be Mon Sep 17 00:00:00 2001 From: Anthony Ronning <101225832+AnthonyRonning@users.noreply.github.com> Date: Fri, 9 Oct 2026 04:17:57 +0000 Subject: [PATCH] Show progress while web OAuth sign-in is pending --- .../src/components/AppleAuthProvider.test.tsx | 77 +++++++++++++++++++ .../src/components/AppleAuthProvider.tsx | 21 ++++- .../frontend/src/routes/login.tsx | 38 ++++++++- .../frontend/src/routes/signup.tsx | 38 ++++++++- 4 files changed, 163 insertions(+), 11 deletions(-) diff --git a/apps/maple-research/frontend/src/components/AppleAuthProvider.test.tsx b/apps/maple-research/frontend/src/components/AppleAuthProvider.test.tsx index 691fec7f6..823249eed 100644 --- a/apps/maple-research/frontend/src/components/AppleAuthProvider.test.tsx +++ b/apps/maple-research/frontend/src/components/AppleAuthProvider.test.tsx @@ -237,6 +237,76 @@ describe("AppleAuthProvider", () => { return { completion, control }; } + function expectPending(pending: boolean): void { + const button = renderer!.root.findByType("button"); + expect(button.props.disabled).toBe(pending); + expect(button.props["aria-busy"]).toBe(pending); + expect(button.children.includes("Signing in…")).toBe(pending); + expect( + button.findAllByType("svg").some((icon) => icon.props.className.includes("animate-spin")) + ).toBe(pending); + } + + test("shows pending from initiation through the closed popup's callback and navigation", async () => { + let resolveInitiation!: (result: { auth_url: string; state: string }) => void; + let resolveCallback!: () => void; + initiateAppleAuth.mockImplementationOnce( + () => + new Promise((resolve) => { + resolveInitiation = resolve; + }) + ); + handleAppleCallback.mockImplementationOnce( + () => + new Promise((resolve) => { + resolveCallback = resolve; + }) + ); + await act(async () => { + renderer = create( + + ); + }); + expectPending(false); + + let completion!: Promise; + await act(async () => { + completion = renderer!.root.findByType("button").props.onClick(); + }); + expectPending(true); + expect(appleSignIn).not.toHaveBeenCalled(); + + await act(async () => { + resolveInitiation({ + auth_url: `https://appleid.apple.com/auth/authorize?client_id=cloud.opensecret.maple.services&nonce=${"11".repeat(32)}`, + state: "state-one" + }); + }); + expectPending(true); + expect(appleSignIn).toHaveBeenCalledTimes(1); + + await act(async () => { + signInControls.shift()!.resolve({ + authorization: { code: "callback-code", state: "state-one" } + }); + }); + expect(handleAppleCallback).toHaveBeenCalledTimes(1); + expectPending(true); + expect(redirectAfterLogin).not.toHaveBeenCalled(); + await act(async () => { + await renderer!.root.findByType("button").props.onClick(); + }); + expect(initiateAppleAuth).toHaveBeenCalledTimes(1); + + await act(async () => { + resolveCallback(); + await completion; + }); + expect(onSuccess).toHaveBeenCalledTimes(1); + expect(redirectAfterLogin).toHaveBeenCalledTimes(1); + expectPending(true); + }); + for (const clientId of [ "cloud.opensecret.maple.services", "cloud.opensecret.maple.dev.services" @@ -326,6 +396,7 @@ describe("AppleAuthProvider", () => { ).toEqual([]); const firstAttempt = await startAttempt(); + expectPending(true); await act(async () => { await renderer?.root.findByType("button").props.onClick(); }); @@ -352,8 +423,10 @@ describe("AppleAuthProvider", () => { expect(onError).toHaveBeenCalledTimes(0); expect(handleAppleCallback).toHaveBeenCalledTimes(0); + expectPending(false); const legacyCancellationAttempt = await startAttempt(); + expectPending(true); await act(async () => { legacyCancellationAttempt.control.reject({ error: "popup_closed_by_user" }); await legacyCancellationAttempt.completion; @@ -361,8 +434,10 @@ describe("AppleAuthProvider", () => { expect(onError).toHaveBeenCalledTimes(0); expect(handleAppleCallback).toHaveBeenCalledTimes(0); + expectPending(false); const failedAttempt = await startAttempt(); + expectPending(true); await act(async () => { failedAttempt.control.reject({ error: "authorization_failed" }); await failedAttempt.completion; @@ -371,6 +446,7 @@ describe("AppleAuthProvider", () => { expect(onError).toHaveBeenCalledTimes(1); expect(onError.mock.calls[0]?.[0]).toEqual(new Error("authorization_failed")); expect(handleAppleCallback).toHaveBeenCalledTimes(0); + expectPending(false); await act(async () => { renderer?.update( @@ -385,6 +461,7 @@ describe("AppleAuthProvider", () => { }); const retry = await startAttempt(); + expectPending(true); documentTarget.dispatchEvent( appleEvent("AppleIDSignInOnSuccess", { authorization: { code: "promise-code", state: "state-two" } diff --git a/apps/maple-research/frontend/src/components/AppleAuthProvider.tsx b/apps/maple-research/frontend/src/components/AppleAuthProvider.tsx index 47ca3b3f5..df0950c45 100644 --- a/apps/maple-research/frontend/src/components/AppleAuthProvider.tsx +++ b/apps/maple-research/frontend/src/components/AppleAuthProvider.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useRef, useState } from "react"; import { useOpenSecret } from "@mapleai/sdk"; +import { Loader2 } from "lucide-react"; import { Button, type ButtonProps } from "./ui/button"; import { Apple } from "./icons/Apple"; import { HostedNativeSignInConfirmation } from "./HostedNativeSignInConfirmation"; @@ -121,6 +122,7 @@ export function AppleAuthProvider({ const os = useOpenSecret(); const appleScriptLoaded = useRef(false); const isSignInPending = useRef(false); + const [isPending, setIsPending] = useState(false); const active = useRef(true); const ownedTarget = useRef(null); const [nativeConfirmation, setNativeConfirmation] = useState( @@ -217,6 +219,8 @@ export function AppleAuthProvider({ const handleAppleSignIn = async () => { if (isSignInPending.current || nativeConfirmation || !active.current) return; isSignInPending.current = true; + setIsPending(true); + let completed = false; const nativeFlow = isNativeOAuthRedirect(); const target = nativeFlow ? readTransportV2DesktopOAuth("apple") : null; ownedTarget.current = target; @@ -238,6 +242,7 @@ export function AppleAuthProvider({ } await completeAuthorization(authorization, nativeFlow, target); + completed = true; } catch (error) { if (!active.current || (target && !isCurrentDesktopOAuthTarget(target))) return; const signInError = getAppleAuthError(error); @@ -247,7 +252,11 @@ export function AppleAuthProvider({ onError?.(signInError); } } finally { - isSignInPending.current = false; + // Successful sign-in stays busy until navigation or account confirmation replaces it. + if (!completed) { + isSignInPending.current = false; + if (active.current) setIsPending(false); + } } }; @@ -269,9 +278,15 @@ export function AppleAuthProvider({ onClick={handleAppleSignIn} variant={buttonVariant} className={className || "w-full"} + disabled={isPending} + aria-busy={isPending} > - - {buttonLabel} + {isPending ? ( +