History

Show the DCR rejection reason as an inline card, not a toast

When transparent Dynamic Client Registration is rejected with an actionable reason (e.g. a server that refuses our redirect URI), surface the message as an inline error card on the bring-your-own-app recovery view instead of a transient toast, so the reason stays visible while the user registers an app.

Rhys Sullivan committed 4 months ago

Commit a414b711 parent d5ba68f1 file · +26 −4Browse files

Unified · Split

packages/react/src/components/add-account-modal.tsx+26−4
@@ -751,6 +751,10 @@ function AddAccountModalView(props: AddAccountModalProps) { · 750 unmodified lines
751751 const [dcrBusy, setDcrBusy] = useState(false);
752752 const [dcrFailed, setDcrFailed] = useState(false);
753753 const [oauthFallbackProbe, setOAuthFallbackProbe] = useState<DcrProbeResult | null>(null);
754+ // When transparent DCR is rejected with an actionable reason (e.g. the server
755+ // refuses our redirect URI), surface it as an inline error card on the
756+ // bring-your-own-app recovery view instead of a transient toast.
757+ const [dcrFallbackMessage, setDcrFallbackMessage] = useState<string | null>(null);
754758 // FIX 3 escape hatch: when no registered app matched the integration's
755759 // endpoints, the unmatched apps are collapsed behind an opt-in expander.
756760 const [showOtherApps, setShowOtherApps] = useState(false);
@@ -817,6 +821,7 @@ function AddAccountModalView(props: AddAccountModalProps) { · 60 unmodified lines
817821 setPickedApp(null);
818822 setOAuthFallbackProbe(null);
819823 setDcrFailed(false);
824+ setDcrFallbackMessage(null);
820825 }, [initialState, allMethods, defaultOwner, ownerOptions]);
821826
822827 useEffect(() => {
@@ -1004,6 +1009,7 @@ function AddAccountModalView(props: AddAccountModalProps) { · 181 unmodified lines
10041009 setOnePasswordItemId("");
10051010 setDcrFailed(false);
10061011 setOAuthFallbackProbe(null);
1012+ setDcrFallbackMessage(null);
10071013 };
10081014
10091015 // A just-created custom method joins the in-session list and is auto-selected
@@ -1267,10 +1273,10 @@ function AddAccountModalView(props: AddAccountModalProps) { · 257 unmodified lines
12671273 if (outcome.kind === "fallback") {
12681274 setOAuthFallbackProbe("probe" in outcome ? outcome.probe : null);
12691275 setDcrFailed(true);
1270− toast.error(
1271− ("message" in outcome ? outcome.message : undefined) ??
1272− "Automatic setup unavailable. Register an app instead.",
1273− );
1276+ // Surface the server's actionable rejection reason on the recovery view as
1277+ // an inline error card. Generic fallbacks (no message) fall through to the
1278+ // "register an app" empty state, which already guides the user.
1279+ setDcrFallbackMessage("message" in outcome ? (outcome.message ?? null) : null);
12741280 }
12751281 };
12761282
@@ -1511,6 +1517,22 @@ function AddAccountModalView(props: AddAccountModalProps) { · 234 unmodified lines
15111517 <p className="text-xs text-muted-foreground">Loading OAuth apps…</p>
15121518 ) : (
15131519 <div className="space-y-3">
1520+ {dcrFallbackMessage ? (
1521+ <div
1522+ role="alert"
1523+ className="space-y-1 rounded-lg border border-destructive/40 bg-destructive/5 px-3 py-3"
1524+ >
1525+ <p className="text-sm font-medium text-destructive">
1526+ Couldn&apos;t set up {integrationName} automatically
1527+ </p>
1528+ <p className="text-xs text-muted-foreground">
1529+ {dcrFallbackMessage}
1530+ </p>
1531+ <p className="text-xs text-muted-foreground">
1532+ Register an app below to connect.
1533+ </p>
1534+ </div>
1535+ ) : null}
15141536 {/* No registered app matched the integration's endpoint:
15151537 empty state + a prominent register CTA, and an opt-in
15161538 collapsed "use a different registered app" escape hatch. */}