Skip to content

Commit ad2bc99

Browse files
authored
feat: resend email confirmation (#1805)
fixes FTD-2211
1 parent 84128a9 commit ad2bc99

File tree

6 files changed

+149
-43
lines changed

6 files changed

+149
-43
lines changed

@3rdweb-sdk/react/hooks/useApi.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,43 @@ export function useConfirmEmail() {
355355
);
356356
}
357357

358+
export function useResendEmailConfirmation() {
359+
const { user } = useLoggedInUser();
360+
const queryClient = useQueryClient();
361+
362+
return useMutationWithInvalidate(
363+
async () => {
364+
invariant(user?.address, "walletAddress is required");
365+
366+
const res = await fetch(
367+
`${THIRDWEB_API_HOST}/v1/account/resendEmailConfirmation`,
368+
{
369+
method: "POST",
370+
credentials: "include",
371+
headers: {
372+
"Content-Type": "application/json",
373+
},
374+
body: JSON.stringify({}),
375+
},
376+
);
377+
const json = await res.json();
378+
379+
if (json.error) {
380+
throw new Error(json.message);
381+
}
382+
383+
return json.data;
384+
},
385+
{
386+
onSuccess: () => {
387+
return queryClient.invalidateQueries(
388+
accountKeys.me(user?.address as string),
389+
);
390+
},
391+
},
392+
);
393+
}
394+
358395
export function useCreatePaymentMethod() {
359396
const { user } = useLoggedInUser();
360397
const queryClient = useQueryClient();

components/onboarding/Billing.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export const OnboardingBilling: React.FC<OnboardingBillingProps> = ({
7171
<Flex flexDir="column" gap={8}>
7272
<OnboardingTitle
7373
heading="Add a payment method"
74-
description="To continue using thirdweb without interruption after exceeding your Starter plan limits, please add a payment method. Your card will be used for verification, no charges will be made without notice."
74+
description="thirdweb is free to get started with free monthly usage limits. We recommend that you add a payment method so that you can use thirdweb services without interruption after you exceed free usage limits."
7575
/>
7676
<Flex flexDir="column" gap={8}>
7777
{stripePromise && (

components/onboarding/ConfirmEmail.tsx

Lines changed: 91 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
import { useConfirmEmail } from "@3rdweb-sdk/react/hooks/useApi";
2-
import { Button } from "tw-components";
1+
import {
2+
useConfirmEmail,
3+
useResendEmailConfirmation,
4+
} from "@3rdweb-sdk/react/hooks/useApi";
5+
import { Button, Text } from "tw-components";
36
import OtpInput from "react-otp-input";
47
import { useState, ClipboardEvent } from "react";
58
import { Input, Flex } from "@chakra-ui/react";
@@ -12,6 +15,7 @@ import {
1215
import { zodResolver } from "@hookform/resolvers/zod";
1316
import { useErrorHandler } from "contexts/error-handler";
1417
import { useTrack } from "hooks/analytics/useTrack";
18+
import { useTxNotifications } from "hooks/useTxNotifications";
1519

1620
interface OnboardingConfirmEmailProps {
1721
email: string;
@@ -29,6 +33,12 @@ export const OnboardingConfirmEmail: React.FC<OnboardingConfirmEmailProps> = ({
2933
const { onError } = useErrorHandler();
3034
const trackEvent = useTrack();
3135

36+
const { onSuccess: onResendSuccess, onError: onResendError } =
37+
useTxNotifications(
38+
"We've sent you a new email confirmation code.",
39+
"Couldn't send email confirmation. Try later!",
40+
);
41+
3242
const form = useForm<EmailConfirmationValidationSchema>({
3343
resolver: zodResolver(emailConfirmationValidationSchema),
3444
values: {
@@ -37,6 +47,7 @@ export const OnboardingConfirmEmail: React.FC<OnboardingConfirmEmailProps> = ({
3747
});
3848

3949
const mutation = useConfirmEmail();
50+
const resendMutation = useResendEmailConfirmation();
4051

4152
const handleChange = (value: string) => {
4253
setToken(value.toUpperCase());
@@ -84,6 +95,42 @@ export const OnboardingConfirmEmail: React.FC<OnboardingConfirmEmailProps> = ({
8495
});
8596
});
8697

98+
const handleResend = () => {
99+
setSaving(true);
100+
101+
trackEvent({
102+
category: "account",
103+
action: "resendEmailConfirmation",
104+
label: "attempt",
105+
});
106+
107+
resendMutation.mutate(undefined, {
108+
onSuccess: () => {
109+
setSaving(false);
110+
onResendSuccess();
111+
112+
trackEvent({
113+
category: "account",
114+
action: "resendEmailConfirmation",
115+
label: "success",
116+
});
117+
},
118+
onError: (error) => {
119+
onResendError(error);
120+
form.reset();
121+
setToken("");
122+
setSaving(false);
123+
124+
trackEvent({
125+
category: "account",
126+
action: "resendEmailConfirmation",
127+
label: "error",
128+
error,
129+
});
130+
},
131+
});
132+
};
133+
87134
const handlePaste = (e: ClipboardEvent<HTMLDivElement>) => {
88135
const data = e.clipboardData.getData("text");
89136
if (data?.match(/^[A-Z]{6}$/)) {
@@ -106,34 +153,48 @@ export const OnboardingConfirmEmail: React.FC<OnboardingConfirmEmailProps> = ({
106153

107154
<form onSubmit={handleSubmit}>
108155
<Flex gap={8} flexDir="column" w="full">
109-
<OtpInput
110-
shouldAutoFocus
111-
value={token}
112-
onChange={handleChange}
113-
onPaste={handlePaste}
114-
skipDefaultStyles
115-
numInputs={6}
116-
containerStyle={{
117-
display: "flex",
118-
flexDirection: "row",
119-
gap: "12px",
120-
}}
121-
renderInput={(props) => (
122-
<Input
123-
{...props}
124-
w={20}
125-
h={16}
126-
rounded="md"
127-
textAlign="center"
128-
fontSize="larger"
129-
borderColor={
130-
form.getFieldState("confirmationToken", form.formState).error
131-
? "red.500"
132-
: "borderColor"
133-
}
134-
/>
135-
)}
136-
/>
156+
<Flex gap={3} flexDir="column" w="full">
157+
<OtpInput
158+
shouldAutoFocus
159+
value={token}
160+
onChange={handleChange}
161+
onPaste={handlePaste}
162+
skipDefaultStyles
163+
numInputs={6}
164+
containerStyle={{
165+
display: "flex",
166+
flexDirection: "row",
167+
gap: "12px",
168+
}}
169+
renderInput={(props) => (
170+
<Input
171+
{...props}
172+
w={20}
173+
h={16}
174+
rounded="md"
175+
textAlign="center"
176+
fontSize="larger"
177+
borderColor={
178+
form.getFieldState("confirmationToken", form.formState)
179+
.error
180+
? "red.500"
181+
: "borderColor"
182+
}
183+
/>
184+
)}
185+
/>
186+
187+
<Button
188+
size="lg"
189+
fontSize="sm"
190+
variant="link"
191+
onClick={handleResend}
192+
colorScheme="blue"
193+
isDisabled={saving}
194+
>
195+
<Text color="blue.500">Resend code</Text>
196+
</Button>
197+
</Flex>
137198

138199
<Flex flexDir="column" gap={3}>
139200
<Button

components/onboarding/PaymentForm.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
import { useErrorHandler } from "contexts/error-handler";
99
import { useTrack } from "hooks/analytics/useTrack";
1010
import { FormEvent, useState } from "react";
11-
import { Button } from "tw-components";
11+
import { Button, Text } from "tw-components";
1212

1313
interface OnboardingPaymentForm {
1414
onSave: () => void;
@@ -112,14 +112,14 @@ export const OnboardingPaymentForm: React.FC<OnboardingPaymentForm> = ({
112112
Add payment
113113
</Button>
114114
<Button
115-
w="full"
116115
size="lg"
117-
fontSize="md"
118-
variant="outline"
119-
isDisabled={saving}
116+
fontSize="sm"
117+
variant="link"
120118
onClick={onCancel}
119+
isDisabled={saving}
120+
colorScheme="blue"
121121
>
122-
I&apos;ll do this later
122+
<Text color="blue.500">I&apos;ll do this later</Text>
123123
</Button>
124124
</Flex>
125125
)}

components/onboarding/index.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useRouter } from "next/router";
77
import { OnboardingBilling } from "./Billing";
88
import { useTrack } from "hooks/analytics/useTrack";
99
import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
10+
import { RE_INTERNAL_TEST_EMAIL } from "utils/regex";
1011

1112
export const Onboarding: React.FC = () => {
1213
const meQuery = useAccount();
@@ -34,6 +35,11 @@ export const Onboarding: React.FC = () => {
3435
if (state === "onboarding") {
3536
if (email) {
3637
setUpdatedEmail(email);
38+
39+
if (email.match(RE_INTERNAL_TEST_EMAIL)) {
40+
setState("skipped");
41+
return;
42+
}
3743
}
3844
setState("confirming");
3945

@@ -45,11 +51,11 @@ export const Onboarding: React.FC = () => {
4551
},
4652
});
4753
} else if (state === "confirming") {
48-
const newState = ["validPayment", "paymentVerification"].includes(
49-
account.status,
50-
)
51-
? "skipped"
52-
: "billing";
54+
const newState =
55+
["validPayment", "paymentVerification"].includes(account.status) ||
56+
account.onboardSkipped
57+
? "skipped"
58+
: "billing";
5359
setState(newState);
5460

5561
trackEvent({
@@ -78,7 +84,7 @@ export const Onboarding: React.FC = () => {
7884
}
7985

8086
// user hasn't confirmed email
81-
if (!account.emailConfirmedAt) {
87+
if (!account.emailConfirmedAt && !account.unconfirmedEmail) {
8288
setState("onboarding");
8389
}
8490
// user has changed email and needs to confirm

utils/regex.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ export const RE_DOMAIN = new RegExp(
77
);
88

99
export const RE_BUNDLE_ID = new RegExp(/^[a-z0-9.-]{3,64}$/);
10+
11+
export const RE_INTERNAL_TEST_EMAIL = new RegExp(/^\w+\+.+@thirdweb\.com$/);

0 commit comments

Comments
 (0)