Skip to content
100 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 px-4 py-32 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">
53 Email confirmed
54 </h1>
55 {loaderData.added ? (
56 <>
57 <p className="mt-2 text-muted">
58 The address is confirmed on {loaderData.username}. You can make it
59 your primary address in your settings.
60 </p>
61 <div className="mt-8">
62 <ButtonLink to="/settings/emails">Back to settings</ButtonLink>
63 </div>
64 </>
65 ) : (
66 <>
67 <p className={`mt-2 ${loaderData.lapsed ? "text-fg" : "text-muted"}`} role="status">
68 {loaderData.line} Your account {loaderData.username} is ready.
69 </p>
70 <div className="mt-8">
71 <ButtonLink to={loaderData.signedIn ? loaderData.to : `/login?next=${encodeURIComponent(loaderData.to)}`}>
72 {loaderData.signedIn ? "Continue" : "Sign in"}
73 </ButtonLink>
74 </div>
75 </>
76 )}
77 </>
78 ) : (
79 <>
80 <MailWarning size={40} className="mx-auto text-warn" />
81 <h1 className="mt-6 text-2xl font-semibold tracking-tight">
82 That link did not work
83 </h1>
84 <p className="mt-2 text-muted">{loaderData.message}</p>
85 <p className="mt-2 text-sm text-muted">
86 Links work once, for an hour, and a newer email replaces them.
87 {loaderData.signedIn
88 ? " Enter the code from your latest email, or send a new one."
89 : " Sign in to enter the code from the email, or to send a new one."}
90 </p>
91 <div className="mt-8">
92 <ButtonLink to={loaderData.signedIn ? CONFIRM_PATH : `/login?next=${encodeURIComponent(CONFIRM_PATH)}`} variant="quiet">
93 {loaderData.signedIn ? "Enter a code" : "Sign in"}
94 </ButtonLink>
95 </div>
96 </>
97 )}
98 </main>
99 );
100}