mirror of
https://github.com/lukevella/rallly.git
synced 2025-04-30 18:56:45 +02:00
✨ Enable changing user email address (#1493)
This commit is contained in:
parent
31dc85bbbb
commit
8c77047f74
14 changed files with 411 additions and 22 deletions
|
@ -1,9 +1,8 @@
|
||||||
const typescriptTransform = require("i18next-scanner-typescript");
|
const typescriptTransform = require("i18next-scanner-typescript");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
input: ["src/**/*.{ts,tsx}", "!src/utils/auth.ts"],
|
input: ["src/**/*.{ts,tsx}", "!src/auth.ts"],
|
||||||
options: {
|
options: {
|
||||||
keySeparator: ".",
|
|
||||||
nsSeparator: false,
|
nsSeparator: false,
|
||||||
defaultNs: "app",
|
defaultNs: "app",
|
||||||
defaultValue: "__STRING_NOT_TRANSLATED__",
|
defaultValue: "__STRING_NOT_TRANSLATED__",
|
||||||
|
|
|
@ -279,5 +279,15 @@
|
||||||
"subscribe": "Subscribe",
|
"subscribe": "Subscribe",
|
||||||
"cancelAnytime": "Cancel anytime from your <a>billing page</a>.",
|
"cancelAnytime": "Cancel anytime from your <a>billing page</a>.",
|
||||||
"unsubscribeToastTitle": "You have disabled notifications",
|
"unsubscribeToastTitle": "You have disabled notifications",
|
||||||
"unsubscribeToastDescription": "You will no longer receive notifications for this poll"
|
"unsubscribeToastDescription": "You will no longer receive notifications for this poll",
|
||||||
|
"emailChangeSuccess": "Email changed successfully",
|
||||||
|
"emailChangeSuccessDescription": "Your email has been updated",
|
||||||
|
"emailChangeFailed": "Email change failed",
|
||||||
|
"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.",
|
||||||
|
"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."
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -20,6 +20,8 @@ import {
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
import { useUser } from "@/components/user-provider";
|
import { useUser } from "@/components/user-provider";
|
||||||
|
|
||||||
|
import { ProfileEmailAddress } from "./profile-email-address";
|
||||||
|
|
||||||
export const ProfilePage = () => {
|
export const ProfilePage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
|
@ -78,6 +80,19 @@ export const ProfilePage = () => {
|
||||||
>
|
>
|
||||||
<ProfileSettings />
|
<ProfileSettings />
|
||||||
</SettingsSection>
|
</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 />
|
<hr />
|
||||||
|
|
||||||
<SettingsSection
|
<SettingsSection
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { trpc } from "@/trpc/client";
|
||||||
export const ProfileSettings = () => {
|
export const ProfileSettings = () => {
|
||||||
const { user, refresh } = useUser();
|
const { user, refresh } = useUser();
|
||||||
const changeName = trpc.user.changeName.useMutation();
|
const changeName = trpc.user.changeName.useMutation();
|
||||||
|
|
||||||
const form = useForm<{
|
const form = useForm<{
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
@ -33,9 +34,11 @@ export const ProfileSettings = () => {
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit(async (data) => {
|
onSubmit={handleSubmit(async (data) => {
|
||||||
|
if (data.name !== user.name) {
|
||||||
await changeName.mutateAsync({ name: data.name });
|
await changeName.mutateAsync({ name: data.name });
|
||||||
await refresh();
|
}
|
||||||
reset(data);
|
reset(data);
|
||||||
|
await refresh();
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-y-4">
|
<div className="flex flex-col gap-y-4">
|
||||||
|
@ -54,20 +57,7 @@ export const ProfileSettings = () => {
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="email"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
<Trans i18nKey="email" />
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} disabled={true} />
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div className="mt-4 flex">
|
<div className="mt-4 flex">
|
||||||
<Button
|
<Button
|
||||||
loading={formState.isSubmitting}
|
loading={formState.isSubmitting}
|
||||||
|
|
|
@ -29,6 +29,7 @@ const handler = (request: Request) => {
|
||||||
isGuest: session.user.email === null,
|
isGuest: session.user.email === null,
|
||||||
locale: session.user.locale ?? undefined,
|
locale: session.user.locale ?? undefined,
|
||||||
image: session.user.image ?? undefined,
|
image: session.user.image ?? undefined,
|
||||||
|
email: session.user.email ?? undefined,
|
||||||
getEmailClient: () =>
|
getEmailClient: () =>
|
||||||
getEmailClient(session.user?.locale ?? undefined),
|
getEmailClient(session.user?.locale ?? undefined),
|
||||||
},
|
},
|
||||||
|
|
64
apps/web/src/app/api/user/verify-email-change/route.ts
Normal file
64
apps/web/src/app/api/user/verify-email-change/route.ts
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import { prisma } from "@rallly/database";
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
import type { NextRequest } from "next/server";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
import { getServerSession } from "@/auth";
|
||||||
|
import { decryptToken } from "@/utils/session";
|
||||||
|
|
||||||
|
type EmailChangePayload = {
|
||||||
|
fromEmail: string;
|
||||||
|
toEmail: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const COOKIE_CONFIG = {
|
||||||
|
path: "/",
|
||||||
|
httpOnly: false,
|
||||||
|
secure: false,
|
||||||
|
expires: new Date(Date.now() + 5 * 1000), // 5 seconds
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const setEmailChangeCookie = (
|
||||||
|
type: "success" | "error",
|
||||||
|
value: string = "1",
|
||||||
|
) => {
|
||||||
|
cookies().set(`email-change-${type}`, value, COOKIE_CONFIG);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEmailChange = async (token: string) => {
|
||||||
|
const payload = await decryptToken<EmailChangePayload>(token);
|
||||||
|
|
||||||
|
if (!payload) {
|
||||||
|
setEmailChangeCookie("error", "invalidToken");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.user.update({
|
||||||
|
where: { email: payload.fromEmail },
|
||||||
|
data: { email: payload.toEmail },
|
||||||
|
});
|
||||||
|
|
||||||
|
setEmailChangeCookie("success");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GET = async (request: NextRequest) => {
|
||||||
|
const token = request.nextUrl.searchParams.get("token");
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return NextResponse.json({ error: "No token provided" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await getServerSession();
|
||||||
|
|
||||||
|
if (!session || !session.user.email) {
|
||||||
|
return NextResponse.redirect(
|
||||||
|
new URL(`/login?callbackUrl=${request.url}`, request.url),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await handleEmailChange(token);
|
||||||
|
|
||||||
|
return NextResponse.redirect(new URL("/settings/profile", request.url));
|
||||||
|
};
|
|
@ -7,6 +7,7 @@ export type TRPCContext = {
|
||||||
locale?: string;
|
locale?: string;
|
||||||
getEmailClient: (locale?: string) => EmailClient;
|
getEmailClient: (locale?: string) => EmailClient;
|
||||||
image?: string;
|
image?: string;
|
||||||
|
email?: string;
|
||||||
};
|
};
|
||||||
ip?: string;
|
ip?: string;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import { DeleteObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
|
import { DeleteObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
|
||||||
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
||||||
import { prisma } from "@rallly/database";
|
import { prisma } from "@rallly/database";
|
||||||
|
import { absoluteUrl } from "@rallly/utils/absolute-url";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { waitUntil } from "@vercel/functions";
|
import { waitUntil } from "@vercel/functions";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { env } from "@/env";
|
import { env } from "@/env";
|
||||||
import { getS3Client } from "@/utils/s3";
|
import { getS3Client } from "@/utils/s3";
|
||||||
|
import { createToken } from "@/utils/session";
|
||||||
import { getSubscriptionStatus } from "@/utils/subscription";
|
import { getSubscriptionStatus } from "@/utils/subscription";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -120,6 +122,46 @@ export const user = router({
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}),
|
}),
|
||||||
|
requestEmailChange: privateProcedure
|
||||||
|
.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,
|
||||||
|
toEmail: input.email,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ttl: 60 * 10,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.user.getEmailClient().sendTemplate("ChangeEmailRequest", {
|
||||||
|
to: input.email,
|
||||||
|
props: {
|
||||||
|
verificationUrl: absoluteUrl(
|
||||||
|
`/api/user/verify-email-change?token=${token}`,
|
||||||
|
),
|
||||||
|
fromEmail: ctx.user.email,
|
||||||
|
toEmail: input.email,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true as const };
|
||||||
|
}),
|
||||||
getAvatarUploadUrl: privateProcedure
|
getAvatarUploadUrl: privateProcedure
|
||||||
.use(rateLimitMiddleware)
|
.use(rateLimitMiddleware)
|
||||||
.input(
|
.input(
|
||||||
|
|
|
@ -64,13 +64,22 @@ export const proProcedure = t.procedure.use(
|
||||||
|
|
||||||
export const privateProcedure = t.procedure.use(
|
export const privateProcedure = t.procedure.use(
|
||||||
middleware(async ({ ctx, next }) => {
|
middleware(async ({ ctx, next }) => {
|
||||||
if (ctx.user.isGuest) {
|
const email = ctx.user.email;
|
||||||
|
if (!email) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "Login is required",
|
message: "Login is required",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return next();
|
|
||||||
|
return next({
|
||||||
|
ctx: {
|
||||||
|
user: {
|
||||||
|
...ctx.user,
|
||||||
|
email,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -46,5 +46,12 @@
|
||||||
"common_viewOn": "View on {{domain}}",
|
"common_viewOn": "View on {{domain}}",
|
||||||
"newComment_preview": "Go to your poll to see what they said.",
|
"newComment_preview": "Go to your poll to see what they said.",
|
||||||
"newComment_heading": "New Comment",
|
"newComment_heading": "New Comment",
|
||||||
"newPoll_preview": "Share your participant link to start collecting responses."
|
"newPoll_preview": "Share your participant link to start collecting responses.",
|
||||||
|
"changeEmailRequest_preview": "Please verify your email address",
|
||||||
|
"changeEmailRequest_heading": "Verify Your New Email Address",
|
||||||
|
"changeEmailRequest_text2": "To complete this change, please click the button below:",
|
||||||
|
"changeEmailRequest_button": "Verify Email Address",
|
||||||
|
"changeEmailRequest_subject": "Verify your new email address",
|
||||||
|
"changeEmailRequest_text3": "This link will expire in 10 minutes. If you did not request this change, please contact support.",
|
||||||
|
"changeEmailRequest_text1": "We've received a request to change the email address for your account from <b>{{fromEmail}}</b> to <b>{{toEmail}}</b>."
|
||||||
}
|
}
|
||||||
|
|
13
packages/emails/src/previews/change-email-request.tsx
Normal file
13
packages/emails/src/previews/change-email-request.tsx
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { previewEmailContext } from "../components/email-context";
|
||||||
|
import { ChangeEmailRequest } from "../templates/change-email-request";
|
||||||
|
|
||||||
|
export default function ChangeEmailRequestPreview() {
|
||||||
|
return (
|
||||||
|
<ChangeEmailRequest
|
||||||
|
fromEmail="john@example.com"
|
||||||
|
toEmail="jane@example.com"
|
||||||
|
verificationUrl="https://rallly.co/verify-email-change?token=1234567890"
|
||||||
|
ctx={previewEmailContext}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { ChangeEmailRequest } from "./templates/change-email-request";
|
||||||
import { FinalizeHostEmail } from "./templates/finalized-host";
|
import { FinalizeHostEmail } from "./templates/finalized-host";
|
||||||
import { FinalizeParticipantEmail } from "./templates/finalized-participant";
|
import { FinalizeParticipantEmail } from "./templates/finalized-participant";
|
||||||
import { LoginEmail } from "./templates/login";
|
import { LoginEmail } from "./templates/login";
|
||||||
|
@ -17,6 +18,7 @@ const templates = {
|
||||||
NewParticipantConfirmationEmail,
|
NewParticipantConfirmationEmail,
|
||||||
NewPollEmail,
|
NewPollEmail,
|
||||||
RegisterEmail,
|
RegisterEmail,
|
||||||
|
ChangeEmailRequest,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const emailTemplates = Object.keys(templates) as TemplateName[];
|
export const emailTemplates = Object.keys(templates) as TemplateName[];
|
||||||
|
|
82
packages/emails/src/templates/change-email-request.tsx
Normal file
82
packages/emails/src/templates/change-email-request.tsx
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
import { Section } from "@react-email/section";
|
||||||
|
import { Trans } from "react-i18next/TransWithoutContext";
|
||||||
|
|
||||||
|
import { EmailLayout } from "../components/email-layout";
|
||||||
|
import { Button, Heading, Text } from "../components/styled-components";
|
||||||
|
import type { EmailContext } from "../types";
|
||||||
|
|
||||||
|
interface ChangeEmailRequestProps {
|
||||||
|
ctx: EmailContext;
|
||||||
|
verificationUrl: string;
|
||||||
|
fromEmail: string;
|
||||||
|
toEmail: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChangeEmailRequest = ({
|
||||||
|
ctx,
|
||||||
|
verificationUrl,
|
||||||
|
fromEmail,
|
||||||
|
toEmail,
|
||||||
|
}: ChangeEmailRequestProps) => {
|
||||||
|
return (
|
||||||
|
<EmailLayout
|
||||||
|
ctx={ctx}
|
||||||
|
preview={ctx.t("changeEmailRequest_preview", {
|
||||||
|
ns: "emails",
|
||||||
|
defaultValue: "Please verify your email address",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Heading>
|
||||||
|
{ctx.t("changeEmailRequest_heading", {
|
||||||
|
defaultValue: "Verify Your New Email Address",
|
||||||
|
ns: "emails",
|
||||||
|
})}
|
||||||
|
</Heading>
|
||||||
|
<Text>
|
||||||
|
<Trans
|
||||||
|
i18n={ctx.i18n}
|
||||||
|
t={ctx.t}
|
||||||
|
i18nKey="changeEmailRequest_text1"
|
||||||
|
ns="emails"
|
||||||
|
defaults="We've received a request to change the email address for your account from <b>{{fromEmail}}</b> to <b>{{toEmail}}</b>."
|
||||||
|
values={{ fromEmail, toEmail }}
|
||||||
|
components={{ b: <b /> }}
|
||||||
|
/>
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
{ctx.t("changeEmailRequest_text2", {
|
||||||
|
defaultValue:
|
||||||
|
"To complete this change, please click the button below:",
|
||||||
|
ns: "emails",
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
<Section>
|
||||||
|
<Button href={verificationUrl}>
|
||||||
|
{ctx.t("changeEmailRequest_button", {
|
||||||
|
ns: "emails",
|
||||||
|
defaultValue: "Verify Email Address",
|
||||||
|
})}
|
||||||
|
</Button>
|
||||||
|
</Section>
|
||||||
|
<Text light>
|
||||||
|
{ctx.t("changeEmailRequest_text3", {
|
||||||
|
ns: "emails",
|
||||||
|
defaultValue:
|
||||||
|
"This link will expire in 10 minutes. If you did not request this change, please contact support.",
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
</EmailLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ChangeEmailRequest.getSubject = (
|
||||||
|
_props: ChangeEmailRequestProps,
|
||||||
|
ctx: EmailContext,
|
||||||
|
) => {
|
||||||
|
return ctx.t("changeEmailRequest_subject", {
|
||||||
|
defaultValue: "Verify your new email address",
|
||||||
|
ns: "emails",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChangeEmailRequest;
|
Loading…
Add table
Reference in a new issue