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
751 const [dcrBusy, setDcrBusy] = useState(false);751 const [dcrBusy, setDcrBusy] = useState(false);
752 const [dcrFailed, setDcrFailed] = useState(false);752 const [dcrFailed, setDcrFailed] = useState(false);
753 const [oauthFallbackProbe, setOAuthFallbackProbe] = useState<DcrProbeResult | null>(null);753 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);
754 // FIX 3 escape hatch: when no registered app matched the integration's758 // FIX 3 escape hatch: when no registered app matched the integration's
755 // endpoints, the unmatched apps are collapsed behind an opt-in expander.759 // endpoints, the unmatched apps are collapsed behind an opt-in expander.
756 const [showOtherApps, setShowOtherApps] = useState(false);760 const [showOtherApps, setShowOtherApps] = useState(false);
@@ -817,6 +821,7 @@ function AddAccountModalView(props: AddAccountModalProps) { · 60 unmodified lines
817 setPickedApp(null);821 setPickedApp(null);
818 setOAuthFallbackProbe(null);822 setOAuthFallbackProbe(null);
819 setDcrFailed(false);823 setDcrFailed(false);
824 setDcrFallbackMessage(null);
820 }, [initialState, allMethods, defaultOwner, ownerOptions]);825 }, [initialState, allMethods, defaultOwner, ownerOptions]);
821 826
822 useEffect(() => {827 useEffect(() => {
@@ -1004,6 +1009,7 @@ function AddAccountModalView(props: AddAccountModalProps) { · 181 unmodified lines
1004 setOnePasswordItemId("");1009 setOnePasswordItemId("");
1005 setDcrFailed(false);1010 setDcrFailed(false);
1006 setOAuthFallbackProbe(null);1011 setOAuthFallbackProbe(null);
1012 setDcrFallbackMessage(null);
1007 };1013 };
1008 1014
1009 // A just-created custom method joins the in-session list and is auto-selected1015 // 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
1267 if (outcome.kind === "fallback") {1273 if (outcome.kind === "fallback") {
1268 setOAuthFallbackProbe("probe" in outcome ? outcome.probe : null);1274 setOAuthFallbackProbe("probe" in outcome ? outcome.probe : null);
1269 setDcrFailed(true);1275 setDcrFailed(true);
1270 toast.error(1276 // Surface the server's actionable rejection reason on the recovery view as
1271 ("message" in outcome ? outcome.message : undefined) ??1277 // an inline error card. Generic fallbacks (no message) fall through to the
1272 "Automatic setup unavailable. Register an app instead.",1278 // "register an app" empty state, which already guides the user.
1273 );1279 setDcrFallbackMessage("message" in outcome ? (outcome.message ?? null) : null);
1274 }1280 }
1275 };1281 };
1276 1282
@@ -1511,6 +1517,22 @@ function AddAccountModalView(props: AddAccountModalProps) { · 234 unmodified lines
1511 <p className="text-xs text-muted-foreground">Loading OAuth apps…</p>1517 <p className="text-xs text-muted-foreground">Loading OAuth apps…</p>
1512 ) : (1518 ) : (
1513 <div className="space-y-3">1519 <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}
1514 {/* No registered app matched the integration's endpoint:1536 {/* No registered app matched the integration's endpoint:
1515 empty state + a prominent register CTA, and an opt-in1537 empty state + a prominent register CTA, and an opt-in
1516 collapsed "use a different registered app" escape hatch. */}1538 collapsed "use a different registered app" escape hatch. */}