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