From 97bacc99f1e0d4afd241b45b2e98d2d19b375b82 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Tue, 31 Dec 2024 22:31:48 +0800 Subject: [PATCH] feat: allow change email --- .../src/modules/profile/email-management.tsx | 123 ++++++++++++++++++ .../modules/profile/profile-setting-form.tsx | 44 +------ .../src/pages/settings/(settings)/profile.tsx | 2 + locales/settings/en.json | 3 + packages/shared/src/auth.ts | 1 + packages/shared/src/hono.ts | 9 ++ 6 files changed, 139 insertions(+), 43 deletions(-) create mode 100644 apps/renderer/src/modules/profile/email-management.tsx diff --git a/apps/renderer/src/modules/profile/email-management.tsx b/apps/renderer/src/modules/profile/email-management.tsx new file mode 100644 index 000000000..3f97664a0 --- /dev/null +++ b/apps/renderer/src/modules/profile/email-management.tsx @@ -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>({ + resolver: zodResolver(formSchema), + defaultValues: { + email: user?.email || "", + }, + }) + + const updateEmailMutation = useMutation({ + mutationFn: async (values: z.infer) => { + 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 ( +
+ { + updateEmailMutation.mutate(values) + })} + className="space-y-4" + > + ( + + {t("profile.email.label")} + +
+ +
+
+ +
+ )} + /> + +
+ {!user?.emailVerified && !form.formState.isDirty && ( + + )} + +
+ + + ) +} diff --git a/apps/renderer/src/modules/profile/profile-setting-form.tsx b/apps/renderer/src/modules/profile/profile-setting-form.tsx index d2ad59f37..03c679189 100644 --- a/apps/renderer/src/modules/profile/profile-setting-form.tsx +++ b/apps/renderer/src/modules/profile/profile-setting-form.tsx @@ -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) { updateMutation.mutate(values) } @@ -88,34 +74,6 @@ export const ProfileSettingForm = ({ return (
-
- -

- {user?.email} - - {user?.email && ( - - )} - - {user?.emailVerified ? t("profile.email.verified") : t("profile.email.unverified")} - -

- {!user?.emailVerified && ( - - )} -
+ diff --git a/locales/settings/en.json b/locales/settings/en.json index 1b4acffd6..3bbbc4603 100644 --- a/locales/settings/en.json +++ b/locales/settings/en.json @@ -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", diff --git a/packages/shared/src/auth.ts b/packages/shared/src/auth.ts index 60e37b677..5c34753a9 100644 --- a/packages/shared/src/auth.ts +++ b/packages/shared/src/auth.ts @@ -37,6 +37,7 @@ const authClient = createAuthClient({ // @keep-sorted export const { + changeEmail, changePassword, createSession, forgetPassword, diff --git a/packages/shared/src/hono.ts b/packages/shared/src/hono.ts index 258abc711..047b9a387 100644 --- a/packages/shared/src/hono.ts +++ b/packages/shared/src/hono.ts @@ -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; + }; }; account: { accountLinking: {