Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

93 linesCodeBlameRaw
1import { CircleCheck, MailWarning } from "lucide-react";
2import { redirect } from "react-router";
3
4import type { Route } from "./+types/verify";
5import { page } from "../lib/meta";
6import { ButtonLink } from "../components/ui";
7import { CONFIRM_PATH, afterConfirming, confirmedLine } from "../lib/confirm-gate";
8import { identity } from "../lib/services.server";
9import { assertSameOrigin, getViewer, requireUser } from "../lib/session.server";
10
11export function meta(args: Route.MetaArgs) {
12 return page(args, { title: "Confirm your email · g1t" });
13}
14
15/**
16 * Follows the link from the confirmation email, signed in or not: the same
17 * email's code does the same on /confirm-email.
18 */
19export async function loader({ request, context }: Route.LoaderArgs) {
20 const token = new URL(request.url).searchParams.get("token") ?? "";
21 const viewer = getViewer(context);
22 const result = await identity.verifyEmail(token);
23 if (!result.ok) return { ok: false as const, message: result.error.message, signedIn: viewer != null };
24 const done = result.value;
25 // An address added to an account in use goes back to settings; a new
26 // account's first goes on to the workspace its invite joined, or home.
27 const added = viewer?.verified === true;
28 return {
29 ok: true as const,
30 username: done.username,
31 added,
32 line: confirmedLine(done),
33 lapsed: Boolean(done.inviteLapsed),
34 to: afterConfirming("/", done.joined, done.invitedTo),
35 signedIn: viewer != null,
36 };
37}
38
39/** An old form's "resend": the confirmation page sends new codes now. */
40export async function action({ request, context }: Route.ActionArgs) {
41 assertSameOrigin(request);
42 await identity.resendVerification(requireUser(context, request));
43 throw redirect(CONFIRM_PATH);
44}
45
46export default function Verify({ loaderData }: Route.ComponentProps) {
47 return (
48 <main className="mx-auto max-w-md pt-10 text-center">
49 {loaderData.ok ? (
50 <>
51 <CircleCheck size={40} className="mx-auto text-success" />
52 <h1 className="mt-6 text-2xl font-semibold tracking-tight">Email confirmed</h1>
53 {loaderData.added ? (
54 <>
55 <p className="mt-2 text-muted">The address is confirmed on {loaderData.username}. You can make it your primary address in your settings.</p>
56 <div className="mt-8">
57 <ButtonLink to="/settings/emails">Back to settings</ButtonLink>
58 </div>
59 </>
60 ) : (
61 <>
62 <p className={`mt-2 ${loaderData.lapsed ? "text-fg" : "text-muted"}`} role="status">
63 {loaderData.line} Your account {loaderData.username} is ready.
64 </p>
65 <div className="mt-8">
66 <ButtonLink to={loaderData.signedIn ? loaderData.to : `/login?next=${encodeURIComponent(loaderData.to)}`}>
67 {loaderData.signedIn ? "Continue" : "Sign in"}
68 </ButtonLink>
69 </div>
70 </>
71 )}
72 </>
73 ) : (
74 <>
75 <MailWarning size={40} className="mx-auto text-warn" />
76 <h1 className="mt-6 text-2xl font-semibold tracking-tight">That link did not work</h1>
77 <p className="mt-2 text-muted">{loaderData.message}</p>
78 <p className="mt-2 text-sm text-muted">
79 Links work once, for an hour, and a newer email replaces them.
80 {loaderData.signedIn
81 ? " Enter the code from your latest email, or send a new one."
82 : " Sign in to enter the code from the email, or to send a new one."}
83 </p>
84 <div className="mt-8">
85 <ButtonLink to={loaderData.signedIn ? CONFIRM_PATH : `/login?next=${encodeURIComponent(CONFIRM_PATH)}`} variant="outline">
86 {loaderData.signedIn ? "Enter a code" : "Sign in"}
87 </ButtonLink>
88 </div>
89 </>
90 )}
91 </main>
92 );
93}