159 lines
4.8 KiB
TypeScript
159 lines
4.8 KiB
TypeScript
import { useRouter, useLocalSearchParams } from "expo-router";
|
|
import { useState } from "react";
|
|
import { View, Text } from "react-native";
|
|
import { z } from "zod";
|
|
import { useForm } from "react-hook-form";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
|
|
import { AuthLayout } from "@/components/auth/AuthLayout";
|
|
import { AuthHeader } from "@/components/auth/AuthHeader";
|
|
import { AuthCard } from "@/components/auth/AuthCard";
|
|
import { AuthFooter } from "@/components/auth/AuthFooter";
|
|
import { FormInput } from "@/components/auth/FormInput";
|
|
import { PasswordInput } from "@/components/auth/PasswordInput";
|
|
import { AuthButton } from "@/components/auth/AuthButton";
|
|
import { getApiUrl } from "@/store/api/baseApi";
|
|
|
|
const schema = z
|
|
.object({
|
|
password: z.string().min(8, "Password must be at least 8 characters"),
|
|
confirmPassword: z.string(),
|
|
})
|
|
.refine((d) => d.password === d.confirmPassword, {
|
|
path: ["confirmPassword"],
|
|
message: "Passwords do not match",
|
|
});
|
|
|
|
type FormVals = z.infer<typeof schema>;
|
|
|
|
const ResetPasswordScreen = () => {
|
|
const router = useRouter();
|
|
const { token: tokenParam } = useLocalSearchParams<{ token?: string }>();
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [message, setMessage] = useState("");
|
|
const [error, setError] = useState("");
|
|
const [tokenOverride, setTokenOverride] = useState("");
|
|
|
|
const {
|
|
handleSubmit,
|
|
setValue,
|
|
watch,
|
|
formState: { errors },
|
|
} = useForm<FormVals>({
|
|
resolver: zodResolver(schema),
|
|
mode: "onSubmit",
|
|
reValidateMode: "onChange",
|
|
shouldUnregister: false,
|
|
defaultValues: { password: "", confirmPassword: "" },
|
|
});
|
|
|
|
const password = watch("password");
|
|
const confirmPassword = watch("confirmPassword");
|
|
const tokenFromLink =
|
|
typeof tokenParam === "string" && tokenParam.length > 0 ? tokenParam : "";
|
|
const token = tokenFromLink || tokenOverride;
|
|
|
|
const onSubmit = async (vals: FormVals) => {
|
|
setSubmitting(true);
|
|
setError("");
|
|
setMessage("");
|
|
try {
|
|
if (!token) {
|
|
throw new Error("Reset link token is required");
|
|
}
|
|
const res = await fetch(
|
|
`${getApiUrl()?.replace(/\/$/, "")}/auth/password/reset`,
|
|
{
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ token, password: vals.password }),
|
|
}
|
|
);
|
|
const data = await res.json().catch(() => ({}));
|
|
if (!res.ok) {
|
|
throw new Error(data?.error || "Failed to reset password");
|
|
}
|
|
setMessage("Password has been reset. Redirecting to sign in...");
|
|
setTimeout(() => router.replace("/sign-in"), 1500);
|
|
} catch (e: any) {
|
|
setError(e?.message || "Failed to reset password");
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<AuthLayout>
|
|
<AuthHeader title="Project Athena" />
|
|
<AuthCard
|
|
title="Reset Password"
|
|
subtitle={
|
|
tokenFromLink
|
|
? "Set a new password"
|
|
: "Paste the reset link token and set a new password"
|
|
}
|
|
>
|
|
{!!error && (
|
|
<Text style={{ color: "#F83434", marginBottom: 8 }}>{error}</Text>
|
|
)}
|
|
{!!message && (
|
|
<Text style={{ color: "#22C927", marginBottom: 8 }}>{message}</Text>
|
|
)}
|
|
|
|
{!tokenFromLink && (
|
|
<FormInput
|
|
label={"Reset token"}
|
|
value={tokenOverride}
|
|
onChangeText={setTokenOverride}
|
|
icon="key"
|
|
disabled={submitting}
|
|
onSubmitEditing={handleSubmit(onSubmit)}
|
|
returnKeyType="next"
|
|
/>
|
|
)}
|
|
|
|
<PasswordInput
|
|
label={errors.password?.message || "New Password"}
|
|
value={password}
|
|
onChangeText={(t) =>
|
|
setValue("password", t, { shouldValidate: true })
|
|
}
|
|
disabled={submitting}
|
|
onSubmitEditing={handleSubmit(onSubmit)}
|
|
returnKeyType="next"
|
|
/>
|
|
|
|
<PasswordInput
|
|
label={errors.confirmPassword?.message || "Confirm Password"}
|
|
value={confirmPassword}
|
|
onChangeText={(t) =>
|
|
setValue("confirmPassword", t, { shouldValidate: true })
|
|
}
|
|
disabled={submitting}
|
|
onSubmitEditing={handleSubmit(onSubmit)}
|
|
returnKeyType="send"
|
|
submitBehavior="blurAndSubmit"
|
|
/>
|
|
|
|
<View style={{ marginTop: 8 }}>
|
|
<AuthButton
|
|
onPress={handleSubmit(onSubmit)}
|
|
loading={submitting}
|
|
disabled={submitting}
|
|
>
|
|
{submitting ? "Resetting password..." : "Reset Password"}
|
|
</AuthButton>
|
|
</View>
|
|
</AuthCard>
|
|
<AuthFooter
|
|
question="Remembered your password?"
|
|
actionText="Sign in"
|
|
onPress={() => router.push("/sign-in")}
|
|
disabled={submitting}
|
|
/>
|
|
</AuthLayout>
|
|
);
|
|
};
|
|
|
|
export default ResetPasswordScreen;
|