@marianmeres/stuic 3.77.0 → 3.78.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/dist/components/Checkout/CheckoutGuestOrLoginForm.svelte +179 -10
- package/dist/components/Checkout/CheckoutGuestOrLoginForm.svelte.d.ts +15 -0
- package/dist/components/Checkout/README.md +78 -0
- package/dist/components/Checkout/_internal/checkout-i18n-defaults.js +39 -0
- package/dist/components/RegisterForm/RegisterForm.svelte +17 -0
- package/package.json +1 -1
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import type { Props as GuestFormProps } from "./CheckoutGuestForm.svelte";
|
|
6
6
|
import type { Props as LoginFormProps } from "./CheckoutLoginForm.svelte";
|
|
7
7
|
import type { Props as LoginFormModalProps } from "../LoginForm/LoginFormModal.svelte";
|
|
8
|
+
import type { Props as LoginOrRegisterFormModalProps } from "../LoginOrRegisterForm/LoginOrRegisterFormModal.svelte";
|
|
9
|
+
import type { LoginOrRegisterFormMode } from "../LoginOrRegisterForm/LoginOrRegisterForm.svelte";
|
|
8
10
|
import type { NotificationsStack } from "../Notifications/notifications-stack.svelte.js";
|
|
9
11
|
|
|
10
12
|
export type FormMode = "guest-only" | "login-only" | "tabbed" | "stacked";
|
|
@@ -41,6 +43,49 @@
|
|
|
41
43
|
| "showRememberMe"
|
|
42
44
|
>;
|
|
43
45
|
|
|
46
|
+
/**
|
|
47
|
+
* When provided (and `formMode === "tabbed"`), clicking the login tab opens
|
|
48
|
+
* a `LoginOrRegisterFormModal` instead of rendering `<CheckoutLoginForm>` inline,
|
|
49
|
+
* giving consumers login + register + verify-OTP in a single modal.
|
|
50
|
+
*
|
|
51
|
+
* Mutually exclusive with `loginModal` — if both are provided,
|
|
52
|
+
* `loginOrRegisterModal` wins.
|
|
53
|
+
*
|
|
54
|
+
* `mode` and `verifyEmail` are forwarded one-way (prop → modal); use
|
|
55
|
+
* `onModeChange` to keep consumer-side state in sync. To programmatically
|
|
56
|
+
* flip into verify mode (e.g., on a `requiresVerification` server response),
|
|
57
|
+
* the consumer updates its own `mode` state and the new value flows down.
|
|
58
|
+
*/
|
|
59
|
+
loginOrRegisterModal?: Pick<
|
|
60
|
+
LoginOrRegisterFormModalProps,
|
|
61
|
+
| "title"
|
|
62
|
+
| "classModal"
|
|
63
|
+
| "classInner"
|
|
64
|
+
| "classForm"
|
|
65
|
+
| "noXClose"
|
|
66
|
+
| "noClickOutsideClose"
|
|
67
|
+
| "onClose"
|
|
68
|
+
| "mode"
|
|
69
|
+
| "verifyEmail"
|
|
70
|
+
| "onLogin"
|
|
71
|
+
| "onRegister"
|
|
72
|
+
| "onVerify"
|
|
73
|
+
| "onResendCode"
|
|
74
|
+
| "onForgotPassword"
|
|
75
|
+
| "onModeChange"
|
|
76
|
+
| "isSubmitting"
|
|
77
|
+
| "loginProps"
|
|
78
|
+
| "registerProps"
|
|
79
|
+
| "verifyProps"
|
|
80
|
+
| "socialLogins"
|
|
81
|
+
| "socialDividerLabel"
|
|
82
|
+
| "footer"
|
|
83
|
+
| "modeSwitcher"
|
|
84
|
+
| "loginModeLabel"
|
|
85
|
+
| "registerModeLabel"
|
|
86
|
+
| "verifyModeLabel"
|
|
87
|
+
>;
|
|
88
|
+
|
|
44
89
|
/** Tab label for the guest form tab. Default from i18n. */
|
|
45
90
|
guestTabLabel?: string;
|
|
46
91
|
|
|
@@ -73,12 +118,15 @@
|
|
|
73
118
|
import CheckoutGuestForm from "./CheckoutGuestForm.svelte";
|
|
74
119
|
import CheckoutLoginForm from "./CheckoutLoginForm.svelte";
|
|
75
120
|
import LoginFormModal from "../LoginForm/LoginFormModal.svelte";
|
|
121
|
+
import LoginOrRegisterFormModal from "../LoginOrRegisterForm/LoginOrRegisterFormModal.svelte";
|
|
76
122
|
import TabbedMenu from "../TabbedMenu/TabbedMenu.svelte";
|
|
77
123
|
import { H, type HLevel } from "../H/index.js";
|
|
78
124
|
import CheckoutSectionHeader from "./CheckoutSectionHeader.svelte";
|
|
79
125
|
|
|
80
|
-
// Map login_form.*
|
|
81
|
-
|
|
126
|
+
// Map login_form.* / register_form.* / email_verify_form.* keys to
|
|
127
|
+
// their checkout.* equivalents so consumers can keep a single i18n prefix.
|
|
128
|
+
const FORM_KEY_MAP: Record<string, string> = {
|
|
129
|
+
// LoginForm
|
|
82
130
|
"login_form.email_label": "checkout.login.email_label",
|
|
83
131
|
"login_form.email_placeholder": "checkout.login.email_placeholder",
|
|
84
132
|
"login_form.password_label": "checkout.login.password_label",
|
|
@@ -93,6 +141,46 @@
|
|
|
93
141
|
"login_form.remember_me": "checkout.login.remember_me",
|
|
94
142
|
"login_form.remember_me_tooltip": "checkout.login.remember_me_tooltip",
|
|
95
143
|
"login_form.modal_title": "checkout.login.modal_title",
|
|
144
|
+
// RegisterForm
|
|
145
|
+
"register_form.email_label": "checkout.register.email_label",
|
|
146
|
+
"register_form.email_placeholder": "checkout.register.email_placeholder",
|
|
147
|
+
"register_form.password_label": "checkout.register.password_label",
|
|
148
|
+
"register_form.password_placeholder": "checkout.register.password_placeholder",
|
|
149
|
+
"register_form.password_confirm_label": "checkout.register.password_confirm_label",
|
|
150
|
+
"register_form.password_confirm_placeholder":
|
|
151
|
+
"checkout.register.password_confirm_placeholder",
|
|
152
|
+
"register_form.submit": "checkout.register.submit",
|
|
153
|
+
"register_form.submitting": "checkout.register.submitting",
|
|
154
|
+
"register_form.email_required": "checkout.register.email_required",
|
|
155
|
+
"register_form.email_invalid": "checkout.register.email_invalid",
|
|
156
|
+
"register_form.password_required": "checkout.register.password_required",
|
|
157
|
+
"register_form.password_too_short": "checkout.register.password_too_short",
|
|
158
|
+
"register_form.password_confirm_required": "checkout.register.password_confirm_required",
|
|
159
|
+
"register_form.password_mismatch": "checkout.register.password_mismatch",
|
|
160
|
+
"register_form.field_required": "checkout.register.field_required",
|
|
161
|
+
"register_form.social_divider": "checkout.register.social_divider",
|
|
162
|
+
"register_form.already_have_account": "checkout.register.already_have_account",
|
|
163
|
+
"register_form.modal_title": "checkout.register.modal_title",
|
|
164
|
+
// EmailVerifyForm
|
|
165
|
+
"email_verify_form.heading": "checkout.verify.heading",
|
|
166
|
+
"email_verify_form.subheading": "checkout.verify.subheading",
|
|
167
|
+
"email_verify_form.submit": "checkout.verify.submit",
|
|
168
|
+
"email_verify_form.submitting": "checkout.verify.submitting",
|
|
169
|
+
"email_verify_form.resend_prompt": "checkout.verify.resend_prompt",
|
|
170
|
+
"email_verify_form.resend": "checkout.verify.resend",
|
|
171
|
+
"email_verify_form.resend_cooldown": "checkout.verify.resend_cooldown",
|
|
172
|
+
"email_verify_form.resent": "checkout.verify.resent",
|
|
173
|
+
"email_verify_form.attempts_remaining": "checkout.verify.attempts_remaining",
|
|
174
|
+
// LoginOrRegisterForm (composite)
|
|
175
|
+
"login_or_register_form.mode_login": "checkout.login_or_register.mode_login",
|
|
176
|
+
"login_or_register_form.mode_register": "checkout.login_or_register.mode_register",
|
|
177
|
+
"login_or_register_form.mode_verify": "checkout.login_or_register.mode_verify",
|
|
178
|
+
"login_or_register_form.social_divider": "checkout.login_or_register.social_divider",
|
|
179
|
+
"login_or_register_form.modal_title_login": "checkout.login_or_register.modal_title_login",
|
|
180
|
+
"login_or_register_form.modal_title_register":
|
|
181
|
+
"checkout.login_or_register.modal_title_register",
|
|
182
|
+
"login_or_register_form.modal_title_verify":
|
|
183
|
+
"checkout.login_or_register.modal_title_verify",
|
|
96
184
|
};
|
|
97
185
|
|
|
98
186
|
let {
|
|
@@ -100,6 +188,7 @@
|
|
|
100
188
|
loginForm,
|
|
101
189
|
formMode = "tabbed",
|
|
102
190
|
loginModal,
|
|
191
|
+
loginOrRegisterModal,
|
|
103
192
|
notifications,
|
|
104
193
|
guestTabLabel,
|
|
105
194
|
loginTabLabel,
|
|
@@ -116,32 +205,75 @@
|
|
|
116
205
|
|
|
117
206
|
let t = $derived(tProp ?? t_default);
|
|
118
207
|
|
|
208
|
+
// `loginOrRegisterModal` wins when both are passed.
|
|
209
|
+
let _useLoginOrRegisterModal = $derived(!!loginOrRegisterModal);
|
|
210
|
+
let _useLoginModal = $derived(!loginOrRegisterModal && !!loginModal);
|
|
211
|
+
|
|
212
|
+
$effect(() => {
|
|
213
|
+
if (loginModal && loginOrRegisterModal) {
|
|
214
|
+
console.warn(
|
|
215
|
+
"[CheckoutGuestOrLoginForm] Both `loginModal` and `loginOrRegisterModal` " +
|
|
216
|
+
"were provided; `loginOrRegisterModal` takes precedence."
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
119
221
|
let loginModalRef: LoginFormModal = $state()!;
|
|
222
|
+
let loginOrRegisterModalRef: LoginOrRegisterFormModal = $state()!;
|
|
120
223
|
|
|
121
|
-
// Adapted t for
|
|
224
|
+
// Adapted t for the modals — maps login_form.* / register_form.* /
|
|
225
|
+
// email_verify_form.* / login_or_register_form.* keys to their checkout.* equivalents.
|
|
122
226
|
let modalT = $derived(
|
|
123
|
-
loginModal
|
|
227
|
+
loginModal || loginOrRegisterModal
|
|
124
228
|
? (
|
|
125
229
|
key: string,
|
|
126
230
|
values?: false | null | undefined | Record<string, string | number>,
|
|
127
231
|
fallback?: string | boolean
|
|
128
|
-
) => t(
|
|
232
|
+
) => t(FORM_KEY_MAP[key] ?? key, values, fallback)
|
|
129
233
|
: undefined
|
|
130
234
|
);
|
|
131
235
|
|
|
236
|
+
// Local mirrors of the bindable LoginOrRegisterFormModal state — kept in sync
|
|
237
|
+
// from the consumer-supplied values via $effect, so consumer-driven updates
|
|
238
|
+
// (e.g., flipping `mode = "verify"` after a `requiresVerification` response)
|
|
239
|
+
// flow down. Modal-driven changes are forwarded back via `onModeChange`.
|
|
240
|
+
let _loroMode: LoginOrRegisterFormMode = $state("login");
|
|
241
|
+
let _loroVerifyEmail = $state("");
|
|
242
|
+
|
|
243
|
+
$effect(() => {
|
|
244
|
+
if (loginOrRegisterModal?.mode !== undefined && loginOrRegisterModal.mode !== _loroMode) {
|
|
245
|
+
_loroMode = loginOrRegisterModal.mode;
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
$effect(() => {
|
|
249
|
+
if (
|
|
250
|
+
loginOrRegisterModal?.verifyEmail !== undefined &&
|
|
251
|
+
loginOrRegisterModal.verifyEmail !== _loroVerifyEmail
|
|
252
|
+
) {
|
|
253
|
+
_loroVerifyEmail = loginOrRegisterModal.verifyEmail;
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
132
257
|
let tabItems = $derived([
|
|
133
258
|
{ id: "guest", label: guestTabLabel ?? t("checkout.guest_or_login.guest_tab") },
|
|
134
259
|
{
|
|
135
260
|
id: "login",
|
|
136
261
|
label: loginTabLabel ?? t("checkout.guest_or_login.login_tab"),
|
|
137
|
-
...(
|
|
262
|
+
...(_useLoginOrRegisterModal
|
|
138
263
|
? {
|
|
139
264
|
onSelect: () => {
|
|
140
|
-
|
|
265
|
+
loginOrRegisterModalRef?.open();
|
|
141
266
|
return false;
|
|
142
267
|
},
|
|
143
268
|
}
|
|
144
|
-
:
|
|
269
|
+
: _useLoginModal
|
|
270
|
+
? {
|
|
271
|
+
onSelect: () => {
|
|
272
|
+
loginModalRef?.open();
|
|
273
|
+
return false;
|
|
274
|
+
},
|
|
275
|
+
}
|
|
276
|
+
: {}),
|
|
145
277
|
},
|
|
146
278
|
]);
|
|
147
279
|
|
|
@@ -186,7 +318,7 @@
|
|
|
186
318
|
<div role="tabpanel">
|
|
187
319
|
<CheckoutGuestForm {...guestForm} {notifications} t={tProp} {unstyled} />
|
|
188
320
|
</div>
|
|
189
|
-
{:else if activeTab === "login" && loginForm}
|
|
321
|
+
{:else if activeTab === "login" && loginForm && !_useLoginOrRegisterModal}
|
|
190
322
|
<div role="tabpanel">
|
|
191
323
|
<CheckoutLoginForm {...loginForm} {notifications} t={tProp} {unstyled} />
|
|
192
324
|
</div>
|
|
@@ -203,7 +335,7 @@
|
|
|
203
335
|
{/if}
|
|
204
336
|
{/if}
|
|
205
337
|
|
|
206
|
-
{#if loginModal && loginForm}
|
|
338
|
+
{#if _useLoginModal && loginModal && loginForm}
|
|
207
339
|
<LoginFormModal
|
|
208
340
|
bind:this={loginModalRef}
|
|
209
341
|
formData={loginForm.formData}
|
|
@@ -225,4 +357,41 @@
|
|
|
225
357
|
{...loginModal}
|
|
226
358
|
/>
|
|
227
359
|
{/if}
|
|
360
|
+
|
|
361
|
+
{#if _useLoginOrRegisterModal && loginOrRegisterModal}
|
|
362
|
+
<LoginOrRegisterFormModal
|
|
363
|
+
bind:this={loginOrRegisterModalRef}
|
|
364
|
+
bind:mode={_loroMode}
|
|
365
|
+
bind:verifyEmail={_loroVerifyEmail}
|
|
366
|
+
onLogin={loginOrRegisterModal.onLogin}
|
|
367
|
+
onRegister={loginOrRegisterModal.onRegister}
|
|
368
|
+
onVerify={loginOrRegisterModal.onVerify}
|
|
369
|
+
onResendCode={loginOrRegisterModal.onResendCode}
|
|
370
|
+
onForgotPassword={loginOrRegisterModal.onForgotPassword}
|
|
371
|
+
onModeChange={(next, prev) => {
|
|
372
|
+
loginOrRegisterModal!.onModeChange?.(next, prev);
|
|
373
|
+
}}
|
|
374
|
+
isSubmitting={loginOrRegisterModal.isSubmitting}
|
|
375
|
+
loginProps={loginOrRegisterModal.loginProps}
|
|
376
|
+
registerProps={loginOrRegisterModal.registerProps}
|
|
377
|
+
verifyProps={loginOrRegisterModal.verifyProps}
|
|
378
|
+
modeSwitcher={loginOrRegisterModal.modeSwitcher}
|
|
379
|
+
loginModeLabel={loginOrRegisterModal.loginModeLabel}
|
|
380
|
+
registerModeLabel={loginOrRegisterModal.registerModeLabel}
|
|
381
|
+
verifyModeLabel={loginOrRegisterModal.verifyModeLabel}
|
|
382
|
+
socialLogins={loginOrRegisterModal.socialLogins}
|
|
383
|
+
socialDividerLabel={loginOrRegisterModal.socialDividerLabel}
|
|
384
|
+
footer={loginOrRegisterModal.footer}
|
|
385
|
+
{notifications}
|
|
386
|
+
title={loginOrRegisterModal.title}
|
|
387
|
+
classModal={loginOrRegisterModal.classModal}
|
|
388
|
+
classInner={loginOrRegisterModal.classInner}
|
|
389
|
+
classForm={loginOrRegisterModal.classForm}
|
|
390
|
+
noXClose={loginOrRegisterModal.noXClose}
|
|
391
|
+
noClickOutsideClose={loginOrRegisterModal.noClickOutsideClose}
|
|
392
|
+
onClose={loginOrRegisterModal.onClose}
|
|
393
|
+
t={modalT}
|
|
394
|
+
{unstyled}
|
|
395
|
+
/>
|
|
396
|
+
{/if}
|
|
228
397
|
</div>
|
|
@@ -4,6 +4,7 @@ import type { TranslateFn } from "../../types.js";
|
|
|
4
4
|
import type { Props as GuestFormProps } from "./CheckoutGuestForm.svelte";
|
|
5
5
|
import type { Props as LoginFormProps } from "./CheckoutLoginForm.svelte";
|
|
6
6
|
import type { Props as LoginFormModalProps } from "../LoginForm/LoginFormModal.svelte";
|
|
7
|
+
import type { Props as LoginOrRegisterFormModalProps } from "../LoginOrRegisterForm/LoginOrRegisterFormModal.svelte";
|
|
7
8
|
import type { NotificationsStack } from "../Notifications/notifications-stack.svelte.js";
|
|
8
9
|
export type FormMode = "guest-only" | "login-only" | "tabbed" | "stacked";
|
|
9
10
|
export interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
|
|
@@ -25,6 +26,20 @@ export interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "children">
|
|
|
25
26
|
* Form-related props are taken from loginForm.
|
|
26
27
|
*/
|
|
27
28
|
loginModal?: Pick<LoginFormModalProps, "title" | "classModal" | "classInner" | "classForm" | "noXClose" | "onClose" | "showRememberMe">;
|
|
29
|
+
/**
|
|
30
|
+
* When provided (and `formMode === "tabbed"`), clicking the login tab opens
|
|
31
|
+
* a `LoginOrRegisterFormModal` instead of rendering `<CheckoutLoginForm>` inline,
|
|
32
|
+
* giving consumers login + register + verify-OTP in a single modal.
|
|
33
|
+
*
|
|
34
|
+
* Mutually exclusive with `loginModal` — if both are provided,
|
|
35
|
+
* `loginOrRegisterModal` wins.
|
|
36
|
+
*
|
|
37
|
+
* `mode` and `verifyEmail` are forwarded one-way (prop → modal); use
|
|
38
|
+
* `onModeChange` to keep consumer-side state in sync. To programmatically
|
|
39
|
+
* flip into verify mode (e.g., on a `requiresVerification` server response),
|
|
40
|
+
* the consumer updates its own `mode` state and the new value flows down.
|
|
41
|
+
*/
|
|
42
|
+
loginOrRegisterModal?: Pick<LoginOrRegisterFormModalProps, "title" | "classModal" | "classInner" | "classForm" | "noXClose" | "noClickOutsideClose" | "onClose" | "mode" | "verifyEmail" | "onLogin" | "onRegister" | "onVerify" | "onResendCode" | "onForgotPassword" | "onModeChange" | "isSubmitting" | "loginProps" | "registerProps" | "verifyProps" | "socialLogins" | "socialDividerLabel" | "footer" | "modeSwitcher" | "loginModeLabel" | "registerModeLabel" | "verifyModeLabel">;
|
|
28
43
|
/** Tab label for the guest form tab. Default from i18n. */
|
|
29
44
|
guestTabLabel?: string;
|
|
30
45
|
/** Tab label for the login form tab. Default from i18n. */
|
|
@@ -139,6 +139,84 @@ Every component accepts an optional `t?: TranslateFn` prop. Sensible English def
|
|
|
139
139
|
|
|
140
140
|
`CheckoutLoginForm` internally bridges `checkout.login.*` keys to the generic `LoginForm` component's `login_form.*` keys, so you only need one consistent prefix.
|
|
141
141
|
|
|
142
|
+
When `CheckoutGuestOrLoginForm` is wired to `LoginOrRegisterFormModal` via the optional `loginOrRegisterModal` prop, the same bridging is applied to `register_form.*` (→ `checkout.register.*`), `email_verify_form.*` (→ `checkout.verify.*`), and `login_or_register_form.*` (→ `checkout.login_or_register.*`) — so the entire login + register + verify flow stays under the `checkout.*` prefix.
|
|
143
|
+
|
|
144
|
+
## Login + register + verify in checkout
|
|
145
|
+
|
|
146
|
+
By default `CheckoutGuestOrLoginForm` in tabbed mode renders an inline `<CheckoutLoginForm>` in the login tab. For apps with self-registration, pass `loginOrRegisterModal` to wire the login tab to a `LoginOrRegisterFormModal` instead — giving you login, register, and post-register OTP verification in a single modal:
|
|
147
|
+
|
|
148
|
+
```svelte
|
|
149
|
+
<script lang="ts">
|
|
150
|
+
import {
|
|
151
|
+
CheckoutGuestOrLoginForm,
|
|
152
|
+
createEmptyCustomerFormData,
|
|
153
|
+
createEmptyLoginFormData,
|
|
154
|
+
type LoginOrRegisterFormMode,
|
|
155
|
+
type LoginFormData,
|
|
156
|
+
type RegisterFormData,
|
|
157
|
+
} from "@marianmeres/stuic";
|
|
158
|
+
|
|
159
|
+
let formData = $state(createEmptyCustomerFormData());
|
|
160
|
+
let loginFormData = $state(createEmptyLoginFormData());
|
|
161
|
+
|
|
162
|
+
let mode = $state<LoginOrRegisterFormMode>("login");
|
|
163
|
+
let verifyEmail = $state("");
|
|
164
|
+
let isSubmitting = $state(false);
|
|
165
|
+
let formError = $state<string | null>(null);
|
|
166
|
+
|
|
167
|
+
const loginProps = $derived({ error: formError ?? undefined, showRememberMe: true });
|
|
168
|
+
const registerProps = $derived({ error: formError ?? undefined });
|
|
169
|
+
const verifyProps = $derived({ error: formError ?? undefined, heading: false as const });
|
|
170
|
+
|
|
171
|
+
async function onLogin(d: LoginFormData) {
|
|
172
|
+
// ... call API; on `requiresVerification`, flip:
|
|
173
|
+
// verifyEmail = d.email; mode = "verify";
|
|
174
|
+
}
|
|
175
|
+
async function onRegister(d: RegisterFormData) {
|
|
176
|
+
// ... call API; on success, flip to verify:
|
|
177
|
+
// verifyEmail = d.email; mode = "verify";
|
|
178
|
+
}
|
|
179
|
+
async function onVerify(code: string) {
|
|
180
|
+
// ... call API; on success, modal closes via consumer-managed state.
|
|
181
|
+
}
|
|
182
|
+
async function onResendCode() { /* ... */ }
|
|
183
|
+
</script>
|
|
184
|
+
|
|
185
|
+
<CheckoutGuestOrLoginForm
|
|
186
|
+
formMode="tabbed"
|
|
187
|
+
guestForm={{ formData, onSubmit: handleStartCheckout, isSubmitting, errors: [] }}
|
|
188
|
+
loginForm={{ formData: loginFormData, onSubmit: onLogin, isSubmitting }}
|
|
189
|
+
loginOrRegisterModal={{
|
|
190
|
+
mode,
|
|
191
|
+
verifyEmail,
|
|
192
|
+
onLogin,
|
|
193
|
+
onRegister,
|
|
194
|
+
onVerify,
|
|
195
|
+
onResendCode,
|
|
196
|
+
onForgotPassword: () => {/* ... */},
|
|
197
|
+
onModeChange: (next) => {
|
|
198
|
+
// mirror mode changes back into our local state and clear errors
|
|
199
|
+
mode = next;
|
|
200
|
+
formError = null;
|
|
201
|
+
},
|
|
202
|
+
isSubmitting,
|
|
203
|
+
loginProps,
|
|
204
|
+
registerProps,
|
|
205
|
+
verifyProps,
|
|
206
|
+
onClose: () => {
|
|
207
|
+
formError = null;
|
|
208
|
+
mode = "login";
|
|
209
|
+
},
|
|
210
|
+
}}
|
|
211
|
+
/>
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**State sync.** `mode` and `verifyEmail` flow one-way from prop into the modal — programmatically flipping `mode = "verify"` (e.g., on a `requiresVerification` server response) updates the modal. To observe modal-driven changes (user clicks the "Sign up" tab, etc.), wire `onModeChange` and update your local state there.
|
|
215
|
+
|
|
216
|
+
**Precedence.** `loginOrRegisterModal` takes precedence over `loginModal`. If both are passed, only `loginOrRegisterModal` is wired up (and a dev-mode `console.warn` fires).
|
|
217
|
+
|
|
218
|
+
**i18n.** All `register_form.*` / `email_verify_form.*` / `login_or_register_form.*` keys are bridged to `checkout.register.*` / `checkout.verify.*` / `checkout.login_or_register.*` respectively, so a single `t` function with a unified `checkout.*` prefix covers the full flow.
|
|
219
|
+
|
|
142
220
|
## Accessibility
|
|
143
221
|
|
|
144
222
|
- `CheckoutProgress` renders past/current/future steps with `aria-current="step"` on the active step.
|
|
@@ -98,6 +98,45 @@ const DEFAULTS = {
|
|
|
98
98
|
"checkout.complete.delivery_label": "Delivery",
|
|
99
99
|
"checkout.complete.totals_title": "Order Total",
|
|
100
100
|
"checkout.complete.continue_shopping": "Continue Shopping",
|
|
101
|
+
// -- Register (mirrors register_form.*; surfaced via CheckoutGuestOrLoginForm
|
|
102
|
+
// when the optional `loginOrRegisterModal` is wired up) --
|
|
103
|
+
"checkout.register.email_label": "Email",
|
|
104
|
+
"checkout.register.email_placeholder": "you@example.com",
|
|
105
|
+
"checkout.register.password_label": "Password",
|
|
106
|
+
"checkout.register.password_placeholder": "",
|
|
107
|
+
"checkout.register.password_confirm_label": "Confirm password",
|
|
108
|
+
"checkout.register.password_confirm_placeholder": "",
|
|
109
|
+
"checkout.register.submit": "Create account",
|
|
110
|
+
"checkout.register.submitting": "Creating account...",
|
|
111
|
+
"checkout.register.email_required": "Email is required",
|
|
112
|
+
"checkout.register.email_invalid": "Please enter a valid email address",
|
|
113
|
+
"checkout.register.password_required": "Password is required",
|
|
114
|
+
"checkout.register.password_too_short": "Password must be at least {min} characters",
|
|
115
|
+
"checkout.register.password_confirm_required": "Please confirm your password",
|
|
116
|
+
"checkout.register.password_mismatch": "Passwords do not match",
|
|
117
|
+
"checkout.register.field_required": "{label} is required",
|
|
118
|
+
"checkout.register.social_divider": "or continue with",
|
|
119
|
+
"checkout.register.already_have_account": "Already have an account?",
|
|
120
|
+
"checkout.register.modal_title": "Create account",
|
|
121
|
+
// -- Verify (mirrors email_verify_form.*; surfaced via CheckoutGuestOrLoginForm
|
|
122
|
+
// when the optional `loginOrRegisterModal` is wired up) --
|
|
123
|
+
"checkout.verify.heading": "Check your email",
|
|
124
|
+
"checkout.verify.subheading": "We sent a 6-digit code to {email}",
|
|
125
|
+
"checkout.verify.submit": "Verify",
|
|
126
|
+
"checkout.verify.submitting": "Verifying...",
|
|
127
|
+
"checkout.verify.resend_prompt": "Didn't receive it?",
|
|
128
|
+
"checkout.verify.resend": "Resend code",
|
|
129
|
+
"checkout.verify.resend_cooldown": "Resend available in {seconds}s",
|
|
130
|
+
"checkout.verify.resent": "New code sent",
|
|
131
|
+
"checkout.verify.attempts_remaining": "{count} attempts remaining",
|
|
132
|
+
// -- LoginOrRegister composite (mirrors login_or_register_form.*) --
|
|
133
|
+
"checkout.login_or_register.mode_login": "Log in",
|
|
134
|
+
"checkout.login_or_register.mode_register": "Sign up",
|
|
135
|
+
"checkout.login_or_register.mode_verify": "Verify",
|
|
136
|
+
"checkout.login_or_register.social_divider": "or continue with",
|
|
137
|
+
"checkout.login_or_register.modal_title_login": "Log In",
|
|
138
|
+
"checkout.login_or_register.modal_title_register": "Create account",
|
|
139
|
+
"checkout.login_or_register.modal_title_verify": "Verify your email",
|
|
101
140
|
// -- CheckoutGuestOrLoginForm (composite) --
|
|
102
141
|
"checkout.guest_or_login.guest_tab": "Guest",
|
|
103
142
|
"checkout.guest_or_login.login_tab": "Log In",
|
|
@@ -95,6 +95,7 @@
|
|
|
95
95
|
</script>
|
|
96
96
|
|
|
97
97
|
<script lang="ts">
|
|
98
|
+
import { untrack } from "svelte";
|
|
98
99
|
import { twMerge } from "../../utils/tw-merge.js";
|
|
99
100
|
import { t_default } from "./_internal/register-form-i18n-defaults.js";
|
|
100
101
|
import {
|
|
@@ -146,6 +147,22 @@
|
|
|
146
147
|
// Internal validation errors (set on submit)
|
|
147
148
|
let internalErrors = $state<RegisterFormValidationError[]>([]);
|
|
148
149
|
|
|
150
|
+
// Clear internal field errors as soon as the user edits any tracked field, so a
|
|
151
|
+
// previous failed-submit's errors don't linger after the user has fixed them.
|
|
152
|
+
// Re-validation on the next submit will repopulate `internalErrors` if anything
|
|
153
|
+
// is still wrong. `untrack` for the read+write so this effect only re-runs on
|
|
154
|
+
// formData changes — otherwise `handleSubmitValid` setting `internalErrors`
|
|
155
|
+
// would immediately re-fire this effect and wipe the errors back out.
|
|
156
|
+
$effect(() => {
|
|
157
|
+
void formData.email;
|
|
158
|
+
void formData.password;
|
|
159
|
+
void formData.passwordConfirm;
|
|
160
|
+
for (const f of extraFields) void formData.extra?.[f.name];
|
|
161
|
+
untrack(() => {
|
|
162
|
+
if (internalErrors.length) internalErrors = [];
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
149
166
|
// Merge internal + external errors; external takes precedence per field
|
|
150
167
|
let allErrors = $derived.by(() => {
|
|
151
168
|
const map = new Map<string, string>();
|