@marianmeres/stuic 3.77.1 → 3.79.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 +189 -35
- package/dist/components/Checkout/CheckoutGuestOrLoginForm.svelte.d.ts +15 -0
- package/dist/components/Checkout/README.md +80 -2
- package/dist/components/Checkout/_guest-or-login-form.css +1 -26
- package/dist/components/Checkout/_internal/checkout-i18n-defaults.js +39 -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";
|
|
76
|
-
import
|
|
121
|
+
import LoginOrRegisterFormModal from "../LoginOrRegisterForm/LoginOrRegisterFormModal.svelte";
|
|
122
|
+
import ButtonGroupRadio from "../ButtonGroupRadio/ButtonGroupRadio.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,33 +205,58 @@
|
|
|
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
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
+
|
|
257
|
+
let tabOptions = $derived([
|
|
258
|
+
{ value: "guest", label: guestTabLabel ?? t("checkout.guest_or_login.guest_tab") },
|
|
259
|
+
{ value: "login", label: loginTabLabel ?? t("checkout.guest_or_login.login_tab") },
|
|
146
260
|
]);
|
|
147
261
|
|
|
148
262
|
let _class = $derived(
|
|
@@ -173,23 +287,26 @@
|
|
|
173
287
|
<CheckoutLoginForm {...loginForm} {notifications} t={tProp} {unstyled} />
|
|
174
288
|
{/if}
|
|
175
289
|
{:else if formMode === "tabbed"}
|
|
176
|
-
<
|
|
177
|
-
|
|
178
|
-
value={activeTab}
|
|
179
|
-
onSelect={(item) => {
|
|
180
|
-
activeTab = item.id as "guest" | "login";
|
|
181
|
-
}}
|
|
182
|
-
{unstyled}
|
|
290
|
+
<ButtonGroupRadio
|
|
291
|
+
options={tabOptions}
|
|
292
|
+
bind:value={activeTab as string}
|
|
183
293
|
class={unstyled ? undefined : "stuic-checkout-guest-or-login-tabs"}
|
|
294
|
+
onButtonClick={(index) => {
|
|
295
|
+
if (tabOptions[index]?.value !== "login") return;
|
|
296
|
+
if (_useLoginOrRegisterModal) {
|
|
297
|
+
loginOrRegisterModalRef?.open();
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
if (_useLoginModal) {
|
|
301
|
+
loginModalRef?.open();
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
}}
|
|
184
305
|
/>
|
|
185
306
|
{#if activeTab === "guest" && guestForm}
|
|
186
|
-
<
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
{:else if activeTab === "login" && loginForm}
|
|
190
|
-
<div role="tabpanel">
|
|
191
|
-
<CheckoutLoginForm {...loginForm} {notifications} t={tProp} {unstyled} />
|
|
192
|
-
</div>
|
|
307
|
+
<CheckoutGuestForm {...guestForm} {notifications} t={tProp} {unstyled} />
|
|
308
|
+
{:else if activeTab === "login" && loginForm && !_useLoginOrRegisterModal && !_useLoginModal}
|
|
309
|
+
<CheckoutLoginForm {...loginForm} {notifications} t={tProp} {unstyled} />
|
|
193
310
|
{/if}
|
|
194
311
|
{:else if formMode === "stacked"}
|
|
195
312
|
{#if loginForm}
|
|
@@ -203,7 +320,7 @@
|
|
|
203
320
|
{/if}
|
|
204
321
|
{/if}
|
|
205
322
|
|
|
206
|
-
{#if loginModal && loginForm}
|
|
323
|
+
{#if _useLoginModal && loginModal && loginForm}
|
|
207
324
|
<LoginFormModal
|
|
208
325
|
bind:this={loginModalRef}
|
|
209
326
|
formData={loginForm.formData}
|
|
@@ -225,4 +342,41 @@
|
|
|
225
342
|
{...loginModal}
|
|
226
343
|
/>
|
|
227
344
|
{/if}
|
|
345
|
+
|
|
346
|
+
{#if _useLoginOrRegisterModal && loginOrRegisterModal}
|
|
347
|
+
<LoginOrRegisterFormModal
|
|
348
|
+
bind:this={loginOrRegisterModalRef}
|
|
349
|
+
bind:mode={_loroMode}
|
|
350
|
+
bind:verifyEmail={_loroVerifyEmail}
|
|
351
|
+
onLogin={loginOrRegisterModal.onLogin}
|
|
352
|
+
onRegister={loginOrRegisterModal.onRegister}
|
|
353
|
+
onVerify={loginOrRegisterModal.onVerify}
|
|
354
|
+
onResendCode={loginOrRegisterModal.onResendCode}
|
|
355
|
+
onForgotPassword={loginOrRegisterModal.onForgotPassword}
|
|
356
|
+
onModeChange={(next, prev) => {
|
|
357
|
+
loginOrRegisterModal!.onModeChange?.(next, prev);
|
|
358
|
+
}}
|
|
359
|
+
isSubmitting={loginOrRegisterModal.isSubmitting}
|
|
360
|
+
loginProps={loginOrRegisterModal.loginProps}
|
|
361
|
+
registerProps={loginOrRegisterModal.registerProps}
|
|
362
|
+
verifyProps={loginOrRegisterModal.verifyProps}
|
|
363
|
+
modeSwitcher={loginOrRegisterModal.modeSwitcher}
|
|
364
|
+
loginModeLabel={loginOrRegisterModal.loginModeLabel}
|
|
365
|
+
registerModeLabel={loginOrRegisterModal.registerModeLabel}
|
|
366
|
+
verifyModeLabel={loginOrRegisterModal.verifyModeLabel}
|
|
367
|
+
socialLogins={loginOrRegisterModal.socialLogins}
|
|
368
|
+
socialDividerLabel={loginOrRegisterModal.socialDividerLabel}
|
|
369
|
+
footer={loginOrRegisterModal.footer}
|
|
370
|
+
{notifications}
|
|
371
|
+
title={loginOrRegisterModal.title}
|
|
372
|
+
classModal={loginOrRegisterModal.classModal}
|
|
373
|
+
classInner={loginOrRegisterModal.classInner}
|
|
374
|
+
classForm={loginOrRegisterModal.classForm}
|
|
375
|
+
noXClose={loginOrRegisterModal.noXClose}
|
|
376
|
+
noClickOutsideClose={loginOrRegisterModal.noClickOutsideClose}
|
|
377
|
+
onClose={loginOrRegisterModal.onClose}
|
|
378
|
+
t={modalT}
|
|
379
|
+
{unstyled}
|
|
380
|
+
/>
|
|
381
|
+
{/if}
|
|
228
382
|
</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. */
|
|
@@ -26,7 +26,7 @@ Each step container renders a `CheckoutProgress` indicator (unless `hideProgress
|
|
|
26
26
|
| `CheckoutOrderSummary` | Totals (subtotal/tax/shipping/discount/total) |
|
|
27
27
|
| `CheckoutOrderReview` | Read-only order dump (items + addresses + delivery) |
|
|
28
28
|
| `CheckoutOrderConfirmation` | Completed-order summary with order number & next steps |
|
|
29
|
-
| `CheckoutGuestOrLoginForm` |
|
|
29
|
+
| `CheckoutGuestOrLoginForm` | Guest / login switcher (segmented pill) |
|
|
30
30
|
| `CheckoutGuestForm` | Guest-checkout fields |
|
|
31
31
|
| `CheckoutLoginForm` | Login (adapts the generic `LoginForm` to checkout i18n) |
|
|
32
32
|
| `CheckoutAddressForm` | Structured address input |
|
|
@@ -139,11 +139,89 @@ 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.
|
|
145
223
|
- Form submissions do **not** automatically move focus to the first error field. Consumers wanting this behavior should do it in their `onContinue` handler after receiving validation errors.
|
|
146
|
-
- `CheckoutGuestOrLoginForm` uses
|
|
224
|
+
- `CheckoutGuestOrLoginForm` uses `ButtonGroupRadio` (`role="radiogroup"`) for the guest/login switch; focus does not auto-move to the panel heading on switch.
|
|
147
225
|
|
|
148
226
|
## Address equality (advanced)
|
|
149
227
|
|
|
@@ -3,10 +3,6 @@
|
|
|
3
3
|
--stuic-checkout-guest-or-login-gap: 0.25rem;
|
|
4
4
|
--stuic-checkout-guest-or-login-divider-color: var(--stuic-color-border);
|
|
5
5
|
--stuic-checkout-guest-or-login-tabs-margin-bottom: 1.5rem;
|
|
6
|
-
--stuic-checkout-guest-or-login-tabs-background-color: var(--stuic-color-muted);
|
|
7
|
-
--stuic-checkout-guest-or-login-tabs-active-background-color: var(
|
|
8
|
-
--stuic-color-muted-foreground
|
|
9
|
-
);
|
|
10
6
|
}
|
|
11
7
|
|
|
12
8
|
@layer components {
|
|
@@ -16,32 +12,11 @@
|
|
|
16
12
|
gap: var(--stuic-checkout-guest-or-login-gap);
|
|
17
13
|
}
|
|
18
14
|
|
|
19
|
-
/*
|
|
15
|
+
/* Spacing below the guest/login switcher (ButtonGroupRadio) */
|
|
20
16
|
.stuic-checkout-guest-or-login-tabs {
|
|
21
|
-
--stuic-tabbed-menu-radius: 9999px;
|
|
22
|
-
--stuic-tabbed-menu-item-max-width: none;
|
|
23
|
-
background-color: var(--stuic-checkout-guest-or-login-tabs-background-color);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
.stuic-checkout-guest-or-login-tabs.stuic-tabbed-menu {
|
|
27
|
-
padding: 3px;
|
|
28
|
-
border: 1px solid var(--stuic-checkout-guest-or-login-divider-color);
|
|
29
|
-
border-radius: 9999px;
|
|
30
17
|
margin-bottom: var(--stuic-checkout-guest-or-login-tabs-margin-bottom);
|
|
31
18
|
}
|
|
32
19
|
|
|
33
|
-
/* Override TabbedMenu's top-only border-radius to full pill shape */
|
|
34
|
-
.stuic-checkout-guest-or-login-tabs .stuic-tabbed-menu-tab {
|
|
35
|
-
border-radius: 9999px;
|
|
36
|
-
background-color: var(--stuic-checkout-guest-or-login-tabs-background-color);
|
|
37
|
-
border: 0;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
.stuic-checkout-guest-or-login-tabs .stuic-tabbed-menu-tab[aria-selected="true"] {
|
|
41
|
-
border-color: var(--stuic-tabbed-menu-border-active);
|
|
42
|
-
background-color: var(--stuic-checkout-guest-or-login-tabs-active-background-color);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
20
|
/* Divider (stacked mode) */
|
|
46
21
|
.stuic-checkout-guest-or-login-divider {
|
|
47
22
|
display: flex;
|
|
@@ -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",
|