"use client";
import type { TurnstileInstance } from "@marsidev/react-turnstile";
import { useRef, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { ActionButton } from "@/components/ui/action-button";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { signIn } from "@/lib/auth/client";
import { useClientConfig } from "@/lib/clientConfig";
import { zodResolver } from "@hookform/resolvers/zod";
import { Turnstile } from "@marsidev/react-turnstile";
import { useMutation } from "@tanstack/react-query";
import { TRPCClientError } from "@trpc/client";
import { AlertCircle, UserX } from "lucide-react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { useTRPC } from "@karakeep/shared-react/trpc";
import { zSignUpSchema } from "@karakeep/shared/types/users";
import { isMobileAppRedirect } from "@karakeep/shared/utils/redirectUrl";
const VERIFY_EMAIL_ERROR = "Please verify your email address before signing in";
interface SignUpFormProps {
redirectUrl: string;
}
export default function SignUpForm({ redirectUrl }: SignUpFormProps) {
const api = useTRPC();
const form = useForm<z.infer<typeof zSignUpSchema>>({
resolver: zodResolver(zSignUpSchema),
defaultValues: {
email: "",
name: "",
password: "",
confirmPassword: "",
turnstileToken: "",
},
});
const [errorMessage, setErrorMessage] = useState("");
const router = useRouter();
const clientConfig = useClientConfig();
const turnstileSiteKey = clientConfig.turnstile?.siteKey;
const turnstileRef = useRef<TurnstileInstance>(null);
const createUserMutation = useMutation(api.users.create.mutationOptions());
if (
clientConfig.auth.disableSignups ||
clientConfig.auth.disablePasswordAuth
) {
return (
<Card className="w-full">
<CardHeader className="text-center">
<CardTitle className="text-2xl font-bold">
Sign Up Unavailable
</CardTitle>
<CardDescription>
Account registration is currently disabled
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-4">
<Alert>
<UserX className="h-4 w-4" />
<AlertDescription>
Signups are currently disabled. Please contact an administrator
for access.
</AlertDescription>
</Alert>
<Button asChild className="w-full">
<Link href="/signin">Back to Sign In</Link>
</Button>
</div>
</CardContent>
</Card>
);
}
return (
<Card className="w-full">
<CardHeader className="text-center">
<CardTitle className="text-2xl font-bold">
Create Your Account
</CardTitle>
<CardDescription>
Join Karakeep to start organizing your bookmarks
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<Form {...form}>
<form
onSubmit={form.handleSubmit(async (value) => {
if (turnstileSiteKey && !value.turnstileToken) {
form.setError("turnstileToken", {
type: "manual",
message: "Please complete the verification challenge",
});
return;
}
form.clearErrors("turnstileToken");
try {
await createUserMutation.mutateAsync({
...value,
redirectUrl,
});
} catch (e) {
if (e instanceof TRPCClientError) {
setErrorMessage(e.message);
}
// Reset turnstile widget on error to get a new token
if (turnstileSiteKey) {
turnstileRef.current?.reset();
form.setValue("turnstileToken", "");
}
return;
}
const resp = await signIn("credentials", {
redirect: false,
email: value.email.trim(),
password: value.password,
});
if (!resp || !resp.ok || resp.error) {
if (resp?.error === VERIFY_EMAIL_ERROR) {
router.replace(
`/check-email?email=${encodeURIComponent(value.email.trim())}&redirectUrl=${encodeURIComponent(redirectUrl)}`,
);
} else {
setErrorMessage(
resp?.error ?? "Hit an unexpected error while signing in",
);
}
// Reset turnstile widget on error to get a new token
if (turnstileSiteKey) {
turnstileRef.current?.reset();
form.setValue("turnstileToken", "");
}
return;
}
if (isMobileAppRedirect(redirectUrl)) {
window.location.href = redirectUrl;
} else {
router.replace(redirectUrl);
}
})}
className="space-y-4"
>
{errorMessage && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{errorMessage}</AlertDescription>
</Alert>
)}
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Full Name</FormLabel>
<FormControl>
<Input
type="text"
placeholder="Enter your full name"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
type="email"
placeholder="Enter your email"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Create a password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Confirm Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Confirm your password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{turnstileSiteKey && (
<FormField
control={form.control}
name="turnstileToken"
render={({ field }) => (
<FormItem>
<FormLabel>Verification</FormLabel>
<FormControl>
<Turnstile
ref={turnstileRef}
siteKey={turnstileSiteKey}
onSuccess={(token) => {
field.onChange(token);
form.clearErrors("turnstileToken");
}}
onExpire={() => field.onChange("")}
onError={() => {
field.onChange("");
form.setError("turnstileToken", {
type: "manual",
message:
"Verification failed, please reload the challenge",
});
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<ActionButton
type="submit"
loading={
form.formState.isSubmitting || createUserMutation.isPending
}
className="w-full"
>
Sign up
</ActionButton>
{(clientConfig.legal.termsOfServiceUrl ||
clientConfig.legal.privacyPolicyUrl) && (
<p className="text-center text-xs text-muted-foreground">
By clicking on 'Sign up' above, you are agreeing to
the{" "}
{clientConfig.legal.termsOfServiceUrl && (
<Link
href={clientConfig.legal.termsOfServiceUrl}
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-foreground"
>
Terms of Service
</Link>
)}
{clientConfig.legal.termsOfServiceUrl &&
clientConfig.legal.privacyPolicyUrl &&
" and "}
{clientConfig.legal.privacyPolicyUrl && (
<Link
href={clientConfig.legal.privacyPolicyUrl}
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-foreground"
>
Privacy Policy
</Link>
)}
.
</p>
)}
</form>
</Form>
<div className="text-center">
<p className="text-sm text-gray-600">
Already have an account?{" "}
<Link
href="/signin"
className="font-medium text-blue-600 hover:text-blue-500"
>
Sign in
</Link>
</p>
</div>
</CardContent>
</Card>
);
}