Skip to content

Commit

Permalink
Update
Browse files Browse the repository at this point in the history
  • Loading branch information
lukevella committed Jan 13, 2025
1 parent f059798 commit 3b2edc5
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 87 deletions.
5 changes: 4 additions & 1 deletion apps/web/public/locales/en/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -287,5 +287,8 @@
"emailChangeInvalidToken": "The verification link is invalid or has expired. Please try again.",
"emailChangeError": "An error occurred while changing your email",
"emailChangeRequestSent": "Verify your new email address",
"emailChangeRequestSentDescription": "To complete the change, please check your email for a verification link."
"emailChangeRequestSentDescription": "To complete the change, please check your email for a verification link.",
"profileEmailAddress": "Email Address",
"profileEmailAddressDescription": "Your email address is used to log in to your account",
"emailAlreadyInUse": "Email already in use. Please try a different one or delete the existing account."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { usePostHog } from "@rallly/posthog/client";
import { Alert, AlertDescription, AlertTitle } from "@rallly/ui/alert";
import { Button } from "@rallly/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@rallly/ui/form";
import { useToast } from "@rallly/ui/hooks/use-toast";
import { Input } from "@rallly/ui/input";
import Cookies from "js-cookie";
import { InfoIcon } from "lucide-react";
import React from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";

import { Trans } from "@/components/trans";
import { useUser } from "@/components/user-provider";
import { trpc } from "@/trpc/client";

export const ProfileEmailAddress = () => {
const { user, refresh } = useUser();
const requestEmailChange = trpc.user.requestEmailChange.useMutation();
const posthog = usePostHog();
const form = useForm<{
name: string;
email: string;
}>({
defaultValues: {
name: user.isGuest ? "" : user.name,
email: user.email ?? "",
},
});
const { t } = useTranslation("app");
const { toast } = useToast();

const [didRequestEmailChange, setDidRequestEmailChange] =
React.useState(false);

React.useEffect(() => {
const success = Cookies.get("email-change-success");
const error = Cookies.get("email-change-error");

if (success) {
posthog.capture("email change completed");
toast({
title: t("emailChangeSuccess", {
defaultValue: "Email changed successfully",
}),
description: t("emailChangeSuccessDescription", {
defaultValue: "Your email has been updated",
}),
});
}

if (error) {
posthog.capture("email change failed", { error });
toast({
variant: "destructive",
title: t("emailChangeFailed", {
defaultValue: "Email change failed",
}),
description:
error === "invalidToken"
? t("emailChangeInvalidToken", {
defaultValue:
"The verification link is invalid or has expired. Please try again.",
})
: t("emailChangeError", {
defaultValue: "An error occurred while changing your email",
}),
});
}
}, [posthog, refresh, t, toast]);

const { handleSubmit, formState, reset } = form;
return (
<div className="grid gap-y-4">
<Form {...form}>
<form
onSubmit={handleSubmit(async (data) => {
reset(data);
if (data.email !== user.email) {
posthog.capture("email change requested");
const res = await requestEmailChange.mutateAsync({
email: data.email,
});
if (res.success === false) {
if (res.reason === "emailAlreadyInUse") {
form.setError("email", {
message: t("emailAlreadyInUse", {
defaultValue:
"This email address is already associated with another account. Please use a different email address.",
}),
});
}
} else {
setDidRequestEmailChange(true);
}
}
await refresh();
})}
>
<div className="flex flex-col gap-y-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans i18nKey="email" />
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{didRequestEmailChange ? (
<Alert icon={InfoIcon}>
<AlertTitle>
<Trans
i18nKey="emailChangeRequestSent"
defaults="Verify your new email address"
/>
</AlertTitle>
<AlertDescription>
<Trans
i18nKey="emailChangeRequestSentDescription"
defaults="To complete the change, please check your email for a verification link."
/>
</AlertDescription>
</Alert>
) : null}
<div className="mt-4 flex">
<Button
loading={formState.isSubmitting}
variant="primary"
type="submit"
disabled={!formState.isDirty}
>
<Trans i18nKey="save" />
</Button>
</div>
</div>
</form>
</Form>
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import {
import { Trans } from "@/components/trans";
import { useUser } from "@/components/user-provider";

import { ProfileEmailAddress } from "./profile-email-address";

export const ProfilePage = () => {
const { t } = useTranslation();
const { user } = useUser();
Expand Down Expand Up @@ -78,6 +80,19 @@ export const ProfilePage = () => {
>
<ProfileSettings />
</SettingsSection>
<SettingsSection
title={
<Trans i18nKey="profileEmailAddress" defaults="Email Address" />
}
description={
<Trans
i18nKey="profileEmailAddressDescription"
defaults="Your email address is used to log in to your account"
/>
}
>
<ProfileEmailAddress />
</SettingsSection>
<hr />

<SettingsSection
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { usePostHog } from "@rallly/posthog/client";
import { Alert, AlertDescription, AlertTitle } from "@rallly/ui/alert";
import { Button } from "@rallly/ui/button";
import {
Form,
Expand All @@ -8,13 +6,8 @@ import {
FormItem,
FormLabel,
} from "@rallly/ui/form";
import { useToast } from "@rallly/ui/hooks/use-toast";
import { Input } from "@rallly/ui/input";
import Cookies from "js-cookie";
import { InfoIcon } from "lucide-react";
import React from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";

import { ProfilePicture } from "@/app/[locale]/(admin)/settings/profile/profile-picture";
import { Trans } from "@/components/trans";
Expand All @@ -24,8 +17,7 @@ import { trpc } from "@/trpc/client";
export const ProfileSettings = () => {
const { user, refresh } = useUser();
const changeName = trpc.user.changeName.useMutation();
const requestEmailChange = trpc.user.requestEmailChange.useMutation();
const posthog = usePostHog();

const form = useForm<{
name: string;
email: string;
Expand All @@ -35,64 +27,18 @@ export const ProfileSettings = () => {
email: user.email ?? "",
},
});
const { t } = useTranslation("app");
const { toast } = useToast();

const [didRequestEmailChange, setDidRequestEmailChange] =
React.useState(false);

React.useEffect(() => {
const success = Cookies.get("email-change-success");
const error = Cookies.get("email-change-error");

if (success) {
posthog.capture("email change completed");
toast({
title: t("emailChangeSuccess", {
defaultValue: "Email changed successfully",
}),
description: t("emailChangeSuccessDescription", {
defaultValue: "Your email has been updated",
}),
});
}

if (error) {
posthog.capture("email change failed", { error });
toast({
variant: "destructive",
title: t("emailChangeFailed", {
defaultValue: "Email change failed",
}),
description:
error === "invalidToken"
? t("emailChangeInvalidToken", {
defaultValue:
"The verification link is invalid or has expired. Please try again.",
})
: t("emailChangeError", {
defaultValue: "An error occurred while changing your email",
}),
});
}
}, [posthog, refresh, t, toast]);

const { control, handleSubmit, formState, reset } = form;
return (
<div className="grid gap-y-4">
<Form {...form}>
<form
onSubmit={handleSubmit(async (data) => {
if (data.email !== user.email) {
posthog.capture("email change requested");
await requestEmailChange.mutateAsync({ email: data.email });
setDidRequestEmailChange(true);
}
if (data.name !== user.name) {
await changeName.mutateAsync({ name: data.name });
}
await refresh();
reset(data);
await refresh();
})}
>
<div className="flex flex-col gap-y-4">
Expand All @@ -111,36 +57,7 @@ export const ProfileSettings = () => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans i18nKey="email" />
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
</FormItem>
)}
/>
{didRequestEmailChange ? (
<Alert icon={InfoIcon}>
<AlertTitle>
<Trans
i18nKey="emailChangeRequestSent"
defaults="Verify your new email address"
/>
</AlertTitle>
<AlertDescription>
<Trans
i18nKey="emailChangeRequestSentDescription"
defaults="To complete the change, please check your email for a verification link."
/>
</AlertDescription>
</Alert>
) : null}

<div className="mt-4 flex">
<Button
loading={formState.isSubmitting}
Expand Down
14 changes: 14 additions & 0 deletions apps/web/src/trpc/routers/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,18 @@ export const user = router({
.use(rateLimitMiddleware)
.input(z.object({ email: z.string().email() }))
.mutation(async ({ input, ctx }) => {
// check if the email is already in use
const existingUser = await prisma.user.count({
where: { email: input.email },
});

if (existingUser) {
return {
success: false as const,
reason: "emailAlreadyInUse" as const,
};
}

// create a verification token
const token = await createToken({
fromEmail: ctx.user.email,
Expand All @@ -124,6 +136,8 @@ export const user = router({
toEmail: input.email,
},
});

return { success: true as const };
}),
getAvatarUploadUrl: privateProcedure
.use(rateLimitMiddleware)
Expand Down

0 comments on commit 3b2edc5

Please sign in to comment.