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.
Unified · Split
| @@ -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's | 758 | // 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-selected | 1015 | // 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'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-in | 1537 | 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. */} |