Skip to content

Commit b2d23e2

Browse files
fix(mosaic): hold phone entry while sending the first code
1 parent 927b40d commit b2d23e2

3 files changed

Lines changed: 58 additions & 7 deletions

File tree

‎.changeset/quiet-phones-wait.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
'@clerk/mosaic': patch
33
---
44

5-
Disable Cancel while verifying or resending a phone verification code in `UserProfile`.
5+
Keep the phone entry screen pending until the first verification code is sent in `UserProfile`. Disable Cancel while verifying or resending a phone verification code.

‎packages/mosaic/src/features/user-profile/user-profile-phone-section/__tests__/user-profile-phone-section.feature.test.tsx‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,55 @@ describe('the user profile phone section', () => {
141141
expect(await screen.findByText('Something went wrong. Please try again.')).toBeInTheDocument();
142142
});
143143

144+
it('holds the phone entry step until the first code is sent', async () => {
145+
const { actor, fapi } = await renderSection();
146+
const prepare = holdRequests('post', '/v1/me/phone_numbers/:id/prepare_verification');
147+
148+
try {
149+
await addPhone(actor, '8015550100');
150+
await waitFor(() => expect(prepare.requests).toHaveLength(1));
151+
expect(screen.getByRole('dialog', { name: 'Add phone number' })).toBeVisible();
152+
expect(screen.getByRole('textbox', { name: 'Phone' })).toHaveAttribute('aria-disabled', 'true');
153+
expect(screen.getByRole('button', { name: 'Send code' })).toHaveAttribute('aria-busy', 'true');
154+
expect(screen.getByRole('button', { name: 'Send code' })).toHaveAttribute('aria-disabled', 'true');
155+
expect(screen.queryByRole('textbox', { name: 'Verification code' })).toBeNull();
156+
await actor.keyboard('{Escape}{Enter}');
157+
expect(screen.getByRole('dialog', { name: 'Add phone number' })).toBeVisible();
158+
expect(prepare.requests).toHaveLength(1);
159+
expect(fapi.client.sessions[0]?.user.phone_numbers).toHaveLength(1);
160+
} finally {
161+
prepare.release();
162+
}
163+
164+
await enterCode(actor, VERIFICATION_CODE);
165+
await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull());
166+
expect(listed()).toEqual([HOME_LABEL]);
167+
});
168+
169+
it('recovers from a failed first code without creating the phone again', async () => {
170+
const { actor, fapi } = await renderSection();
171+
const prepare = holdRequests('post', '/v1/me/phone_numbers/:id/prepare_verification');
172+
173+
try {
174+
await addPhone(actor, '8015550100');
175+
await waitFor(() => expect(prepare.requests).toHaveLength(1));
176+
expect(screen.getByRole('dialog', { name: 'Add phone number' })).toBeVisible();
177+
prepare.fail('phone_number_invalid', 'Unable to send a code.');
178+
} finally {
179+
prepare.release();
180+
}
181+
182+
expect(await screen.findByText('Unable to send a code.')).toBeVisible();
183+
expect(screen.getByRole('button', { name: 'Cancel' })).toBeEnabled();
184+
const retryFapi = serveFapi(fapi);
185+
await actor.click(screen.getByRole('button', { name: 'Resend' }));
186+
expect(await screen.findByRole('button', { name: /Resend \(\d+\)/ })).toBeDisabled();
187+
await enterCode(actor, VERIFICATION_CODE);
188+
await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull());
189+
expect(retryFapi.client.sessions[0]?.user.phone_numbers).toHaveLength(1);
190+
expect(listed()).toEqual([HOME_LABEL]);
191+
});
192+
144193
it('keeps the dialog open and says the code was wrong', async () => {
145194
const { actor } = await renderSection();
146195

‎packages/mosaic/src/features/user-profile/user-profile-phone-section/user-profile-add-phone.controller.ts‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ function missingDependency(): Promise<never> {
5353
const tick = { actions: assign(context => ({ resendSeconds: Math.max(0, context.resendSeconds - 1) })) };
5454
const fail = assign<ErrorInvokeEvent>((_, event) => ({ error: toFormError(event.error) }));
5555
const start = {
56-
target: 'sending' as const,
56+
target: 'sendingFirstCode' as const,
5757
actions: assign<Extract<Event, { type: 'START' }>>((_, event) => ({
5858
verifyingId: event.id,
5959
phoneNumber: event.phoneNumber,
@@ -94,6 +94,7 @@ const machine = createMachine({
9494
on: { CANCEL: 'idle', START: start },
9595
},
9696
preparing: sendCode,
97+
sendingFirstCode: sendCode,
9798
sending: sendCode,
9899
verify: {
99100
on: {
@@ -147,9 +148,10 @@ export function useUserProfileAddPhoneController({
147148
}, [open, resendSeconds, send]);
148149

149150
const isReverifying = phoneForm.isSubmitting && Boolean(reverification?.visible);
150-
const step = isReverifying ? 'reverify' : state === 'phone' ? 'phone' : 'verify';
151-
const form = state === 'phone' ? phoneForm : codeForm;
152-
const formError = state === 'phone' ? phoneForm.fields.phoneNumber.feedback : codeForm.fields.code.feedback;
151+
const isPhoneStep = state === 'phone' || state === 'sendingFirstCode';
152+
const step = isReverifying ? 'reverify' : isPhoneStep ? 'phone' : 'verify';
153+
const form = isPhoneStep ? phoneForm : codeForm;
154+
const formError = isPhoneStep ? phoneForm.fields.phoneNumber.feedback : codeForm.fields.code.feedback;
153155
const machineError = error?.global ? errorText(error.global) : undefined;
154156

155157
return {
@@ -159,10 +161,10 @@ export function useUserProfileAddPhoneController({
159161
open,
160162
step,
161163
reverification,
162-
phoneNumber: state === 'phone' ? phoneForm.values.phoneNumber : snapshot.context.phoneNumber,
164+
phoneNumber: isPhoneStep ? phoneForm.values.phoneNumber : snapshot.context.phoneNumber,
163165
code: codeForm.values.code,
164166
errorMessage: formError?.message ?? form.error ?? machineError,
165-
isPending: form.isSubmitting,
167+
isPending: form.isSubmitting || state === 'sendingFirstCode',
166168
onOpenChange: open => {
167169
if (form.isSubmitting) {
168170
if (open || state !== 'phone' || !reverification?.onCancel) {

0 commit comments

Comments
 (0)