feat: allow change email

This commit is contained in:
Stephen Zhou 2024-12-31 22:31:48 +08:00
parent 7233a58ca7
commit 97bacc99f1
No known key found for this signature in database
6 changed files with 139 additions and 43 deletions

View File

@ -0,0 +1,123 @@
import { Button } from "@follow/components/ui/button/index.js"
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@follow/components/ui/form/index.js"
import { Input } from "@follow/components/ui/input/Input.js"
import { changeEmail, sendVerificationEmail } from "@follow/shared/auth"
import { zodResolver } from "@hookform/resolvers/zod"
import { useMutation } from "@tanstack/react-query"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import { z } from "zod"
import { setWhoami, useWhoami } from "~/atoms/user"
const formSchema = z.object({
email: z.string().email(),
})
export function EmailManagement() {
const { t } = useTranslation("settings")
const user = useWhoami()
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
email: user?.email || "",
},
})
const updateEmailMutation = useMutation({
mutationFn: async (values: z.infer<typeof formSchema>) => {
await new Promise((resolve) => setTimeout(resolve, 1000))
const res = await changeEmail({ newEmail: values.email })
if (res.error) {
throw new Error(res.error.message)
}
},
onSuccess: (_, variables) => {
if (user?.emailVerified) {
toast.success(t("profile.email.changed_verification_sent"))
} else {
if (user) {
setWhoami({
...user,
email: variables.email,
})
}
form.reset({ email: variables.email })
toast.success(t("profile.email.changed"))
}
},
onError: (error) => {
toast.error(error.message)
},
})
const verifyEmailMutation = useMutation({
mutationFn: async () => {
if (!user?.email) return
return sendVerificationEmail({
email: user.email,
})
},
onSuccess: () => {
toast.success(t("profile.email.verification_sent"))
},
})
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit((values) => {
updateEmailMutation.mutate(values)
})}
className="space-y-4"
>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>{t("profile.email.label")}</FormLabel>
<FormControl>
<div className="flex items-center gap-x-2">
<Input {...field} />
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="space-x-2 text-right">
{!user?.emailVerified && !form.formState.isDirty && (
<Button
variant="outline"
type="button"
isLoading={verifyEmailMutation.isPending}
onClick={() => {
verifyEmailMutation.mutate()
}}
>
{t("profile.email.send_verification")}
</Button>
)}
<Button
type="submit"
isLoading={updateEmailMutation.isPending}
disabled={updateEmailMutation.isPending || !form.formState.isDirty}
>
{t("profile.email.change")}
</Button>
</div>
</form>
</Form>
)
}

View File

@ -10,8 +10,7 @@ import {
FormMessage,
} from "@follow/components/ui/form/index.jsx"
import { Input } from "@follow/components/ui/input/index.js"
import { Label } from "@follow/components/ui/label/index.js"
import { sendVerificationEmail, updateUser } from "@follow/shared/auth"
import { updateUser } from "@follow/shared/auth"
import { cn } from "@follow/utils/utils"
import { zodResolver } from "@hookform/resolvers/zod"
import { useMutation } from "@tanstack/react-query"
@ -21,7 +20,6 @@ import { toast } from "sonner"
import { z } from "zod"
import { setWhoami, useWhoami } from "~/atoms/user"
import { CopyButton } from "~/components/ui/code-highlighter"
import { toastFetchError } from "~/lib/error-parser"
const formSchema = z.object({
@ -69,18 +67,6 @@ export const ProfileSettingForm = ({
},
})
const verifyEmailMutation = useMutation({
mutationFn: async () => {
if (!user?.email) return
return sendVerificationEmail({
email: user.email,
})
},
onSuccess: () => {
toast.success(t("profile.email.verification_sent"))
},
})
function onSubmit(values: z.infer<typeof formSchema>) {
updateMutation.mutate(values)
}
@ -88,34 +74,6 @@ export const ProfileSettingForm = ({
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className={cn("mt-4 space-y-4", className)}>
<div className="space-y-2">
<Label>{t("profile.email.label")}</Label>
<p className="group flex gap-2 text-sm text-muted-foreground">
{user?.email}
{user?.email && (
<CopyButton
value={user.email}
className="size-5 p-1 opacity-0 duration-300 group-hover:opacity-100"
/>
)}
<span className={cn(user?.emailVerified ? "text-green-500" : "text-red-500")}>
{user?.emailVerified ? t("profile.email.verified") : t("profile.email.unverified")}
</span>
</p>
{!user?.emailVerified && (
<Button
variant="outline"
type="button"
isLoading={verifyEmailMutation.isPending}
onClick={() => {
verifyEmailMutation.mutate()
}}
>
{t("profile.email.send_verification")}
</Button>
)}
</div>
<FormField
control={form.control}
name="handle"

View File

@ -1,4 +1,5 @@
import { AccountManagement } from "~/modules/profile/account-management"
import { EmailManagement } from "~/modules/profile/email-management"
import { ProfileSettingForm } from "~/modules/profile/profile-setting-form"
import { UpdatePasswordForm } from "~/modules/profile/update-password-form"
import { SettingsTitle } from "~/modules/settings/title"
@ -16,6 +17,7 @@ export function Component() {
return (
<>
<SettingsTitle />
<EmailManagement />
<ProfileSettingForm />
<AccountManagement />
<UpdatePasswordForm />

View File

@ -268,6 +268,9 @@
"profile.change_password.label": "Change Password",
"profile.confirm_password.label": "Confirm Password",
"profile.current_password.label": "Current Password",
"profile.email.change": "Change Email",
"profile.email.changed": "Email changed.",
"profile.email.changed_verification_sent": "Email to verify the new email has been sent.",
"profile.email.label": "Email",
"profile.email.send_verification": "Send Verification Email",
"profile.email.unverified": "Unverified",

View File

@ -37,6 +37,7 @@ const authClient = createAuthClient({
// @keep-sorted
export const {
changeEmail,
changePassword,
createSession,
forgetPassword,

View File

@ -9628,6 +9628,15 @@ declare const auth: {
type: "string";
};
};
changeEmail: {
enabled: true;
sendChangeEmailVerification: ({ newEmail, url }: {
user: better_auth.User;
newEmail: string;
url: string;
token: string;
}) => Promise<void>;
};
};
account: {
accountLinking: {