metatron-open-seo/src/routes/_auth.sign-in.tsx

259 lines
7.4 KiB
TypeScript

import { useForm } from "@tanstack/react-form";
import { Link, createFileRoute } from "@tanstack/react-router";
import { useState } from "react";
import { toast } from "sonner";
import {
AuthPageCard,
authRedirectSearchSchema,
getFieldError,
getFormError,
useAuthPageState,
} from "@/client/features/auth/AuthPage";
import { captureClientEvent } from "@/client/lib/posthog";
import { authClient } from "@/lib/auth-client";
import { getSignInSearch } from "@/lib/auth-redirect";
import { z } from "zod";
const signInSchema = z.object({
email: z.string().trim().email("Enter a valid email address."),
password: z.string().min(1, "Enter your password."),
});
export const Route = createFileRoute("/_auth/sign-in")({
validateSearch: authRedirectSearchSchema,
component: SignInPage,
});
function SignInPage() {
const search = Route.useSearch();
const { redirectTo, oauthQuery, isHostedMode } = useAuthPageState(
search.redirect,
);
const [verificationEmail, setVerificationEmail] = useState<string | null>(
null,
);
const [isSendingVerification, setIsSendingVerification] = useState(false);
const form = useForm({
defaultValues: {
email: "",
password: "",
},
validators: {
onSubmit: signInSchema,
},
onSubmit: async ({ formApi, value }) => {
try {
const email = value.email.trim();
captureClientEvent("auth:sign_in_submit", {
redirect_to: redirectTo,
});
setVerificationEmail(null);
const result = await authClient.signIn.email({
email,
password: value.password,
callbackURL: redirectTo,
...(oauthQuery ? { oauth_query: oauthQuery } : {}),
});
if (!result.error) {
captureClientEvent("auth:sign_in_success", {
redirect_to: redirectTo,
});
return;
}
if (result.error.status === 403) {
captureClientEvent("auth:sign_in_block_unverified", {
redirect_to: redirectTo,
});
setVerificationEmail(email);
formApi.setErrorMap({
onSubmit: {
form: "Please confirm your email before signing in.",
fields: {},
},
});
return;
}
formApi.setErrorMap({
onSubmit: {
form: result.error.message || "We couldn't sign you in.",
fields: {},
},
});
} catch {
formApi.setErrorMap({
onSubmit: {
form: "Unable to sign in right now. Please try again.",
fields: {},
},
});
}
},
});
async function handleResendVerification() {
if (!verificationEmail) {
return;
}
setIsSendingVerification(true);
try {
const callbackURL = new URL("/verify-email", window.location.origin);
if (redirectTo !== "/")
callbackURL.searchParams.set("redirect", redirectTo);
const result = await authClient.sendVerificationEmail({
email: verificationEmail,
callbackURL: callbackURL.toString(),
});
if (result.error) {
toast.error(result.error.message || "We couldn't send another email.");
return;
}
captureClientEvent("auth:verification_resend");
toast.success("A new email is on the way.");
} catch {
toast.error(
"We couldn't send another email right now. Please try again.",
);
} finally {
setIsSendingVerification(false);
}
}
return (
<AuthPageCard
title="Sign in"
footer={
isHostedMode ? (
<div className="flex justify-between text-sm text-base-content/50">
<Link
to="/forgot-password"
search={getSignInSearch(redirectTo)}
className="text-base-content underline underline-offset-2 hover:text-base-content/80 transition-colors"
>
Forgot password?
</Link>
<Link
to="/sign-up"
search={getSignInSearch(redirectTo)}
className="text-base-content underline underline-offset-2 hover:text-base-content/80 transition-colors"
>
Create account
</Link>
</div>
) : null
}
>
<form
className="space-y-4"
onSubmit={(event) => {
event.preventDefault();
void form.handleSubmit();
}}
>
<form.Field name="email">
{(field) => {
const error = getFieldError(field.state.meta.errors);
return (
<div>
<input
type="email"
className="input input-bordered w-full"
placeholder="Email address..."
value={field.state.value}
onChange={(event) => field.handleChange(event.target.value)}
autoComplete="email"
disabled={!isHostedMode}
required
/>
{error ? (
<p className="mt-1 text-sm text-error">{error}</p>
) : null}
</div>
);
}}
</form.Field>
<form.Field name="password">
{(field) => {
const error = getFieldError(field.state.meta.errors);
return (
<div>
<input
type="password"
className="input input-bordered w-full"
placeholder="Password..."
value={field.state.value}
onChange={(event) => field.handleChange(event.target.value)}
autoComplete="current-password"
disabled={!isHostedMode}
required
/>
{error ? (
<p className="mt-1 text-sm text-error">{error}</p>
) : null}
</div>
);
}}
</form.Field>
{verificationEmail ? (
<div className="alert alert-warning items-start">
<div className="space-y-3">
<p className="text-sm">
Please check {verificationEmail} for a link to confirm your
email.
</p>
<button
type="button"
className="btn btn-sm btn-outline"
onClick={() => {
void handleResendVerification();
}}
disabled={isSendingVerification}
>
{isSendingVerification
? "Sending email..."
: "Send another email"}
</button>
</div>
</div>
) : null}
<form.Subscribe
selector={(state) => ({
submitError: state.errorMap.onSubmit,
isSubmitting: state.isSubmitting,
})}
>
{({ submitError, isSubmitting }) => {
const errorMessage = getFormError(submitError);
return (
<>
{errorMessage ? (
<p className="text-sm text-error">{errorMessage}</p>
) : null}
<button
className="btn btn-soft w-full"
disabled={!isHostedMode || isSubmitting}
>
{isSubmitting ? "Signing in..." : "Sign in"}
</button>
</>
);
}}
</form.Subscribe>
</form>
</AuthPageCard>
);
}