@akinon/pz-theme 2.0.95-beta.1 → 2.0.95-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -15
- package/package.json +4 -4
- package/src/__tests__/auth-commerce-events.test.tsx +1 -5
- package/src/__tests__/auth-form-error-block.test.tsx +0 -2
- package/src/__tests__/auth-form-validation.test.tsx +0 -2
- package/src/__tests__/auth-slot-coverage.test.ts +1 -9
- package/src/__tests__/auth-step-title.test.tsx +0 -2
- package/src/__tests__/detached-submit-state.test.tsx +2 -80
- package/src/__tests__/forgot-password-composed.test.tsx +0 -2
- package/src/__tests__/otp-form-composed.test.tsx +0 -2
- package/src/__tests__/password-reset-step.test.tsx +0 -2
- package/src/__tests__/register-form-composed.test.tsx +4 -253
- package/src/__tests__/registry-parity.test.ts +0 -12
- package/src/auth/auth-field-errors.tsx +0 -31
- package/src/blocks/account-button-block.tsx +0 -25
- package/src/blocks/add-to-cart-block.tsx +0 -15
- package/src/blocks/auth-field-block.tsx +3 -13
- package/src/blocks/auth-flow-block.tsx +3 -45
- package/src/blocks/auth-form-block.tsx +54 -337
- package/src/blocks/block-renderer-registry.tsx +0 -8
- package/src/blocks/button-block.tsx +13 -43
- package/src/blocks/change-password-form-block.tsx +0 -15
- package/src/blocks/forgot-password-block.tsx +9 -132
- package/src/blocks/newsletter-form-block.tsx +0 -16
- package/src/blocks/otp-form-block.tsx +67 -161
- package/src/blocks/profile-form-block.tsx +0 -13
- package/src/blocks/register-form-block.tsx +110 -362
- package/src/chrome/__tests__/theme-config-source.test.ts +1 -2
- package/src/chrome/placeholder-slugs.ts +0 -3
- package/src/chrome/route-slug.ts +0 -11
- package/src/index.ts +0 -10
- package/src/overlays/__tests__/modal-registry.test.ts +1 -5
- package/src/overlays/modal-chrome-css.ts +1 -8
- package/src/overlays/modal-preview-host.tsx +2 -7
- package/src/overlays/modal-registry.ts +1 -38
- package/src/overlays/overlay-surface.tsx +0 -23
- package/src/overlays/overlay-tokens.ts +0 -12
- package/src/overlays/theme-modal-mount.tsx +6 -85
- package/src/utils/visibility-rules.ts +0 -114
- package/src/__tests__/auth-captcha.test.tsx +0 -261
- package/src/__tests__/auth-destination.test.tsx +0 -348
- package/src/__tests__/auth-error-reading.test.ts +0 -232
- package/src/__tests__/auth-flow-scope.test.tsx +0 -162
- package/src/__tests__/auth-form-success-block.test.tsx +0 -429
- package/src/__tests__/block-action-audience.test.ts +0 -154
- package/src/__tests__/form-state-composition.test.tsx +0 -484
- package/src/__tests__/modal-login-close.test.tsx +0 -366
- package/src/__tests__/reset-password-composed.test.tsx +0 -385
- package/src/auth/auth-captcha.tsx +0 -125
- package/src/auth/auth-destination.ts +0 -168
- package/src/auth/form-state-context.tsx +0 -170
- package/src/auth/form-success-context.tsx +0 -56
- package/src/auth/read-auth-error.ts +0 -332
- package/src/blocks/auth-success-block.tsx +0 -85
- package/src/blocks/captcha-block.tsx +0 -61
- package/src/blocks/form-state-block.tsx +0 -84
- package/src/blocks/reset-password-block.tsx +0 -717
- package/src/overlays/__tests__/modal-loading.test.tsx +0 -235
- package/src/overlays/modal-host-context.tsx +0 -66
- package/src/overlays/overlay-loading.tsx +0 -144
|
@@ -8,28 +8,12 @@ import {
|
|
|
8
8
|
AuthFieldErrors,
|
|
9
9
|
AuthFieldErrorsProvider,
|
|
10
10
|
clearAuthFieldError,
|
|
11
|
-
collectAuthFieldErrors
|
|
12
|
-
focusFirstAuthFieldError
|
|
11
|
+
collectAuthFieldErrors
|
|
13
12
|
} from '../auth/auth-field-errors';
|
|
14
|
-
import {
|
|
15
|
-
authFormMessage,
|
|
16
|
-
readCommerceError,
|
|
17
|
-
splitByKnownFields
|
|
18
|
-
} from '../auth/read-auth-error';
|
|
19
13
|
import {
|
|
20
14
|
AuthFormErrorProvider,
|
|
21
15
|
containsAuthErrorBlock
|
|
22
16
|
} from '../auth/form-error-context';
|
|
23
|
-
import {
|
|
24
|
-
AuthFormSuccessProvider,
|
|
25
|
-
containsAuthSuccessBlock
|
|
26
|
-
} from '../auth/form-success-context';
|
|
27
|
-
import {
|
|
28
|
-
AuthFormStateProvider,
|
|
29
|
-
AuthFormStateValue,
|
|
30
|
-
containsFormStateBlock,
|
|
31
|
-
showsInFormState
|
|
32
|
-
} from '../auth/form-state-context';
|
|
33
17
|
import {
|
|
34
18
|
clearFormSubmitState,
|
|
35
19
|
publishFormSubmitState
|
|
@@ -40,17 +24,8 @@ import {
|
|
|
40
24
|
setAuthStep
|
|
41
25
|
} from '../auth/auth-flow-bus';
|
|
42
26
|
import { renderComposedChildren } from './render-composed-children';
|
|
43
|
-
import { useHostModalCloser } from '../overlays/modal-host-context';
|
|
44
27
|
import { BlockRendererProps } from './block-renderer-registry';
|
|
45
28
|
|
|
46
|
-
/**
|
|
47
|
-
* The only name this form submits under, and therefore the only one a
|
|
48
|
-
* field-level refusal can be hung on. Named as well as measured, because a
|
|
49
|
-
* composition whose CTA is a detached Button still has to attribute
|
|
50
|
-
* "no account with this address" to the address field.
|
|
51
|
-
*/
|
|
52
|
-
const FORGOT_PASSWORD_FIELD_NAMES = ['email'] as const;
|
|
53
|
-
|
|
54
29
|
/**
|
|
55
30
|
* Functional auth block: e-mail input that triggers the password reset mail
|
|
56
31
|
* through the v1 `forgotPassword` mutation. Behaviour is fixed (commerce sends
|
|
@@ -83,19 +58,6 @@ const ForgotPasswordBlock = ({
|
|
|
83
58
|
getResponsiveValue(block.properties?.[key], currentBreakpoint, fallback)
|
|
84
59
|
);
|
|
85
60
|
|
|
86
|
-
/**
|
|
87
|
-
* Whether finishing closes the modal this block is inside. OFF by default
|
|
88
|
-
* here, because this block's success state is something the shopper is meant
|
|
89
|
-
* to read — see `useHostModalCloser`.
|
|
90
|
-
*/
|
|
91
|
-
const closeHostModalOnSuccess = readToggle(
|
|
92
|
-
block.properties?.closeHostModalOnSuccess,
|
|
93
|
-
currentBreakpoint,
|
|
94
|
-
false
|
|
95
|
-
);
|
|
96
|
-
const closeHostModal = useHostModalCloser(closeHostModalOnSuccess);
|
|
97
|
-
|
|
98
|
-
|
|
99
61
|
const submitLabel = prop('submitLabel', 'Send reset link');
|
|
100
62
|
// Empty on purpose: without a busy label the submit caption stays put while
|
|
101
63
|
// the request runs, which is what published blocks render today.
|
|
@@ -181,21 +143,6 @@ const ForgotPasswordBlock = ({
|
|
|
181
143
|
.filter((childBlock) => (isDesigner ? true : !childBlock.hidden))
|
|
182
144
|
.sort((a, b) => (a.order || 0) - (b.order || 0));
|
|
183
145
|
const isComposed = childBlocks.length > 0;
|
|
184
|
-
/**
|
|
185
|
-
* Whether the composition draws the confirmation itself — either with a Form
|
|
186
|
-
* Success block carrying this form's sentence, or with a whole screen scoped
|
|
187
|
-
* to the success state in a Form State container.
|
|
188
|
-
*
|
|
189
|
-
* Two things follow: the built-in success copy stands down, and the form is
|
|
190
|
-
* NOT replaced by it — a block inside a form that has been swapped out is
|
|
191
|
-
* unmounted and would draw nothing at all, which is the whole reason the
|
|
192
|
-
* confirmation was unmanageable in the first place.
|
|
193
|
-
*/
|
|
194
|
-
const successScreenIsComposed =
|
|
195
|
-
isComposed && containsFormStateBlock(childBlocks, 'success', currentBreakpoint);
|
|
196
|
-
const successIsAuthored =
|
|
197
|
-
successScreenIsComposed ||
|
|
198
|
-
(isComposed && containsAuthSuccessBlock(childBlocks));
|
|
199
146
|
|
|
200
147
|
const [email, setEmail] = useState('');
|
|
201
148
|
const [error, setError] = useState('');
|
|
@@ -238,14 +185,10 @@ const ForgotPasswordBlock = ({
|
|
|
238
185
|
// Submitting inside the designer would send a real reset mail.
|
|
239
186
|
if (isDesigner || isLoading) return;
|
|
240
187
|
|
|
241
|
-
// Held onto because `currentTarget` is only the form for as long as the
|
|
242
|
-
// handler runs, and the refusal is read after an await.
|
|
243
|
-
const formElement = event.currentTarget;
|
|
244
|
-
|
|
245
188
|
// The form carries `noValidate`, so this is what stops an empty submit —
|
|
246
189
|
// and it says so under the field rather than in a browser bubble.
|
|
247
190
|
const { errors: nextFieldErrors, firstInvalid } = collectAuthFieldErrors(
|
|
248
|
-
|
|
191
|
+
event.currentTarget,
|
|
249
192
|
{ required: requiredMessage, invalid: invalidMessage }
|
|
250
193
|
);
|
|
251
194
|
|
|
@@ -261,7 +204,7 @@ const ForgotPasswordBlock = ({
|
|
|
261
204
|
// form the way every other composed auth form reads its fields, rather
|
|
262
205
|
// than from state no child writes to.
|
|
263
206
|
const address = isComposed
|
|
264
|
-
? String(new FormData(
|
|
207
|
+
? String(new FormData(event.currentTarget).get('email') ?? '').trim()
|
|
265
208
|
: email;
|
|
266
209
|
|
|
267
210
|
try {
|
|
@@ -273,28 +216,8 @@ const ForgotPasswordBlock = ({
|
|
|
273
216
|
if (successStep) {
|
|
274
217
|
setAuthStep(successStep.flowId, successStep.stepKey);
|
|
275
218
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
// STEP and also asks to close gets the close, because a step inside a
|
|
279
|
-
// closed drawer is moot. Off by default — "check your inbox" is the
|
|
280
|
-
// point of this form's success state.
|
|
281
|
-
closeHostModal();
|
|
282
|
-
} catch (rejection) {
|
|
283
|
-
/**
|
|
284
|
-
* What commerce actually said, rather than "could not send" over a rate
|
|
285
|
-
* limit the shopper cannot see. RTK Query puts the body on `data`; a
|
|
286
|
-
* message naming the address goes under the field, which is where the
|
|
287
|
-
* shopper is looking, and anything form-level stays above it.
|
|
288
|
-
*/
|
|
289
|
-
const reading = splitByKnownFields(
|
|
290
|
-
readCommerceError((rejection as { data?: unknown })?.data),
|
|
291
|
-
formElement,
|
|
292
|
-
FORGOT_PASSWORD_FIELD_NAMES
|
|
293
|
-
);
|
|
294
|
-
|
|
295
|
-
setFieldErrors((current) => ({ ...current, ...reading.fieldMessages }));
|
|
296
|
-
focusFirstAuthFieldError(formElement, reading.fieldMessages);
|
|
297
|
-
setError(authFormMessage(reading, errorLabel));
|
|
219
|
+
} catch {
|
|
220
|
+
setError(errorLabel);
|
|
298
221
|
}
|
|
299
222
|
};
|
|
300
223
|
|
|
@@ -326,26 +249,9 @@ const ForgotPasswordBlock = ({
|
|
|
326
249
|
</button>
|
|
327
250
|
) : null;
|
|
328
251
|
|
|
329
|
-
/** Finished — for real on a storefront, on the toggle in the canvas. */
|
|
330
|
-
const inSuccessState =
|
|
331
|
-
(isSuccess && !isDesigner) || (isDesigner && previewSuccess);
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* What the composition's Form State containers read. `busy` and `hasError`
|
|
335
|
-
* sit beside the screen rather than replacing it, so a refusal does not take
|
|
336
|
-
* the fields away with it — see `auth/form-state-context.tsx`.
|
|
337
|
-
*/
|
|
338
|
-
const formStateValue: AuthFormStateValue = {
|
|
339
|
-
screen: inSuccessState ? 'success' : 'form',
|
|
340
|
-
busy: isLoading,
|
|
341
|
-
hasError: Boolean(error)
|
|
342
|
-
};
|
|
343
|
-
|
|
344
252
|
// The success copy replaces the form so the shopper is not invited to submit
|
|
345
253
|
// the same address twice; the root attribute is kept so styling still applies.
|
|
346
|
-
|
|
347
|
-
// instead — see `successIsAuthored`.
|
|
348
|
-
if (inSuccessState && !successIsAuthored) {
|
|
254
|
+
if ((isSuccess && !isDesigner) || (isDesigner && previewSuccess)) {
|
|
349
255
|
return (
|
|
350
256
|
<div
|
|
351
257
|
data-forgot-password=""
|
|
@@ -373,10 +279,6 @@ const ForgotPasswordBlock = ({
|
|
|
373
279
|
|
|
374
280
|
return (
|
|
375
281
|
<AuthFormErrorProvider value={error}>
|
|
376
|
-
{/* The confirmation, for the block the composition placed for it. Empty
|
|
377
|
-
until the mail is away, so the block draws nothing before then. */}
|
|
378
|
-
<AuthFormSuccessProvider value={inSuccessState ? successLabel : ''}>
|
|
379
|
-
<AuthFormStateProvider value={formStateValue}>
|
|
380
282
|
<AuthFieldErrorsProvider value={fieldErrors}>
|
|
381
283
|
<form
|
|
382
284
|
id={formHtmlId || undefined}
|
|
@@ -384,9 +286,7 @@ const ForgotPasswordBlock = ({
|
|
|
384
286
|
data-forgot-password-composed=""
|
|
385
287
|
// Fills its block wrapper; see the note in auth-step-block.tsx.
|
|
386
288
|
className="w-full"
|
|
387
|
-
data-forgot-password-state={
|
|
388
|
-
inSuccessState ? 'success' : isLoading ? 'busy' : 'idle'
|
|
389
|
-
}
|
|
289
|
+
data-forgot-password-state={isLoading ? 'busy' : 'idle'}
|
|
390
290
|
{...partAttrs('form')}
|
|
391
291
|
onSubmit={submit}
|
|
392
292
|
// Our own message under the field instead of the browser's bubble
|
|
@@ -401,17 +301,7 @@ const ForgotPasswordBlock = ({
|
|
|
401
301
|
}}
|
|
402
302
|
>
|
|
403
303
|
{renderComposedChildren({
|
|
404
|
-
|
|
405
|
-
// not a container is shared chrome and shows on all of them.
|
|
406
|
-
blocks: childBlocks.filter((childBlock) =>
|
|
407
|
-
showsInFormState({
|
|
408
|
-
block: childBlock,
|
|
409
|
-
state: formStateValue,
|
|
410
|
-
isDesigner,
|
|
411
|
-
selectedBlockId,
|
|
412
|
-
currentBreakpoint
|
|
413
|
-
})
|
|
414
|
-
),
|
|
304
|
+
blocks: childBlocks,
|
|
415
305
|
placeholderId,
|
|
416
306
|
sectionId,
|
|
417
307
|
isDesigner,
|
|
@@ -432,14 +322,7 @@ const ForgotPasswordBlock = ({
|
|
|
432
322
|
</div>
|
|
433
323
|
)}
|
|
434
324
|
|
|
435
|
-
{
|
|
436
|
-
LINE — the address is still valid and the fields are still on
|
|
437
|
-
screen, so pressing it again is a RESEND, the affordance a
|
|
438
|
-
"check your inbox" screen normally wants. Dropped where a Form
|
|
439
|
-
State container owns the success screen: the fields are then
|
|
440
|
-
scoped away with it and this would be an orphan button under a
|
|
441
|
-
composition that never asked for it. */}
|
|
442
|
-
{showSubmit && !(inSuccessState && successScreenIsComposed) && (
|
|
325
|
+
{showSubmit && (
|
|
443
326
|
<button
|
|
444
327
|
type="submit"
|
|
445
328
|
data-forgot-password-submit=""
|
|
@@ -461,14 +344,8 @@ const ForgotPasswordBlock = ({
|
|
|
461
344
|
{isLoading && busyLabel ? busyLabel : submitLabel}
|
|
462
345
|
</button>
|
|
463
346
|
)}
|
|
464
|
-
|
|
465
|
-
{/* The authored way back. Not when a container owns the screen:
|
|
466
|
-
the way onward is then part of the composition. */}
|
|
467
|
-
{inSuccessState && !successScreenIsComposed && backControl}
|
|
468
347
|
</form>
|
|
469
348
|
</AuthFieldErrorsProvider>
|
|
470
|
-
</AuthFormStateProvider>
|
|
471
|
-
</AuthFormSuccessProvider>
|
|
472
349
|
</AuthFormErrorProvider>
|
|
473
350
|
);
|
|
474
351
|
}
|
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
import React, { useState } from 'react';
|
|
4
4
|
import { useEmailSubscriptionMutation } from '@akinon/next/data/client/misc';
|
|
5
5
|
import { getResponsiveValue, partAttrs } from '../utils';
|
|
6
|
-
import { useHostModalCloser } from '../overlays/modal-host-context';
|
|
7
|
-
import { readToggle } from './product-block-utils';
|
|
8
6
|
import { BlockRendererProps } from './block-renderer-registry';
|
|
9
7
|
|
|
10
8
|
/** The wording the block has always shipped with; every label is editable. */
|
|
@@ -39,22 +37,9 @@ const NewsletterFormBlock = ({
|
|
|
39
37
|
currentBreakpoint,
|
|
40
38
|
fallback
|
|
41
39
|
);
|
|
42
|
-
|
|
43
40
|
return value === undefined || value === null ? fallback : String(value);
|
|
44
41
|
};
|
|
45
42
|
|
|
46
|
-
/**
|
|
47
|
-
* Whether finishing closes the modal this block is inside. OFF by default
|
|
48
|
-
* here, because this block's success state is something the shopper is meant
|
|
49
|
-
* to read — see `useHostModalCloser`.
|
|
50
|
-
*/
|
|
51
|
-
const closeHostModalOnSuccess = readToggle(
|
|
52
|
-
block.properties?.closeHostModalOnSuccess,
|
|
53
|
-
currentBreakpoint,
|
|
54
|
-
false
|
|
55
|
-
);
|
|
56
|
-
const closeHostModal = useHostModalCloser(closeHostModalOnSuccess);
|
|
57
|
-
|
|
58
43
|
const placeholder = prop('placeholder', 'Your email address');
|
|
59
44
|
const inputLabel = prop('inputLabel', '');
|
|
60
45
|
const submitLabel = prop('submitLabel', 'Subscribe');
|
|
@@ -86,7 +71,6 @@ const NewsletterFormBlock = ({
|
|
|
86
71
|
// form itself is the consent, so it is always sent as true.
|
|
87
72
|
await subscribe({ email: value, subscribe_contract: true }).unwrap();
|
|
88
73
|
setStatus('success');
|
|
89
|
-
closeHostModal();
|
|
90
74
|
setEmail('');
|
|
91
75
|
} catch {
|
|
92
76
|
setErrorKind('request');
|
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import React, { useContext, useEffect, useRef, useState } from 'react';
|
|
4
|
-
import {
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
5
|
import { signIn } from 'next-auth/react';
|
|
6
6
|
import { useLocalization } from '@akinon/next/hooks';
|
|
7
7
|
import { useOtpLoginMutation } from '@akinon/next/data/client/user';
|
|
8
8
|
import { getResponsiveValue, partAttrs, resolveOption } from '../utils';
|
|
9
9
|
import { readToggle } from './product-block-utils';
|
|
10
10
|
import { closeAllModals } from '../overlays/overlay-bus';
|
|
11
|
-
import {
|
|
12
|
-
useHostModalCloser,
|
|
13
|
-
useModalHostId
|
|
14
|
-
} from '../overlays/modal-host-context';
|
|
15
11
|
import {
|
|
16
12
|
AuthFlowIdContext,
|
|
17
13
|
clearPendingOtp,
|
|
@@ -29,36 +25,51 @@ import {
|
|
|
29
25
|
AuthFormErrorProvider,
|
|
30
26
|
containsAuthErrorBlock
|
|
31
27
|
} from '../auth/form-error-context';
|
|
32
|
-
import {
|
|
33
|
-
AuthFormStateProvider,
|
|
34
|
-
AuthFormStateValue,
|
|
35
|
-
showsInFormState
|
|
36
|
-
} from '../auth/form-state-context';
|
|
37
28
|
import {
|
|
38
29
|
clearFormSubmitState,
|
|
39
30
|
publishFormSubmitState
|
|
40
31
|
} from '../auth/form-submit-bus';
|
|
41
32
|
import { OtpFormApi, OtpFormProvider } from '../auth/otp-form-context';
|
|
42
33
|
import { renderComposedChildren } from './render-composed-children';
|
|
43
|
-
import {
|
|
44
|
-
authFormMessage,
|
|
45
|
-
readAuthError,
|
|
46
|
-
splitByKnownFields
|
|
47
|
-
} from '../auth/read-auth-error';
|
|
48
|
-
import {
|
|
49
|
-
AUTH_AFTER_SIGN_IN_MODES,
|
|
50
|
-
AuthAfterSignIn,
|
|
51
|
-
resolveAuthDestination
|
|
52
|
-
} from '../auth/auth-destination';
|
|
34
|
+
import { BlockRendererProps } from './block-renderer-registry';
|
|
53
35
|
|
|
54
36
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
37
|
+
* pz brands serialise an AuthError[] into next-auth's `error` string, but a
|
|
38
|
+
* plain string is also possible, so both shapes are handled. Kept local rather
|
|
39
|
+
* than imported from auth-form-block, for the same reason register-form keeps
|
|
40
|
+
* its own copy: exporting it would change a shipped module's public surface.
|
|
59
41
|
*/
|
|
60
|
-
const
|
|
61
|
-
|
|
42
|
+
const readAuthError = (error: string | null | undefined): string => {
|
|
43
|
+
if (!error) return '';
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const parsed = JSON.parse(error);
|
|
47
|
+
if (!Array.isArray(parsed)) return String(error);
|
|
48
|
+
|
|
49
|
+
const messages: string[] = [];
|
|
50
|
+
|
|
51
|
+
parsed.forEach((entry) => {
|
|
52
|
+
const data = (entry as { data?: unknown })?.data;
|
|
53
|
+
|
|
54
|
+
if (Array.isArray(data)) {
|
|
55
|
+
data.forEach((item) => {
|
|
56
|
+
if (typeof item === 'string') {
|
|
57
|
+
messages.push(item);
|
|
58
|
+
} else if (item && typeof item === 'object') {
|
|
59
|
+
const value = (item as { value?: unknown }).value;
|
|
60
|
+
if (typeof value === 'string') messages.push(value);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
} else if (typeof data === 'string') {
|
|
64
|
+
messages.push(data);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return messages.join(' ') || String(error);
|
|
69
|
+
} catch {
|
|
70
|
+
return String(error);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
62
73
|
|
|
63
74
|
/**
|
|
64
75
|
* Where the serialised AuthError[] actually lives depends on the next-auth
|
|
@@ -71,6 +82,28 @@ const signInErrorDetail = (
|
|
|
71
82
|
response: { error?: string | null; code?: string | null } | null | undefined
|
|
72
83
|
): string | null | undefined => response?.code || response?.error;
|
|
73
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Whether commerce answered with another SMS-verification challenge — which is
|
|
87
|
+
* what a RESEND of a parked registration is supposed to get back, as opposed to
|
|
88
|
+
* the refusals (a rate limit, an expired attempt) that must reach the shopper.
|
|
89
|
+
* Kept local for the same reason `readAuthError` is: exporting register-form's
|
|
90
|
+
* copy would change a shipped module's public surface.
|
|
91
|
+
*/
|
|
92
|
+
const hasOtpChallenge = (error: string | null | undefined): boolean => {
|
|
93
|
+
if (!error) return false;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const parsed = JSON.parse(error);
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
Array.isArray(parsed) &&
|
|
100
|
+
parsed.some((entry) => (entry as { type?: unknown })?.type === 'otp')
|
|
101
|
+
);
|
|
102
|
+
} catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
74
107
|
/**
|
|
75
108
|
* The OTP endpoint answers a successful send with 2xx and an EMPTY body, which
|
|
76
109
|
* RTK Query rejects as a parse failure. Brands treat that rejection as the
|
|
@@ -141,25 +174,6 @@ const OtpFormBlock = ({
|
|
|
141
174
|
getResponsiveValue(block.properties?.[key], currentBreakpoint, fallback)
|
|
142
175
|
);
|
|
143
176
|
|
|
144
|
-
/**
|
|
145
|
-
* The modal this form is inside, if any. Read before the destination is
|
|
146
|
-
* resolved, because it decides that destination's default: a sign-in inside
|
|
147
|
-
* a drawer is a detour from the page the shopper is on, so `stay` is right
|
|
148
|
-
* there and `fixed` — the historical push — is right on a page.
|
|
149
|
-
*/
|
|
150
|
-
const hostModalId = useModalHostId();
|
|
151
|
-
/**
|
|
152
|
-
* Whether finishing closes the modal this form is inside. On here because a
|
|
153
|
-
* sign-in leaves nothing to read; the forms whose success state IS the point
|
|
154
|
-
* ("check your inbox") default to off. See `useHostModalCloser`.
|
|
155
|
-
*/
|
|
156
|
-
const closeHostModalOnSuccess = readToggle(
|
|
157
|
-
block.properties?.closeHostModalOnSuccess,
|
|
158
|
-
currentBreakpoint,
|
|
159
|
-
true
|
|
160
|
-
);
|
|
161
|
-
const closeHostModal = useHostModalCloser(closeHostModalOnSuccess);
|
|
162
|
-
|
|
163
177
|
const collectPhone = readToggle(
|
|
164
178
|
block.properties?.collectPhone,
|
|
165
179
|
currentBreakpoint,
|
|
@@ -217,24 +231,7 @@ const OtpFormBlock = ({
|
|
|
217
231
|
const providerId = prop('providerId', 'default');
|
|
218
232
|
const otpFormType = prop('otpFormType', 'otpLogin');
|
|
219
233
|
const callbackUrl = prop('callbackUrl', '/');
|
|
220
|
-
/**
|
|
221
|
-
* Where a finished verification leaves the shopper. Same three modes as the
|
|
222
|
-
* Auth Form, and the same default — a phone login inside a drawer wants
|
|
223
|
-
* `stay` just as much as an e-mail one does.
|
|
224
|
-
*/
|
|
225
|
-
const afterSignIn = resolveOption<AuthAfterSignIn>(
|
|
226
|
-
block.properties?.afterSignIn,
|
|
227
|
-
currentBreakpoint,
|
|
228
|
-
'fixed',
|
|
229
|
-
AUTH_AFTER_SIGN_IN_MODES as unknown as AuthAfterSignIn[]
|
|
230
|
-
);
|
|
231
234
|
const errorLabel = prop('errorLabel', 'Verification failed.');
|
|
232
|
-
// Named rather than shown as raw JSON — see auth-form-block for the reason
|
|
233
|
-
// this theme cannot answer the challenge yet.
|
|
234
|
-
const captchaLabel = prop(
|
|
235
|
-
'captchaLabel',
|
|
236
|
-
'Please complete the security check and try again.'
|
|
237
|
-
);
|
|
238
235
|
/**
|
|
239
236
|
* Submitting a half-typed code used to be a silent no-op: the built-in verify
|
|
240
237
|
* button is disabled until the last box is filled, so nothing could reach it.
|
|
@@ -251,17 +248,13 @@ const OtpFormBlock = ({
|
|
|
251
248
|
'codeIncompleteMessage',
|
|
252
249
|
'Enter the {length}-digit code we sent you.'
|
|
253
250
|
);
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
* below handles, without touching any other overlay. Left on, this toggle
|
|
258
|
-
* would take the WIDE branch — every open modal on the page — so a phone
|
|
259
|
-
* login would also close an unrelated mini-basket or quick-view.
|
|
260
|
-
*/
|
|
251
|
+
// Defaults on: the commonest home for this block is a drawer, and a drawer
|
|
252
|
+
// still covering the page the shopper just signed into reads as a hang.
|
|
253
|
+
// closeAllModals is a no-op when nothing is open, so pages pay nothing.
|
|
261
254
|
const closeModalsOnSuccess = readToggle(
|
|
262
255
|
block.properties?.closeModalsOnSuccess,
|
|
263
256
|
currentBreakpoint,
|
|
264
|
-
|
|
257
|
+
true
|
|
265
258
|
);
|
|
266
259
|
// The code phase is unreachable in the canvas without sending a real SMS,
|
|
267
260
|
// so the designer picks the phase to dress — the previewSuccess pattern.
|
|
@@ -273,17 +266,6 @@ const OtpFormBlock = ({
|
|
|
273
266
|
);
|
|
274
267
|
|
|
275
268
|
const router = useRouter();
|
|
276
|
-
const search = useSearchParams();
|
|
277
|
-
const pathname = usePathname();
|
|
278
|
-
const destination = resolveAuthDestination({
|
|
279
|
-
mode: afterSignIn,
|
|
280
|
-
authored: callbackUrl,
|
|
281
|
-
search,
|
|
282
|
-
// A sign-in inside a drawer is a detour: a destination that names nowhere
|
|
283
|
-
// in particular means there is nowhere to go. An authored one still wins.
|
|
284
|
-
inModal: Boolean(hostModalId),
|
|
285
|
-
pathname
|
|
286
|
-
});
|
|
287
269
|
const { locale, defaultLocaleValue } = useLocalization();
|
|
288
270
|
const contextFlowId = useContext(AuthFlowIdContext);
|
|
289
271
|
/**
|
|
@@ -524,48 +506,14 @@ const OtpFormBlock = ({
|
|
|
524
506
|
});
|
|
525
507
|
|
|
526
508
|
if (!response || response.error) {
|
|
527
|
-
|
|
528
|
-
// — so it belongs under the boxes the shopper just typed into.
|
|
529
|
-
const reading = splitByKnownFields(
|
|
530
|
-
readAuthError(signInErrorDetail(response), { captcha: captchaLabel }),
|
|
531
|
-
null,
|
|
532
|
-
OTP_FIELD_NAMES
|
|
533
|
-
);
|
|
534
|
-
|
|
535
|
-
setFieldErrors((current) => ({ ...current, ...reading.fieldMessages }));
|
|
536
|
-
setError(authFormMessage(reading, errorLabel));
|
|
537
|
-
|
|
509
|
+
setError(readAuthError(signInErrorDetail(response)) || errorLabel);
|
|
538
510
|
return;
|
|
539
511
|
}
|
|
540
512
|
|
|
541
513
|
clearPendingOtp(contextFlowId);
|
|
542
|
-
/**
|
|
543
|
-
* The surface this form finished inside.
|
|
544
|
-
*
|
|
545
|
-
* Closing the modal it is IN is the default because it is never a
|
|
546
|
-
* surprise: a shopper who signed in inside the drawer is done with the
|
|
547
|
-
* drawer. `closeModalsOnSuccess` keeps its own, wider meaning — close
|
|
548
|
-
* EVERY open modal — for a composition that authored it, which is what a
|
|
549
|
-
* form nested two overlays deep needs.
|
|
550
|
-
*
|
|
551
|
-
* The live GANT drawer is why the default had to change: its OTP form has
|
|
552
|
-
* the toggle on and its login and register forms do not, so signing in
|
|
553
|
-
* with an e-mail left the drawer covering the page.
|
|
554
|
-
*/
|
|
555
|
-
/**
|
|
556
|
-
* Both, not either: a composition that ticked the legacy toggle asked
|
|
557
|
-
* for "every open modal", and the wide close is a superset — so the
|
|
558
|
-
* narrow one runs unconditionally and simply finds nothing left to do.
|
|
559
|
-
* Sequencing them the other way round (an `else`) would have left every
|
|
560
|
-
* already-published drawer on the wide branch forever, which is the one
|
|
561
|
-
* behaviour this family exists to stop.
|
|
562
|
-
*/
|
|
563
514
|
if (closeModalsOnSuccess) closeAllModals();
|
|
564
|
-
closeHostModal();
|
|
565
515
|
router.refresh();
|
|
566
|
-
|
|
567
|
-
// `stay`, where the refresh above is the whole navigation.
|
|
568
|
-
if (destination) router.push(destination);
|
|
516
|
+
router.push(response.url || callbackUrl);
|
|
569
517
|
} catch {
|
|
570
518
|
setError(errorLabel);
|
|
571
519
|
} finally {
|
|
@@ -602,24 +550,8 @@ const OtpFormBlock = ({
|
|
|
602
550
|
// never sent. (Same shape `submitCode` reads.)
|
|
603
551
|
const detail = signInErrorDetail(response);
|
|
604
552
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
});
|
|
608
|
-
|
|
609
|
-
if (response?.error && !resendReading.hasOtpChallenge) {
|
|
610
|
-
// The rate limit commerce actually reported, rather than "could not
|
|
611
|
-
// send the code" over and over while the shopper retries into a
|
|
612
|
-
// lockout. Field-level detail is folded in: there is no field on
|
|
613
|
-
// screen to hang it under at this point in the journey.
|
|
614
|
-
setError(
|
|
615
|
-
[
|
|
616
|
-
resendReading.formMessage,
|
|
617
|
-
...Object.values(resendReading.fieldMessages)
|
|
618
|
-
]
|
|
619
|
-
.filter(Boolean)
|
|
620
|
-
.join(' ') || sendErrorLabel
|
|
621
|
-
);
|
|
622
|
-
|
|
553
|
+
if (response?.error && !hasOtpChallenge(detail)) {
|
|
554
|
+
setError(readAuthError(detail) || sendErrorLabel);
|
|
623
555
|
return;
|
|
624
556
|
}
|
|
625
557
|
|
|
@@ -740,17 +672,6 @@ const OtpFormBlock = ({
|
|
|
740
672
|
</div>
|
|
741
673
|
) : null;
|
|
742
674
|
|
|
743
|
-
/**
|
|
744
|
-
* What the composition's Form State containers read. A verification ends in
|
|
745
|
-
* a sign-in, so there is no success SCREEN — what can be scoped is the
|
|
746
|
-
* request running and the refusal. See `auth/form-state-context.tsx`.
|
|
747
|
-
*/
|
|
748
|
-
const formStateValue: AuthFormStateValue = {
|
|
749
|
-
screen: 'form',
|
|
750
|
-
busy: isBusy,
|
|
751
|
-
hasError: Boolean(error)
|
|
752
|
-
};
|
|
753
|
-
|
|
754
675
|
/**
|
|
755
676
|
* Which children this phase draws.
|
|
756
677
|
*
|
|
@@ -761,19 +682,6 @@ const OtpFormBlock = ({
|
|
|
761
682
|
* flow applies to its steps.
|
|
762
683
|
*/
|
|
763
684
|
const showsInPhase = (childBlock: BlockRendererProps['block']): boolean => {
|
|
764
|
-
// A state container answers to what the form is DOING rather than to which
|
|
765
|
-
// half it is on — a "sending the code…" panel or a designed refusal box —
|
|
766
|
-
// so it is filtered by state, not by phase.
|
|
767
|
-
if (childBlock.type === 'form-state') {
|
|
768
|
-
return showsInFormState({
|
|
769
|
-
block: childBlock,
|
|
770
|
-
state: formStateValue,
|
|
771
|
-
isDesigner,
|
|
772
|
-
selectedBlockId,
|
|
773
|
-
currentBreakpoint
|
|
774
|
-
});
|
|
775
|
-
}
|
|
776
|
-
|
|
777
685
|
if (childBlock.type !== 'otp-phase') return true;
|
|
778
686
|
|
|
779
687
|
if (
|
|
@@ -865,7 +773,6 @@ const OtpFormBlock = ({
|
|
|
865
773
|
// message is what a Form Error block draws wherever the design put it.
|
|
866
774
|
<OtpFormProvider value={api}>
|
|
867
775
|
<AuthFormErrorProvider value={error}>
|
|
868
|
-
<AuthFormStateProvider value={formStateValue}>
|
|
869
776
|
<AuthFieldErrorsProvider value={fieldErrors}>
|
|
870
777
|
<form
|
|
871
778
|
id={formHtmlId || undefined}
|
|
@@ -895,7 +802,6 @@ const OtpFormBlock = ({
|
|
|
895
802
|
{!errorIsAuthored && errorNode}
|
|
896
803
|
</form>
|
|
897
804
|
</AuthFieldErrorsProvider>
|
|
898
|
-
</AuthFormStateProvider>
|
|
899
805
|
</AuthFormErrorProvider>
|
|
900
806
|
</OtpFormProvider>
|
|
901
807
|
);
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
useUpdateProfileMutation
|
|
8
8
|
} from '@akinon/next/data/client/account';
|
|
9
9
|
import { getResponsiveValue, partAttrs } from '../utils';
|
|
10
|
-
import { useHostModalCloser } from '../overlays/modal-host-context';
|
|
11
10
|
import { BlockRendererProps } from './block-renderer-registry';
|
|
12
11
|
import { DesignerPlaceholder, readToggle } from './product-block-utils';
|
|
13
12
|
|
|
@@ -64,17 +63,6 @@ const ProfileFormBlock = ({
|
|
|
64
63
|
getResponsiveValue(block.properties?.[key], currentBreakpoint, fallback)
|
|
65
64
|
);
|
|
66
65
|
|
|
67
|
-
/**
|
|
68
|
-
* Whether saving closes the modal this form is inside. Off by default: the
|
|
69
|
-
* "Saved" confirmation is the only feedback the shopper gets.
|
|
70
|
-
*/
|
|
71
|
-
const closeHostModalOnSuccess = readToggle(
|
|
72
|
-
block.properties?.closeHostModalOnSuccess,
|
|
73
|
-
currentBreakpoint,
|
|
74
|
-
false
|
|
75
|
-
);
|
|
76
|
-
const closeHostModal = useHostModalCloser(closeHostModalOnSuccess);
|
|
77
|
-
|
|
78
66
|
const submitLabel = text('submitLabel', 'Save changes');
|
|
79
67
|
const submitAlign = text('submitAlign', 'stretch');
|
|
80
68
|
const firstNameLabel = text('firstNameLabel', 'First name');
|
|
@@ -177,7 +165,6 @@ const ProfileFormBlock = ({
|
|
|
177
165
|
}).unwrap();
|
|
178
166
|
|
|
179
167
|
setStatus('saved');
|
|
180
|
-
closeHostModal();
|
|
181
168
|
} catch {
|
|
182
169
|
setStatus('error');
|
|
183
170
|
}
|