1import { CloudAuthBoundary } from "../../cloud/CloudAuthBoundary";
2import { activateCloudAuthentication } from "../../cloud/activation";
3import { useAuth, useClerk } from "@clerk/react";
4import { readConnectAuthorizeRequest } from "@t3tools/shared/connectAuth";
5import { useCallback, useEffect, useRef, useState } from "react";
6
7import {
8 buildConnectCliClerkAuthorizeUrl,
9 connectCliSignInRedirectUrl,
10} from "../../cloud/connectCliAuth";
11import { isElectron } from "../../env";
12import { AuthSurfaceShell } from "../auth/AuthSurfaceShell";
13import { resolveClerkSignInProps } from "../clerk/authRedirect";
14import { Button } from "../ui/button";
15
16function ConnectCliAuthMessage({
17 eyebrow,
18 title,
19 description,
20}: {
21 readonly eyebrow?: string;
22 readonly title: string;
23 readonly description: string;
24}) {
25 return (
26 <>
27 {eyebrow ? (
28 <p className="text-3xs font-semibold tracking-widest text-primary uppercase">{eyebrow}</p>
29 ) : null}
30 <h1 className="mt-2 text-2xl font-semibold tracking-tight sm:text-3xl">{title}</h1>
31 <p className="mt-2 text-sm leading-relaxed text-muted-foreground">{description}</p>
32 </>
33 );
34}
35
36const invalidLinkMessage = {
37 eyebrow: "Authorization request",
38 title: "This connect link is incomplete",
39 description:
40 "The link is missing its authorization request. Re-run `t3 connect` in your terminal and open the freshly printed URL.",
41} as const;
42
43/**
44 * /connect: the URL the CLI prints for the loopback flow. Waits for a Clerk
45 * session, then forwards the CLI's PKCE request to Clerk's authorize endpoint
46 * with the loopback redirect URI so the code returns straight to the waiting
47 * CLI. Headless hosts use Clerk's device authorization page instead.
48 */
49function ConfiguredConnectCliAuthorizeSurface() {
50 const [request] = useState(() => readConnectAuthorizeRequest(new URL(window.location.href)));
51 const clerk = useClerk();
52 const { isLoaded, isSignedIn } = useAuth();
53 const signInOpened = useRef(false);
54 const redirecting = useRef(false);
55
56 const openSignIn = useCallback(() => {
57 if (!request) {
58 return;
59 }
60 clerk.openSignIn(
61 resolveClerkSignInProps(
62 connectCliSignInRedirectUrl(request, window.location.href),
63 isElectron,
64 ),
65 );
66 }, [clerk, request]);
67
68 useEffect(() => {
69 if (!request || !isLoaded || redirecting.current) {
70 return;
71 }
72 if (!isSignedIn) {
73 if (!signInOpened.current) {
74 signInOpened.current = true;
75 openSignIn();
76 }
77 return;
78 }
79 const authorizeUrl = buildConnectCliClerkAuthorizeUrl(request);
80 if (!authorizeUrl) {
81 return;
82 }
83 redirecting.current = true;
84 window.location.assign(authorizeUrl);
85 }, [isLoaded, isSignedIn, openSignIn, request]);
86
87 if (!request) {
88 return (
89 <AuthSurfaceShell>
90 <ConnectCliAuthMessage {...invalidLinkMessage} />
91 </AuthSurfaceShell>
92 );
93 }
94
95 return (
96 <AuthSurfaceShell>
97 <ConnectCliAuthMessage
98 eyebrow="Browser authorization"
99 title="Connecting your terminal"
100 description={
101 isSignedIn
102 ? "Redirecting to authorize T3 Connect for your CLI…"
103 : "Sign in to continue authorizing T3 Connect for your CLI."
104 }
105 />
106 {isLoaded && !isSignedIn ? (
107 <div className="mt-6">
108 <Button type="button" onClick={openSignIn}>
109 Sign in
110 </Button>
111 </div>
112 ) : null}
113 </AuthSurfaceShell>
114 );
115}
116
117export function ConnectCliAuthorizeSurface() {
118 useEffect(() => activateCloudAuthentication(), []);
119 return (
120 <CloudAuthBoundary
121 fallback={
122 <AuthSurfaceShell>
123 <p role="status">Loading sign-in…</p>
124 </AuthSurfaceShell>
125 }
126 >
127 <ConfiguredConnectCliAuthorizeSurface />
128 </CloudAuthBoundary>
129 );
130}