Skip to content

Commit 276bdbe

Browse files
fix(mosaic): hold phone entry while sending the first code
1 parent b02d5e7 commit 276bdbe

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
@@ -125,6 +125,55 @@ describe('the user profile phone section', () => {
125125
expect(screen.getByText('+49')).toBeInTheDocument();
126126
});
127127

128+
it('holds the phone entry step until the first code is sent', async () => {
129+
const { actor, fapi } = await renderSection();
130+
const prepare = holdRequests('post', '/v1/me/phone_numbers/:id/prepare_verification');
131+
132+
try {
133+
await addPhone(actor, '8015550100');
134+
await waitFor(() => expect(prepare.requests).toHaveLength(1));
135+
expect(screen.getByRole('dialog', { name: 'Add phone number' })).toBeVisible();
136+
expect(screen.getByRole('textbox', { name: 'Phone' })).toHaveAttribute('aria-disabled', 'true');
137+
expect(screen.getByRole('button', { name: 'Send code' })).toHaveAttribute('aria-busy', 'true');
138+
expect(screen.getByRole('button', { name: 'Send code' })).toHaveAttribute('aria-disabled', 'true');
139+
expect(screen.queryByRole('textbox', { name: 'Verification code' })).toBeNull();
140+
await actor.keyboard('{Escape}{Enter}');
141+
expect(screen.getByRole('dialog', { name: 'Add phone number' })).toBeVisible();
142+
expect(prepare.requests).toHaveLength(1);
143+
expect(fapi.client.sessions[0]?.user.phone_numbers).toHaveLength(1);
144+
} finally {
145+
prepare.release();
146+
}
147+
148+
await enterCode(actor, VERIFICATION_CODE);
149+
await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull());
150+
expect(listed()).toEqual([HOME_LABEL]);
151+
});
152+
153+
it('recovers from a failed first code without creating the phone again', async () => {
154+
const { actor, fapi } = await renderSection();
155+
const prepare = holdRequests('post', '/v1/me/phone_numbers/:id/prepare_verification');
156+
157+
try {
158+
await addPhone(actor, '8015550100');
159+
await waitFor(() => expect(prepare.requests).toHaveLength(1));
160+
expect(screen.getByRole('dialog', { name: 'Add phone number' })).toBeVisible();
161+
prepare.fail('phone_number_invalid', 'Unable to send a code.');
162+
} finally {
163+
prepare.release();
164+
}
165+
166+
expect(await screen.findByText('Unable to send a code.')).toBeVisible();
167+
expect(screen.getByRole('button', { name: 'Cancel' })).toBeEnabled();
168+
const retryFapi = serveFapi(fapi);
169+
await actor.click(screen.getByRole('button', { name: 'Resend' }));
170+
expect(await screen.findByRole('button', { name: /Resend \(\d+\)/ })).toBeDisabled();
171+
await enterCode(actor, VERIFICATION_CODE);
172+
await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull());
173+
expect(retryFapi.client.sessions[0]?.user.phone_numbers).toHaveLength(1);
174+
expect(listed()).toEqual([HOME_LABEL]);
175+
});
176+
128177
it('keeps the dialog open and says the code was wrong', async () => {
129178
const { actor } = await renderSection();
130179

‎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
@@ -54,7 +54,7 @@ const CODE_RESEND_SECONDS = 30;
5454
const tick = { actions: assign(context => ({ resendSeconds: Math.max(0, context.resendSeconds - 1) })) };
5555
const fail = assign<ErrorInvokeEvent>((_, event) => ({ error: toFormError(event.error) }));
5656
const start = {
57-
target: 'sending',
57+
target: 'sendingFirstCode',
5858
actions: assign<Extract<Event, { type: 'START' }>>((_, event) => ({
5959
verifyingId: event.id,
6060
phoneNumber: event.phoneNumber,
@@ -95,6 +95,7 @@ const machine = createMachine({
9595
on: { CANCEL: 'idle', START: start },
9696
},
9797
preparing: sendCode,
98+
sendingFirstCode: sendCode,
9899
sending: sendCode,
99100
verify: {
100101
on: {
@@ -148,9 +149,10 @@ export function useUserProfileAddPhoneController({
148149
}, [open, resendSeconds, send]);
149150

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

156158
return {
@@ -160,10 +162,10 @@ export function useUserProfileAddPhoneController({
160162
open,
161163
step,
162164
reverification,
163-
phoneNumber: state === 'phone' ? phoneForm.values.phoneNumber : snapshot.context.phoneNumber,
165+
phoneNumber: isPhoneStep ? phoneForm.values.phoneNumber : snapshot.context.phoneNumber,
164166
code: codeForm.values.code,
165167
errorMessage: formError?.message ?? form.error ?? machineError,
166-
isPending: form.isSubmitting,
168+
isPending: form.isSubmitting || state === 'sendingFirstCode',
167169
onOpenChange: open => {
168170
if (form.isSubmitting) {
169171
if (open || state !== 'phone' || !reverification?.onCancel) {

0 commit comments

Comments
 (0)