136 lines · 5.5 KB
import { useState } from "react";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { redirectSignedIn } from "~/lib/route-guards";
import { returnSearch, safeReturnPath } from "~/lib/return-to";
import { getAuthOptions, signUp } from "~/fns/auth";
import { getSignupGate } from "~/fns/waitlist";
import { WaitlistForm } from "~/components/WaitlistForm";
import { useStripSearchParam } from "~/hooks/use-strip-search-param";
import { useAction } from "~/hooks/use-action";
import { useTurnstile } from "~/hooks/use-turnstile";
import { useBootstrapToken } from "~/hooks/use-bootstrap-token";
import { AuthCard } from "~/components/AuthCard";
import { SignupConsent } from "~/components/SignupConsent";
import { Button, buttonClass } from "~/ui/Button";
import { TextField } from "~/ui/Field";
import { Callout, ErrorBanner } from "~/ui/Callout";
import { TextLink } from "~/ui/TextLink";

export const Route = createFileRoute("/signup")({
  // `bootstrap` is read straight from the address bar by useBootstrapToken, so it must survive search validation.
  validateSearch: (search: Record<string, unknown>) => ({
    ...returnSearch(search),
    ...(typeof search.bootstrap === "string" ? { bootstrap: search.bootstrap } : {}),
    ...(typeof search.invite === "string" ? { invite: search.invite } : {}),
  }),
  beforeLoad: redirectSignedIn,
  loaderDeps: ({ search }) => ({ invite: search.invite }),
  loader: async ({ deps }) => {
    const [options, gate] = await Promise.all([getAuthOptions(), getSignupGate({ data: { invite: deps.invite } })]);
    return { ...options, gate, inviteToken: gate.invitedEmail ? (deps.invite ?? null) : null };
  },
  component: SignupPage,
});

function SignupPage() {
  const options = Route.useLoaderData();
  // Validated again here: router search state can still carry the raw value during SSR.
  const next = safeReturnPath(Route.useSearch().next) ?? undefined;
  const navigate = useNavigate();
  const action = useAction(signUp);