1import { beginHostedSignIn } from "../../connection/hostedAuth";
2import { Alert, AlertDescription } from "../ui/alert";
3import type { AuthSessionState } from "@t3tools/contracts";
4import { squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime";
5import React, { startTransition, useEffect, useRef, useState, useCallback } from "react";
6
7import { APP_DISPLAY_NAME } from "../../branding";
8import { connectPairing } from "../../connection/onboarding";
9import {
10 peekPairingTokenFromUrl,
11 stripPairingTokenFromUrl,
12 submitServerAuthCredential,
13} from "../../environments/primary";
14import { readHostedPairingRequest } from "../../hostedPairing";
15import { Button } from "../ui/button";
16import { Input } from "../ui/input";
17import { StandalonePage, StandalonePageHeader } from "../ui/standalone-page";
18import { useAtomCommand } from "../../state/use-atom-command";
19
20export function PairingPendingSurface() {
21 return (
22 <StandalonePage tone="pairing">
23 <StandalonePageHeader
24 eyebrow={APP_DISPLAY_NAME}
25 title="Pairing with this environment"
26 description="Validating the pairing link and preparing your session."
27 />
28 </StandalonePage>
29 );
30}
31
32export function PairingRouteSurface(props: Parameters<typeof LegacyPairingRouteSurface>[0]) {
33 const [enabled, setEnabled] = useState<boolean | null>(null);
34 const [error, setError] = useState<string | null>(() =>
35 new URLSearchParams(window.location.search).has("signInError")
36 ? "Sign-in did not finish. Try again."
37 : null,
38 );
39 const attemptedSignIn = useRef(false);
40 useEffect(() => {
41 let stopped = false;
42 void fetch("/api/auth/oidc/config", { credentials: "same-origin", redirect: "error" })
43 .then(async (response) => response.ok && (await response.json()).enabled === true)
44 .then((value) => {
45 if (!stopped) setEnabled(value);
46 })
47 .catch(() => {
48 if (!stopped) setEnabled(false);
49 });
50 return () => {
51 stopped = true;
52 };
53 }, []);
54 useEffect(() => {
55 if (!enabled || error || attemptedSignIn.current || peekPairingTokenFromUrl()) return;
56 attemptedSignIn.current = true;
57 void beginHostedSignIn(window.location.origin).catch(() =>
58 setError("Couldn't start sign-in. Try again."),
59 );
60 }, [enabled, error]);
61 if (enabled === false || peekPairingTokenFromUrl())
62 return <LegacyPairingRouteSurface {...props} />;
63 return (
64 <StandalonePage tone="pairing">
65 <StandalonePageHeader
66 eyebrow={APP_DISPLAY_NAME}
67 title="Sign in"
68 description="Sign in to connect your devices and sync your chats."
69 />
70 {enabled === null ? (
71 <p role="status">Connecting…</p>
72 ) : (
73 <Button
74 onClick={() => {
75 void beginHostedSignIn(window.location.origin).catch(() =>
76 setError("Couldn't start sign-in. Try again."),
77 );
78 }}
79 >
80 Sign in
81 </Button>
82 )}
83 {error ? <p role="alert">{error}</p> : null}
84 </StandalonePage>
85 );
86}
87
88function LegacyPairingRouteSurface({
89 auth,
90 initialErrorMessage,
91 onAuthenticated,
92}: {
93 auth: AuthSessionState["auth"];
94 initialErrorMessage?: string;
95 onAuthenticated: () => void;
96}) {
97 const autoPairTokenRef = useRef<string | null>(peekPairingTokenFromUrl());
98 const [credential, setCredential] = useState(() => autoPairTokenRef.current ?? "");
99 const [errorMessage, setErrorMessage] = useState(initialErrorMessage ?? "");
100 const [isSubmitting, setIsSubmitting] = useState(false);
101 const autoSubmitAttemptedRef = useRef(false);
102
103 const submitCredential = useCallback(
104 async (nextCredential: string) => {
105 setIsSubmitting(true);
106 setErrorMessage("");
107
108 const submitError = await submitServerAuthCredential(nextCredential).then(
109 () => null,
110 (error) => errorMessageFromUnknown(error),
111 );
112
113 setIsSubmitting(false);
114
115 if (submitError) {
116 setErrorMessage(submitError);
117 return;
118 }
119
120 startTransition(() => {
121 onAuthenticated();
122 });
123 },
124 [onAuthenticated],
125 );
126
127 const handleSubmit = useCallback(
128 async (event?: React.SubmitEvent<HTMLFormElement>) => {
129 event?.preventDefault();
130 await submitCredential(credential);
131 },
132 [submitCredential, credential],
133 );
134
135 useEffect(() => {
136 const token = autoPairTokenRef.current;
137 if (!token || autoSubmitAttemptedRef.current) {
138 return;
139 }
140
141 autoSubmitAttemptedRef.current = true;
142 stripPairingTokenFromUrl();
143 void submitCredential(token);
144 }, [submitCredential]);
145
146 return (
147 <StandalonePage tone="pairing">
148 <StandalonePageHeader
149 eyebrow={APP_DISPLAY_NAME}
150 title="Pair with this environment"
151 description={describeAuthGate(auth.bootstrapMethods)}
152 />
153
154 <form className="mt-6 space-y-4" onSubmit={(event) => void handleSubmit(event)}>
155 <div className="space-y-2">
156 <label className="text-sm font-medium" htmlFor="pairing-token">
157 Pairing token
158 </label>
159 <Input
160 id="pairing-token"
161 autoCapitalize="none"
162 autoComplete="off"
163 autoCorrect="off"
164 disabled={isSubmitting}
165 nativeInput
166 onChange={(event) => setCredential(event.currentTarget.value)}
167 placeholder="Paste a one-time token or pairing secret"
168 spellCheck={false}
169 value={credential}
170 />
171 </div>
172
173 {errorMessage ? (
174 <Alert variant="error">
175 <AlertDescription>{errorMessage}</AlertDescription>
176 </Alert>
177 ) : null}
178
179 <div className="flex flex-wrap gap-2">
180 <Button disabled={isSubmitting} size="sm" type="submit">
181 {isSubmitting ? "Pairing..." : "Continue"}
182 </Button>
183 <Button
184 disabled={isSubmitting}
185 onClick={() => window.location.reload()}
186 size="sm"
187 variant="outline"
188 >
189 Reload app
190 </Button>
191 </div>
192 </form>
193
194 <div className="mt-6 rounded-lg border border-border/70 bg-background/55 px-3 py-3 text-xs leading-relaxed text-muted-foreground">
195 {describeSupportedMethods(auth.bootstrapMethods)}
196 </div>
197 </StandalonePage>
198 );
199}
200
201export function HostedPairingRouteSurface() {
202 const connectPairingEnvironment = useAtomCommand(connectPairing, {
203 reportFailure: false,
204 });
205 const hostedPairingRequestRef = useRef(readHostedPairingRequest());
206 const [status, setStatus] = useState<"pairing" | "paired" | "error">(() =>
207 hostedPairingRequestRef.current ? "pairing" : "error",
208 );
209 const [message, setMessage] = useState(() =>
210 hostedPairingRequestRef.current
211 ? "Connecting to this backend."
212 : "This pairing link is missing its backend host or token.",
213 );
214 const [canRetry, setCanRetry] = useState(false);
215 const submitAttemptedRef = useRef(false);
216 const tokenSubmittedRef = useRef(false);
217
218 const submitHostedPairingRequest = useCallback(async () => {
219 const request = hostedPairingRequestRef.current;
220
221 if (!request) {
222 setStatus("error");
223 setMessage("This pairing link is missing its backend host or token.");
224 setCanRetry(false);
225 return;
226 }
227
228 if (tokenSubmittedRef.current) {
229 setStatus("error");
230 setMessage("This one-time pairing token was already submitted. Request a new pairing link.");
231 setCanRetry(false);
232 return;
233 }
234
235 setStatus("pairing");
236 setMessage("Connecting to this backend.");
237 setCanRetry(false);
238 tokenSubmittedRef.current = true;
239
240 const result = await connectPairingEnvironment({
241 host: request.host,
242 pairingCode: request.token,
243 });
244 if (result._tag === "Success") {
245 setStatus("paired");
246 setMessage(`${request.label || "The environment"} is saved in this browser.`);
247 return;
248 }
249
250 tokenSubmittedRef.current = false;
251 setStatus("error");
252 setCanRetry(true);
253 setMessage(
254 `${errorMessageFromUnknown(squashAtomCommandFailure(result))} If the backend accepted this one-time token, request a new pairing link before retrying.`,
255 );
256 }, [connectPairingEnvironment]);
257
258 useEffect(() => {
259 if (submitAttemptedRef.current) {
260 return;
261 }
262 submitAttemptedRef.current = true;
263
264 stripPairingTokenFromUrl();
265 void submitHostedPairingRequest();
266 }, [submitHostedPairingRequest]);
267
268 const request = hostedPairingRequestRef.current;
269
270 return (
271 <StandalonePage tone="pairing">
272 <StandalonePageHeader
273 eyebrow={APP_DISPLAY_NAME}
274 title={
275 status === "paired"
276 ? "Backend paired"
277 : status === "error"
278 ? "Pairing failed"
279 : "Pairing backend"
280 }
281 description={message}
282 />
283
284 {request ? (
285 <div className="mt-5 rounded-lg border border-border/70 bg-background/55 px-3 py-3 text-xs leading-relaxed text-muted-foreground">
286 Host: <span className="font-mono text-foreground/80">{request.host}</span>
287 </div>
288 ) : null}
289
290 {status === "error" ? (
291 <Alert variant="error" className="mt-5">
292 <AlertDescription>
293 Verify the backend is reachable from this browser, supports CORS for hosted clients, and
294 is served over HTTPS when opening this page from HTTPS.
295 </AlertDescription>
296 </Alert>
297 ) : null}
298
299 <div className="mt-6 flex flex-wrap gap-2">
300 {status === "pairing" ? (
301 <Button disabled size="sm">
302 Pairing...
303 </Button>
304 ) : canRetry ? (
305 <Button size="sm" onClick={() => void submitHostedPairingRequest()}>
306 Try again
307 </Button>
308 ) : null}
309 {status === "paired" ? (
310 <Button size="sm" variant="outline" onClick={() => (window.location.href = "/")}>
311 Open app
312 </Button>
313 ) : null}
314 </div>
315 </StandalonePage>
316 );
317}
318
319function errorMessageFromUnknown(error: unknown): string {
320 if (error instanceof Error && error.message.trim().length > 0) {
321 return error.message;
322 }
323
324 if (typeof error === "string" && error.trim().length > 0) {
325 return error;
326 }
327
328 return "Authentication failed.";
329}
330
331function describeAuthGate(bootstrapMethods: ReadonlyArray<string>): string {
332 if (bootstrapMethods.includes("desktop-bootstrap")) {
333 return "This environment expects a trusted pairing credential before the app can connect.";
334 }
335
336 return "Enter a pairing token to start a session with this environment.";
337}
338
339function describeSupportedMethods(bootstrapMethods: ReadonlyArray<string>): string {
340 if (
341 bootstrapMethods.includes("desktop-bootstrap") &&
342 bootstrapMethods.includes("one-time-token")
343 ) {
344 return "Desktop-managed pairing and one-time pairing tokens are both accepted for this environment.";
345 }
346
347 if (bootstrapMethods.includes("desktop-bootstrap")) {
348 return "This environment is desktop-managed. Open it from the desktop app or paste a bootstrap credential if one was issued explicitly.";
349 }
350
351 return "This environment accepts one-time pairing tokens. Pairing links can open this page directly, or you can paste the token here.";
352}