History

Keep the connection form visible during sign-in

Rhys Sullivan committed 1 month ago

Commit 2fbc1fb1 parent 1b19b373 files · +46 −54Browse files

Unified · Split

3 files
  1. .changeset/connection-oauth-cancel.md +1 −1
  2. e2e/scenarios/connection-setup-ux.test.ts +22 −0
  3. packages/react/src/components/add-account-modal.tsx +23 −53
.changeset/connection-oauth-cancel.md+1−1
@@ -2,4 +2,4 @@ · 1 unmodified line
22 "executor": patch
33 ---
44
5−Show a clear, cancellable sign-in state and prevent cancelled OAuth requests from disrupting a retry.
5+Let users cancel sign-in from the connection form and prevent cancelled OAuth requests from disrupting a retry.
e2e/scenarios/connection-setup-ux.test.ts+22−0
@@ -122,6 +122,11 @@ scenario( · 121 unmodified lines
122122 await step("Sign in with a provider account without naming the connection", async () => {
123123 await visit(page, `/integrations/${slug}?addAccount=1`);
124124 await page.getByRole("tab", { name: "OAuth2", exact: true }).click();
125+ // Attached previews can contain other compatible apps. Use this
126+ // scenario's provider instance for the full callback and token exchange.
127+ await page
128+ .getByRole("radio", { name: new RegExp(slug.replace("setup-", ""), "i") })
129+ .check();
125130 const opened = page.waitForEvent("popup");
126131 await page.getByRole("button", { name: "Connect with OAuth", exact: true }).click();
127132 const popup = await opened;
@@ -299,6 +304,11 @@ scenario( · 171 unmodified lines
299304 await step("Start provider sign-in without entering a name", async () => {
300305 await visit(page, `/integrations/${slug}?addAccount=1`);
301306 await page.getByRole("tab", { name: "OAuth2", exact: true }).click();
307+ // Attached previews can contain other compatible apps. Use this
308+ // scenario's provider instance for the full callback and token exchange.
309+ await page
310+ .getByRole("radio", { name: new RegExp(slug.replace("setup-", ""), "i") })
311+ .check();
302312 const opened = page.waitForEvent("popup");
303313 await page.getByRole("button", { name: "Connect with OAuth", exact: true }).click();
304314 const popup = await opened;
@@ -313,6 +323,11 @@ scenario( · 8 unmodified lines
313323 ).toBe(true);
314324 expect(await cancel.isEnabled()).toBe(true);
315325 await page.getByText("Continue in the sign-in window", { exact: true }).waitFor();
326+ expect(
327+ await page.getByRole("tab", { name: "OAuth2", exact: true }).isVisible(),
328+ "the authentication form stays visible during sign-in",
329+ ).toBe(true);
330+ expect(await page.getByRole("textbox", { name: /Display name/ }).isVisible()).toBe(true);
316331 expect(
317332 await page.getByRole("button", { name: "Connecting…", exact: true }).count(),
318333 "waiting for provider consent must not leave a dead Connecting button",
@@ -371,10 +386,17 @@ scenario( · 52 unmodified lines
371386 try {
372387 await step("Cancel while the authorization response is still in flight", async () => {
373388 await visit(page, `/integrations/${slug}?addAccount=1`);
389+ await page
390+ .getByRole("radio", { name: new RegExp(slug.replace("setup-", ""), "i") })
391+ .check();
374392 const opened = page.waitForEvent("popup");
375393 await page.getByRole("button", { name: "Connect with OAuth", exact: true }).click();
376394 const popup = await opened;
377395 const oldState = await started.promise;
396+ expect(
397+ await page.getByRole("textbox", { name: /Display name/ }).isVisible(),
398+ "the connection form stays visible while sign-in is being prepared",
399+ ).toBe(true);
378400 await page.getByRole("button", { name: "Cancel sign-in", exact: true }).click();
379401 await expect.poll(() => popup.isClosed()).toBe(true);
380402 const cancelled = page.waitForResponse((response) =>
packages/react/src/components/add-account-modal.tsx+23−53
@@ -100,15 +100,7 @@ import { · 99 unmodified lines
100100 DropdownMenuItem,
101101 DropdownMenuTrigger,
102102 } from "./dropdown-menu";
103−import {
104− ChevronDown,
105− EyeIcon,
106− EyeOffIcon,
107− LoaderCircleIcon,
108− PanelsTopLeftIcon,
109− PlusIcon,
110− XIcon,
111−} from "lucide-react";
103+import { ChevronDown, EyeIcon, EyeOffIcon, PlusIcon, XIcon } from "lucide-react";
112104 import {
113105 Dialog,
114106 DialogContent,
@@ -2749,49 +2741,7 @@ function AddAccountModalView(props: AddAccountModalProps) { · 2634 unmodified lines
27492741 : "sm:max-w-xl",
27502742 )}
27512743 >
2752− {signInPending ? (
2753− <>
2754− <DialogHeader>
2755− <DialogTitle>Add connection · {integrationName}</DialogTitle>
2756− </DialogHeader>
2757− <div role="status" className="flex flex-col items-center px-4 py-8 text-center">
2758− <div className="mb-5 flex size-12 items-center justify-center rounded-xl border border-border/70 bg-muted/30 text-foreground shadow-sm">
2759− {oauthPopup.phase === "authorizing" ? (
2760− <PanelsTopLeftIcon aria-hidden="true" className="size-5" />
2761− ) : (
2762− <LoaderCircleIcon
2763− aria-hidden="true"
2764− className="size-5 animate-spin motion-reduce:animate-none"
2765− />
2766− )}
2767− </div>
2768− <p className="text-sm font-medium text-foreground">
2769− {oauthPopup.phase === "authorizing"
2770− ? "Continue in the sign-in window"
2771− : oauthPopup.phase === "saving"
2772− ? "Finishing connection"
2773− : "Preparing sign-in"}
2774− </p>
2775− <DialogDescription className="mt-2 max-w-xs text-sm leading-relaxed">
2776− {oauthPopup.phase === "authorizing"
2777− ? "If the window closed or sign-in stalled, cancel and try again."
2778− : oauthPopup.phase === "saving"
2779− ? "Sign-in complete. Updating your connections."
2780− : "The sign-in window will be ready shortly. You can cancel at any time."}
2781− </DialogDescription>
2782− </div>
2783− <DialogFooter className="border-t border-border/60 pt-4 sm:justify-between">
2784− <Button type="button" variant="ghost" onClick={close}>
2785− Close
2786− </Button>
2787− {oauthPopup.phase !== "saving" ? (
2788− <Button type="button" variant="outline" onClick={cancelSignIn}>
2789− Cancel sign-in
2790− </Button>
2791− ) : null}
2792− </DialogFooter>
2793− </>
2794− ) : addingMethod && createCustomMethod ? (
2744+ {addingMethod && createCustomMethod ? (
27952745 <>
27962746 <DialogHeader className="border-b border-border/60 px-5 py-4">
27972747 <DialogTitle className="text-base">Add authentication method</DialogTitle>
@@ -3442,10 +3392,20 @@ function AddAccountModalView(props: AddAccountModalProps) { · 644 unmodified lines
34423392 </p>
34433393 ) : null}
34443394 <DialogFooter>
3395+ {signInPending ? (
3396+ <p role="status" className="flex-1 self-center text-xs text-muted-foreground">
3397+ {oauthPopup.phase === "authorizing"
3398+ ? "Continue in the sign-in window"
3399+ : oauthPopup.phase === "saving"
3400+ ? "Finishing connection…"
3401+ : "Preparing sign-in…"}
3402+ </p>
3403+ ) : null}
34453404 <Button type="button" variant="ghost" onClick={close} disabled={submitting || ccBusy}>
34463405 {isOAuth ? "Close" : "Cancel"}
34473406 </Button>
34483407 {/* Footer action, in precedence order:
3408+ - pending sign-in: cancel, or wait for an authorized connection to save;
34493409 - transparent CIMD (no app registration): create/reuse a public
34503410 metadata-document client and start OAuth;
34513411 - transparent DCR (no picker): a single Connect that runs
@@ -3453,7 +3413,17 @@ function AddAccountModalView(props: AddAccountModalProps) { · 1 unmodified line
34533413 - registering a BYO app: the form owns its own submit, no footer;
34543414 - picked BYO OAuth app: Connect with OAuth / Connect (client creds);
34553415 - credential/no-auth method: Add connection. */}
3456− {cimdActive ? (
3416+ {signInPending ? (
3417+ oauthPopup.phase === "saving" ? (
3418+ <Button type="button" loading>
3419+ Finishing…
3420+ </Button>
3421+ ) : (
3422+ <Button type="button" variant="outline" onClick={cancelSignIn}>
3423+ Cancel sign-in
3424+ </Button>
3425+ )
3426+ ) : cimdActive ? (
34573427 <Button
34583428 type="button"
34593429 onClick={() => void handleCimdConnect()}