@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.
Files changed (60) hide show
  1. package/CHANGELOG.md +33 -15
  2. package/package.json +4 -4
  3. package/src/__tests__/auth-commerce-events.test.tsx +1 -5
  4. package/src/__tests__/auth-form-error-block.test.tsx +0 -2
  5. package/src/__tests__/auth-form-validation.test.tsx +0 -2
  6. package/src/__tests__/auth-slot-coverage.test.ts +1 -9
  7. package/src/__tests__/auth-step-title.test.tsx +0 -2
  8. package/src/__tests__/detached-submit-state.test.tsx +2 -80
  9. package/src/__tests__/forgot-password-composed.test.tsx +0 -2
  10. package/src/__tests__/otp-form-composed.test.tsx +0 -2
  11. package/src/__tests__/password-reset-step.test.tsx +0 -2
  12. package/src/__tests__/register-form-composed.test.tsx +4 -253
  13. package/src/__tests__/registry-parity.test.ts +0 -12
  14. package/src/auth/auth-field-errors.tsx +0 -31
  15. package/src/blocks/account-button-block.tsx +0 -25
  16. package/src/blocks/add-to-cart-block.tsx +0 -15
  17. package/src/blocks/auth-field-block.tsx +3 -13
  18. package/src/blocks/auth-flow-block.tsx +3 -45
  19. package/src/blocks/auth-form-block.tsx +54 -337
  20. package/src/blocks/block-renderer-registry.tsx +0 -8
  21. package/src/blocks/button-block.tsx +13 -43
  22. package/src/blocks/change-password-form-block.tsx +0 -15
  23. package/src/blocks/forgot-password-block.tsx +9 -132
  24. package/src/blocks/newsletter-form-block.tsx +0 -16
  25. package/src/blocks/otp-form-block.tsx +67 -161
  26. package/src/blocks/profile-form-block.tsx +0 -13
  27. package/src/blocks/register-form-block.tsx +110 -362
  28. package/src/chrome/__tests__/theme-config-source.test.ts +1 -2
  29. package/src/chrome/placeholder-slugs.ts +0 -3
  30. package/src/chrome/route-slug.ts +0 -11
  31. package/src/index.ts +0 -10
  32. package/src/overlays/__tests__/modal-registry.test.ts +1 -5
  33. package/src/overlays/modal-chrome-css.ts +1 -8
  34. package/src/overlays/modal-preview-host.tsx +2 -7
  35. package/src/overlays/modal-registry.ts +1 -38
  36. package/src/overlays/overlay-surface.tsx +0 -23
  37. package/src/overlays/overlay-tokens.ts +0 -12
  38. package/src/overlays/theme-modal-mount.tsx +6 -85
  39. package/src/utils/visibility-rules.ts +0 -114
  40. package/src/__tests__/auth-captcha.test.tsx +0 -261
  41. package/src/__tests__/auth-destination.test.tsx +0 -348
  42. package/src/__tests__/auth-error-reading.test.ts +0 -232
  43. package/src/__tests__/auth-flow-scope.test.tsx +0 -162
  44. package/src/__tests__/auth-form-success-block.test.tsx +0 -429
  45. package/src/__tests__/block-action-audience.test.ts +0 -154
  46. package/src/__tests__/form-state-composition.test.tsx +0 -484
  47. package/src/__tests__/modal-login-close.test.tsx +0 -366
  48. package/src/__tests__/reset-password-composed.test.tsx +0 -385
  49. package/src/auth/auth-captcha.tsx +0 -125
  50. package/src/auth/auth-destination.ts +0 -168
  51. package/src/auth/form-state-context.tsx +0 -170
  52. package/src/auth/form-success-context.tsx +0 -56
  53. package/src/auth/read-auth-error.ts +0 -332
  54. package/src/blocks/auth-success-block.tsx +0 -85
  55. package/src/blocks/captcha-block.tsx +0 -61
  56. package/src/blocks/form-state-block.tsx +0 -84
  57. package/src/blocks/reset-password-block.tsx +0 -717
  58. package/src/overlays/__tests__/modal-loading.test.tsx +0 -235
  59. package/src/overlays/modal-host-context.tsx +0 -66
  60. package/src/overlays/overlay-loading.tsx +0 -144
@@ -1,170 +0,0 @@
1
- 'use client';
2
-
3
- import React from 'react';
4
-
5
- import type { Block } from '../theme-block';
6
- import { getResponsiveValue } from '../utils';
7
-
8
- /**
9
- * WHICH SCREEN a composed form is on, and what is true about it right now.
10
- *
11
- * The forms already composed their normal state — fields, message, CTA, all
12
- * blocks a designer places. Every OTHER state was still the form's own
13
- * markup: one authored sentence for the confirmation, one for a dead link, one
14
- * for "checking", and a spinner nobody could dress. So a brand could build its
15
- * login screen and could not build the screen it turns into.
16
- *
17
- * The two halves are separate on purpose, because a shopper's reality is:
18
- *
19
- * - `screen` is exclusive — the form is being filled, OR it has finished, OR
20
- * the link it needs is dead, OR it is still checking that link;
21
- * - `busy` and `hasError` happen WHILE the form is being filled, so a refusal
22
- * must not take the fields away with it. A single exclusive state would do
23
- * exactly that, which is the bug this shape avoids.
24
- *
25
- * Read by `form-state` blocks — the container a designer scopes a piece of the
26
- * composition to. The form itself decides which of those containers to draw,
27
- * the same way an auth flow decides which of its steps to draw and the OTP
28
- * form which of its phases: a container decides nothing about its own
29
- * visibility, so it stays dressable on its own.
30
- */
31
- export interface AuthFormStateValue {
32
- screen: 'form' | 'success' | 'expired' | 'checking';
33
- busy: boolean;
34
- hasError: boolean;
35
- }
36
-
37
- export const FORM_STATE_KEYS = [
38
- 'form',
39
- 'busy',
40
- 'error',
41
- 'success',
42
- 'expired',
43
- 'checking'
44
- ] as const;
45
-
46
- export type FormStateKey = (typeof FORM_STATE_KEYS)[number];
47
-
48
- const IDLE: AuthFormStateValue = {
49
- screen: 'form',
50
- busy: false,
51
- hasError: false
52
- };
53
-
54
- const AuthFormStateContext = React.createContext<AuthFormStateValue>(IDLE);
55
-
56
- export const AuthFormStateProvider = AuthFormStateContext.Provider;
57
-
58
- /** The state of the form this block is inside; the idle form outside one. */
59
- export const useAuthFormState = (): AuthFormStateValue =>
60
- React.useContext(AuthFormStateContext);
61
-
62
- /**
63
- * Whether a container scoped to `want` shows in `state`.
64
- *
65
- * `form` covers being filled INCLUDING while busy or refused — a container
66
- * holding the fields must not vanish when commerce answers with a message.
67
- * `busy` and `error` are additive to it rather than instead of it.
68
- */
69
- export const matchesFormState = (
70
- want: string,
71
- state: AuthFormStateValue
72
- ): boolean => {
73
- switch (want) {
74
- case 'busy':
75
- return state.screen === 'form' && state.busy;
76
- case 'error':
77
- return state.screen === 'form' && state.hasError;
78
- case 'form':
79
- return state.screen === 'form';
80
- case 'success':
81
- case 'expired':
82
- case 'checking':
83
- return state.screen === want;
84
- // An unrecognised value is a container a newer editor wrote: drawing it
85
- // everywhere would be worse than drawing it nowhere, since it was scoped
86
- // deliberately.
87
- default:
88
- return false;
89
- }
90
- };
91
-
92
- /** The state a container is scoped to, '' when it names none. */
93
- export const readFormStateKey = (
94
- block: Block,
95
- currentBreakpoint = 'desktop'
96
- ): string =>
97
- String(
98
- getResponsiveValue(block.properties?.formState, currentBreakpoint, '')
99
- ).trim();
100
-
101
- /**
102
- * Whether the composition takes over one of the form's OWN screens.
103
- *
104
- * Asked before the form draws its built-in replacement for that screen: a
105
- * designer who has composed the confirmation should not get the form's
106
- * sentence as well, and — since these replacements swap out the whole
107
- * composition — the container has to be mounted at all to draw anything.
108
- *
109
- * Hidden containers do not count, for the same reason a hidden Form Error does
110
- * not: it draws nothing, so standing the built-in copy down would leave the
111
- * shopper with no answer whatsoever.
112
- */
113
- export const containsFormStateBlock = (
114
- blocks: Block[] | undefined,
115
- want: string,
116
- currentBreakpoint = 'desktop'
117
- ): boolean =>
118
- (blocks || []).some(
119
- (block) =>
120
- !block.hidden &&
121
- ((block.type === 'form-state' &&
122
- readFormStateKey(block, currentBreakpoint) === want) ||
123
- containsFormStateBlock(block.blocks, want, currentBreakpoint))
124
- );
125
-
126
- /** Whether a subtree holds the block the designer has selected. */
127
- const containsBlockId = (
128
- blocks: Block[] | undefined,
129
- blockId: string
130
- ): boolean =>
131
- (blocks || []).some(
132
- (candidate) =>
133
- candidate.id === blockId || containsBlockId(candidate.blocks, blockId)
134
- );
135
-
136
- /**
137
- * Which of a form's children to draw in the state it is in.
138
- *
139
- * A child that is not a state container is shared chrome — a logo, a title, a
140
- * support line — and shows on every screen, exactly as a non-phase child of
141
- * the OTP form does. A container shows when its state matches, and
142
- * additionally in the canvas while it holds the selected block, so a designer
143
- * can dress the screen they are not previewing.
144
- */
145
- export const showsInFormState = ({
146
- block,
147
- state,
148
- isDesigner,
149
- selectedBlockId,
150
- currentBreakpoint = 'desktop'
151
- }: {
152
- block: Block;
153
- state: AuthFormStateValue;
154
- isDesigner?: boolean;
155
- selectedBlockId?: string;
156
- currentBreakpoint?: string;
157
- }): boolean => {
158
- if (block.type !== 'form-state') return true;
159
-
160
- if (
161
- isDesigner &&
162
- selectedBlockId &&
163
- (block.id === selectedBlockId ||
164
- containsBlockId(block.blocks, selectedBlockId))
165
- ) {
166
- return true;
167
- }
168
-
169
- return matchesFormState(readFormStateKey(block, currentBreakpoint), state);
170
- };
@@ -1,56 +0,0 @@
1
- 'use client';
2
-
3
- import React from 'react';
4
-
5
- import type { Block } from '../theme-block';
6
-
7
- /**
8
- * The sentence a composed form answers a SUCCESS with.
9
- *
10
- * The mirror of `form-error-context.tsx`, and it exists for the same reason
11
- * turned the other way round: a form-level message belongs to the submission
12
- * rather than to a field, so the form has nowhere good to put it. The error
13
- * half was solved by publishing the message and letting a block draw it; the
14
- * success half was still a node the form drew ITSELF, and in the two blocks
15
- * where it matters — "a reset link is on its way", "your password has been
16
- * changed" — that node REPLACES the whole form. A designer could recolour it
17
- * through its style part and nothing more: no icon beside it, no heading above
18
- * it, no way back except the one built-in link, and no way to move it.
19
- *
20
- * With the message published, a `auth-success` block draws it wherever the
21
- * design puts it, with whatever the design puts around it — because the things
22
- * around it are then ordinary sibling blocks. The form keeps its built-in node
23
- * for every composition that has not placed one, so nothing published changes.
24
- *
25
- * Only the forms that HAVE a success state provide it: a sign-in has none —
26
- * the shopper is in and the screen moves on — which is why the login, register
27
- * and OTP forms are not in this list and the block is not offered inside them.
28
- */
29
- export const AuthFormSuccessContext = React.createContext<string>('');
30
-
31
- export const AuthFormSuccessProvider = AuthFormSuccessContext.Provider;
32
-
33
- /** The form's success message, or '' — outside a form and before it finishes. */
34
- export const useAuthFormSuccess = (): string =>
35
- React.useContext(AuthFormSuccessContext);
36
-
37
- /**
38
- * Whether the blocks a form is ABOUT to draw contain a success block.
39
- *
40
- * Two things hang on the answer, which is why it is asked of the rendered
41
- * subset rather than of every child: the built-in message stands down, and —
42
- * in the forms whose success state replaces the form entirely — the
43
- * composition stays on screen so the block placed inside it is still mounted
44
- * to draw anything at all.
45
- *
46
- * A hidden block draws nothing, so it cannot be the place the message goes:
47
- * counting one would silence the built-in line and leave the shopper with no
48
- * confirmation whatsoever. Same rule, and the same past bug, as
49
- * `containsAuthErrorBlock`.
50
- */
51
- export const containsAuthSuccessBlock = (blocks: Block[] | undefined): boolean =>
52
- (blocks || []).some(
53
- (block) =>
54
- !block.hidden &&
55
- (block.type === 'auth-success' || containsAuthSuccessBlock(block.blocks))
56
- );
@@ -1,332 +0,0 @@
1
- /**
2
- * What a commerce refusal actually says, and which field it says it about.
3
- *
4
- * Every composed auth form used to carry its own copy of this, and all three
5
- * copies made the same mistake: they kept a message only when it was a string.
6
- * Commerce sends field errors as `{ name, value: string[] }` — that is the
7
- * shape `buildFieldErrors` in `@akinon/next/api/auth.ts` serialises — so the
8
- * commonest real refusals ("this e-mail is already registered", "this password
9
- * is too common") extracted to nothing, the empty result fell through to
10
- * `String(error)`, and the shopper was shown the raw
11
- * `[{"type":"field_errors","data":[…]}]`. Because that string is non-empty, the
12
- * designer's authored fallback sentence never fired either, so there was no way
13
- * to author around it.
14
- *
15
- * Two shapes reach a form, and both land here:
16
- *
17
- * - the serialised `AuthError[]` string next-auth hands back (v4 on `error`,
18
- * v5 on `code`), which is what a `signIn()` form reads;
19
- * - the raw commerce body an RTK Query mutation rejects with — `{ email:
20
- * ['…'], non_field_errors: ['…'] }` — which is what the forgot-password
21
- * form gets, and used to discard entirely.
22
- *
23
- * The reading separates what belongs to the form from what belongs to a field,
24
- * because those go to different places on screen: the form-level sentence to a
25
- * Form Error block, the field-level ones under the inputs, through the same
26
- * `AuthFieldErrorsProvider` client validation already writes to.
27
- */
28
-
29
- /** Nothing was wrong, or nothing legible was said about it. */
30
- const EMPTY_READING = {
31
- formMessage: '',
32
- fieldMessages: {} as Readonly<Record<string, string>>,
33
- hasOtpChallenge: false,
34
- hasCaptchaChallenge: false
35
- };
36
-
37
- export interface AuthErrorReading {
38
- /** The sentence for the form-level slot. '' when every message found a field. */
39
- formMessage: string;
40
- /** One sentence per submitted control name, ready for the errors provider. */
41
- fieldMessages: Readonly<Record<string, string>>;
42
- /**
43
- * Commerce accepted the credentials and wants an SMS code. A HANDOFF, not a
44
- * failure — the register form parks its payload and an `otp-form` finishes
45
- * the journey — which is why it is reported separately from the messages.
46
- */
47
- hasOtpChallenge: boolean;
48
- /**
49
- * Commerce escalated to a captcha after too many attempts. Also carries no
50
- * message of its own, and nothing in this theme can answer it yet, so a form
51
- * reports it as a sentence rather than pretending the submit merely failed.
52
- */
53
- hasCaptchaChallenge: boolean;
54
- }
55
-
56
- /** Copy for the two challenges that arrive with no words in them. */
57
- export interface AuthErrorCopy {
58
- otp?: string;
59
- captcha?: string;
60
- }
61
-
62
- /**
63
- * Every sentence inside one value, whatever shape it arrived in.
64
- *
65
- * A commerce field error is `string[]`; a `non_field_errors` payload is
66
- * sometimes a bare string; a brand's middleware occasionally re-encodes the
67
- * array as a JSON string. All three are the same thing to a shopper, so all
68
- * three are flattened here rather than in three call sites.
69
- */
70
- const messagesOf = (value: unknown): string[] => {
71
- if (typeof value === 'string') {
72
- const text = value.trim();
73
- if (!text) return [];
74
-
75
- // A JSON-encoded array is unwrapped, but only when it really is one:
76
- // `JSON.parse` would also happily turn "42" into a number and an ordinary
77
- // sentence into a throw.
78
- if (text.startsWith('[') || text.startsWith('{')) {
79
- try {
80
- return messagesOf(JSON.parse(text));
81
- } catch {
82
- return [text];
83
- }
84
- }
85
-
86
- return [text];
87
- }
88
-
89
- if (Array.isArray(value)) {
90
- return value.flatMap((item) => messagesOf(item));
91
- }
92
-
93
- if (value && typeof value === 'object') {
94
- const record = value as Record<string, unknown>;
95
-
96
- // `{ name, value }` is the field-error entry; `{ message }` and
97
- // `{ detail }` are what a brand's own error middleware tends to send.
98
- return messagesOf(record.value ?? record.message ?? record.detail);
99
- }
100
-
101
- return [];
102
- };
103
-
104
- /** A field's messages, appended rather than replacing what is already there. */
105
- const addFieldMessage = (
106
- fieldMessages: Record<string, string>,
107
- name: string,
108
- messages: string[]
109
- ) => {
110
- const text = messages.join(' ');
111
- if (!text) return;
112
-
113
- fieldMessages[name] = fieldMessages[name]
114
- ? `${fieldMessages[name]} ${text}`
115
- : text;
116
- };
117
-
118
- /**
119
- * The serialised `AuthError[]` a next-auth credentials refusal carries.
120
- *
121
- * An unknown entry type keeps its messages at form level rather than being
122
- * dropped: a brand that added its own error type must still be able to tell
123
- * the shopper something, and that is what every previous copy of this did.
124
- */
125
- export const readAuthError = (
126
- error: string | null | undefined,
127
- copy: AuthErrorCopy = {}
128
- ): AuthErrorReading => {
129
- if (!error) return EMPTY_READING;
130
-
131
- let parsed: unknown;
132
-
133
- try {
134
- parsed = JSON.parse(error);
135
- } catch {
136
- return { ...EMPTY_READING, formMessage: String(error) };
137
- }
138
-
139
- if (!Array.isArray(parsed)) {
140
- return { ...EMPTY_READING, formMessage: String(error) };
141
- }
142
-
143
- const formMessages: string[] = [];
144
- const fieldMessages: Record<string, string> = {};
145
- let hasOtpChallenge = false;
146
- let hasCaptchaChallenge = false;
147
-
148
- parsed.forEach((entry) => {
149
- const type = (entry as { type?: unknown })?.type;
150
- const data = (entry as { data?: unknown })?.data;
151
-
152
- if (type === 'otp') {
153
- hasOtpChallenge = true;
154
- if (copy.otp) formMessages.push(copy.otp);
155
-
156
- return;
157
- }
158
-
159
- if (type === 'captcha') {
160
- hasCaptchaChallenge = true;
161
- if (copy.captcha) formMessages.push(copy.captcha);
162
-
163
- return;
164
- }
165
-
166
- if (type === 'field_errors' && Array.isArray(data)) {
167
- data.forEach((item) => {
168
- const name = (item as { name?: unknown })?.name;
169
- const messages = messagesOf((item as { value?: unknown })?.value);
170
-
171
- // `non_field_errors` is filtered out upstream, but a brand's own
172
- // serialiser may still put it in the list — it is not a field, so it
173
- // must not be hung under one.
174
- if (typeof name !== 'string' || !name || name === 'non_field_errors') {
175
- formMessages.push(...messages);
176
-
177
- return;
178
- }
179
-
180
- addFieldMessage(fieldMessages, name, messages);
181
- });
182
-
183
- return;
184
- }
185
-
186
- formMessages.push(...messagesOf(data));
187
- });
188
-
189
- const formMessage = formMessages.join(' ');
190
-
191
- // Nothing legible AND nothing attributed: show the raw detail rather than an
192
- // empty error slot, which is what the copies this replaces did. A challenge
193
- // is not "nothing" — the form has its own answer for those.
194
- if (
195
- !formMessage &&
196
- Object.keys(fieldMessages).length === 0 &&
197
- !hasOtpChallenge &&
198
- !hasCaptchaChallenge
199
- ) {
200
- return { ...EMPTY_READING, formMessage: String(error) };
201
- }
202
-
203
- return { formMessage, fieldMessages, hasOtpChallenge, hasCaptchaChallenge };
204
- };
205
-
206
- /**
207
- * The raw commerce body an RTK Query mutation rejects with — `error.data`.
208
- *
209
- * Same reading, different envelope: keys are field names, `non_field_errors`
210
- * and `detail` are form-level, and a `redirect_url` naming captcha is the same
211
- * escalation the credentials provider reports as `{ type: 'captcha' }` (see
212
- * `buildFieldErrors`).
213
- */
214
- export const readCommerceError = (
215
- payload: unknown,
216
- copy: AuthErrorCopy = {}
217
- ): AuthErrorReading => {
218
- if (!payload) return EMPTY_READING;
219
-
220
- if (typeof payload === 'string') return readAuthError(payload, copy);
221
-
222
- if (typeof payload !== 'object' || Array.isArray(payload)) {
223
- return { ...EMPTY_READING, formMessage: messagesOf(payload).join(' ') };
224
- }
225
-
226
- const record = payload as Record<string, unknown>;
227
- const formMessages: string[] = [];
228
- const fieldMessages: Record<string, string> = {};
229
- let hasCaptchaChallenge = false;
230
-
231
- Object.entries(record).forEach(([name, value]) => {
232
- if (name === 'redirect_url') {
233
- if (typeof value === 'string' && value.includes('captcha')) {
234
- hasCaptchaChallenge = true;
235
- if (copy.captcha) formMessages.push(copy.captcha);
236
- }
237
-
238
- return;
239
- }
240
-
241
- if (name === 'non_field_errors' || name === 'detail' || name === 'error') {
242
- formMessages.push(...messagesOf(value));
243
-
244
- return;
245
- }
246
-
247
- addFieldMessage(fieldMessages, name, messagesOf(value));
248
- });
249
-
250
- return {
251
- formMessage: formMessages.join(' '),
252
- fieldMessages,
253
- hasOtpChallenge: false,
254
- hasCaptchaChallenge
255
- };
256
- };
257
-
258
- /**
259
- * The reading, re-split against the form that is actually on screen.
260
- *
261
- * A message hung on a name no control submits under would be a message the
262
- * shopper never sees — commerce names a field this composition did not author
263
- * (a brand-specific `tckn`, a field the designer removed), and the sentence
264
- * would vanish into a provider nothing reads. Those move up to the form-level
265
- * slot, which is the one place that always exists.
266
- *
267
- * Called with no form (the fixed-row path measures nothing) everything stays
268
- * where the reading put it: those rows render `fieldErrors[name]` for their own
269
- * fixed names, so the attribution is already right.
270
- */
271
- export const splitByKnownFields = (
272
- reading: AuthErrorReading,
273
- form: HTMLFormElement | null | undefined,
274
- knownNames?: readonly string[]
275
- ): AuthErrorReading => {
276
- const names = Object.keys(reading.fieldMessages);
277
- if (names.length === 0) return reading;
278
-
279
- const known = new Set<string>(knownNames || []);
280
-
281
- if (form) {
282
- form
283
- .querySelectorAll<HTMLElement>('input[name], select[name], textarea[name]')
284
- .forEach((control) => {
285
- const name = control.getAttribute('name');
286
- if (name) known.add(name);
287
- });
288
- }
289
-
290
- if (known.size === 0) return reading;
291
-
292
- const kept: Record<string, string> = {};
293
- const orphaned: string[] = [];
294
-
295
- names.forEach((name) => {
296
- if (known.has(name)) {
297
- kept[name] = reading.fieldMessages[name];
298
-
299
- return;
300
- }
301
-
302
- orphaned.push(reading.fieldMessages[name]);
303
- });
304
-
305
- if (orphaned.length === 0) return reading;
306
-
307
- return {
308
- ...reading,
309
- formMessage: [reading.formMessage, ...orphaned]
310
- .filter(Boolean)
311
- .join(' ')
312
- .trim(),
313
- fieldMessages: kept
314
- };
315
- };
316
-
317
- /**
318
- * What a form shows in its form-level slot after a refusal.
319
- *
320
- * The authored fallback is for a refusal nobody could read — NOT for one whose
321
- * every sentence is already under a field, where a second generic line above
322
- * the inputs would only add noise.
323
- */
324
- export const authFormMessage = (
325
- reading: AuthErrorReading,
326
- fallback: string
327
- ): string => {
328
- if (reading.formMessage) return reading.formMessage;
329
- if (Object.keys(reading.fieldMessages).length > 0) return '';
330
-
331
- return fallback;
332
- };
@@ -1,85 +0,0 @@
1
- 'use client';
2
-
3
- import React from 'react';
4
-
5
- import { useAuthFormSuccess } from '../auth/form-success-context';
6
- import { getResponsiveValue, partAttrs } from '../utils';
7
- import { BlockRendererProps } from './block-renderer-registry';
8
- import { DesignerPlaceholder } from './product-block-utils';
9
-
10
- /**
11
- * The confirmation the form around this block finished with.
12
- *
13
- * The success twin of the Form Error block, and the reason it exists is the
14
- * same complaint: "the success message is not something I can manage". It was
15
- * a node the form drew itself, reachable only as a style part — a brand could
16
- * recolour "a reset link is on its way" and could not put an icon beside it, a
17
- * heading above it, or the sentence anywhere other than where the form chose.
18
- *
19
- * As a block it is ordinary content, so everything a design wants AROUND the
20
- * confirmation is an ordinary sibling. Placing it also keeps the composition on
21
- * screen in the forms whose success state would otherwise replace it — which is
22
- * the point: a block inside a form that has been swapped out draws nothing.
23
- * When the whole screen should change instead, the form's Step After Success is
24
- * still the tool for that, and it needs no block at all.
25
- *
26
- * Empty until the form has finished, so a shopper never sees a reserved gap. In
27
- * the canvas it draws `previewMessage` instead, so the state can be dressed
28
- * without having to reach it for real.
29
- *
30
- * Properties: previewMessage.
31
- * Parts: root.
32
- */
33
- const AuthSuccessBlock = ({
34
- block,
35
- isDesigner = false,
36
- currentBreakpoint = 'desktop'
37
- }: BlockRendererProps) => {
38
- const message = useAuthFormSuccess();
39
-
40
- const previewMessage = String(
41
- getResponsiveValue(
42
- block.properties?.previewMessage,
43
- currentBreakpoint,
44
- 'Done. Check your inbox.'
45
- )
46
- ).trim();
47
-
48
- // The canvas shows the state being styled; a storefront shows nothing until
49
- // the form has actually finished. An author who empties the preview text gets
50
- // the placeholder rather than an invisible block they cannot select.
51
- const shown = message || (isDesigner ? previewMessage : '');
52
-
53
- if (!shown) {
54
- if (!isDesigner) return null;
55
-
56
- return (
57
- <DesignerPlaceholder label="Form success (shown once the form finishes)" />
58
- );
59
- }
60
-
61
- return (
62
- <div
63
- data-auth-success=""
64
- // Fills its block wrapper; see the note in auth-step-block.tsx.
65
- className="w-full"
66
- // Only the live message is announced. `role="status"` on the canvas
67
- // preview would have a designer's screen reader read a confirmation for
68
- // something that has not happened.
69
- role={message ? 'status' : undefined}
70
- data-auth-success-state={message ? 'success' : 'preview'}
71
- {...partAttrs('root')}
72
- // No inline colour, unlike its error twin: that block sets one because
73
- // the auth forms paint their message through a variable a brand may
74
- // already have set, while a confirmation has never had a colour of its
75
- // own. So this line inherits the composition's text colour, and the
76
- // block's own Text Color — emitted `!important` as every part style is —
77
- // is what overrides it.
78
- style={{ margin: 0 }}
79
- >
80
- {shown}
81
- </div>
82
- );
83
- };
84
-
85
- export default AuthSuccessBlock;
@@ -1,61 +0,0 @@
1
- 'use client';
2
-
3
- import React from 'react';
4
- import { getResponsiveValue, partAttrs } from '../utils';
5
- import { useAuthCaptcha } from '../auth/auth-captcha';
6
- import { DesignerPlaceholder } from './product-block-utils';
7
- import { BlockRendererProps } from './block-renderer-registry';
8
-
9
- /**
10
- * Where the security check appears inside an auth form.
11
- *
12
- * The challenge itself is not a design decision — commerce decides when to
13
- * escalate, and the widget is Google's — but WHERE it lands is: above the
14
- * submit, under the password, inside the drawer's scrolling area rather than
15
- * behind its bottom bar. So it is a block, and the form it sits in offers the
16
- * widget through `auth/auth-captcha.tsx`.
17
- *
18
- * Renders nothing at all until there is something to render: the form has
19
- * `showCaptcha` off, or commerce has not escalated. That is what lets a
20
- * designer place it once and forget it, instead of composing a second version
21
- * of the form for the abuse-protected case.
22
- *
23
- * Properties: label. Parts: captcha.
24
- */
25
- const CaptchaBlock = ({
26
- block,
27
- currentBreakpoint = 'desktop',
28
- isDesigner = false
29
- }: BlockRendererProps) => {
30
- const captcha = useAuthCaptcha();
31
-
32
- const label = String(
33
- getResponsiveValue(
34
- block.properties?.label,
35
- currentBreakpoint,
36
- 'Security check'
37
- )
38
- );
39
-
40
- /**
41
- * In the canvas it is a marker rather than a live widget. Two reasons, and
42
- * the first is enough on its own: rendering reCAPTCHA into an editor iframe
43
- * asks Google for a token against the wrong origin. The second is that a
44
- * designer has to be able to SEE and select a block whose whole point is to
45
- * be invisible most of the time.
46
- */
47
- if (isDesigner) {
48
- return <DesignerPlaceholder label={label} attribute="data-auth-captcha" />;
49
- }
50
-
51
- // No form is offering a challenge — see the note above.
52
- if (!captcha) return null;
53
-
54
- return (
55
- <div {...partAttrs('captcha')} data-auth-captcha="">
56
- <captcha.View />
57
- </div>
58
- );
59
- };
60
-
61
- export default CaptchaBlock;