Add loader on OAuth2 consent page submit

Track a pending allow/deny action with Button loading spinners so
users cannot double-submit while the approveConsent mutation runs or
while the browser waits on the OAuth callback redirect.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
This commit is contained in:
Cursor Agent
2026-07-02 15:03:34 +00:00
parent c27b7de1ba
commit 41d8f945c0

View File

@@ -159,14 +159,14 @@ export default function ConsentPage(props: {
const { __ } = useTranslate(); const { __ } = useTranslate();
const { toast } = useToast(); const { toast } = useToast();
const [deviceResult, setDeviceResult] = useState<"authorized" | "denied" | null>(null); const [deviceResult, setDeviceResult] = useState<"authorized" | "denied" | null>(null);
const [pendingAction, setPendingAction] = useState<"allow" | "deny" | null>(null);
const data = usePreloadedQuery<ConsentPageQuery>(consentPageQuery, props.queryRef); const data = usePreloadedQuery<ConsentPageQuery>(consentPageQuery, props.queryRef);
usePageTitle(__("Authorize Application")); usePageTitle(__("Authorize Application"));
const { node: consent } = data; const { node: consent } = data;
const [approveConsent, isInFlight] const [approveConsent] = useMutation<ConsentPageMutation>(approveConsentMutation);
= useMutation<ConsentPageMutation>(approveConsentMutation);
const { oidcScopes, apiScopes } = useMemo( const { oidcScopes, apiScopes } = useMemo(
() => partitionScopes(consent.scopes ?? []), () => partitionScopes(consent.scopes ?? []),
@@ -180,7 +180,9 @@ export default function ConsentPage(props: {
const handleAction = useCallback( const handleAction = useCallback(
(approved: boolean) => { (approved: boolean) => {
if (!consent.id) return; if (!consent.id || pendingAction !== null) return;
setPendingAction(approved ? "allow" : "deny");
approveConsent({ approveConsent({
variables: { variables: {
@@ -191,6 +193,7 @@ export default function ConsentPage(props: {
}, },
onCompleted: (response, errors) => { onCompleted: (response, errors) => {
if (errors) { if (errors) {
setPendingAction(null);
toast({ toast({
title: __("Authorization failed"), title: __("Authorization failed"),
description: formatError( description: formatError(
@@ -203,6 +206,7 @@ export default function ConsentPage(props: {
} }
if (!response.approveConsent) { if (!response.approveConsent) {
setPendingAction(null);
toast({ toast({
title: __("Authorization failed"), title: __("Authorization failed"),
description: __("Something went wrong. Please try again."), description: __("Something went wrong. Please try again."),
@@ -221,6 +225,7 @@ export default function ConsentPage(props: {
} }
}, },
onError: (err) => { onError: (err) => {
setPendingAction(null);
toast({ toast({
title: __("Error"), title: __("Error"),
description: description:
@@ -230,7 +235,7 @@ export default function ConsentPage(props: {
}, },
}); });
}, },
[consent, approveConsent, __, toast], [consent, approveConsent, __, toast, pendingAction],
); );
if (!consent.application || !consent.scopes) { if (!consent.application || !consent.scopes) {
@@ -310,17 +315,19 @@ export default function ConsentPage(props: {
<Button <Button
variant="secondary" variant="secondary"
className="flex-1 h-10" className="flex-1 h-10"
disabled={isInFlight} disabled={pendingAction !== null}
loading={pendingAction === "deny"}
onClick={() => handleAction(false)} onClick={() => handleAction(false)}
> >
{__("Deny")} {__("Deny")}
</Button> </Button>
<Button <Button
className="flex-1 h-10" className="flex-1 h-10"
disabled={isInFlight} disabled={pendingAction !== null}
loading={pendingAction === "allow"}
onClick={() => handleAction(true)} onClick={() => handleAction(true)}
> >
{isInFlight ? __("Authorizing...") : __("Allow")} {__("Allow")}
</Button> </Button>
</div> </div>