@wix/himalaya-cli 0.1397.0 → 0.1398.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 (44) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/serve-runtime/src/wix-bridge.d.ts +30 -0
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/icon-catalog.generated.d.ts +1 -1
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/schema.d.ts +2 -0
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/validation.d.ts +1 -0
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/auth/src/copy.generated.d.ts +109 -0
  6. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/auth/src/feedback.d.ts +15 -0
  7. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/auth/src/index.d.ts +3 -0
  8. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/screens/src/index.d.ts +44 -0
  9. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/auth.mjs +304 -18
  10. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/core/validation.mjs +1 -1
  11. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/core.mjs +31 -1
  12. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/screens.mjs +377 -20
  13. package/dist/cli.mjs +941 -342
  14. package/dist/preview-assets.generated.bin +0 -0
  15. package/guides/component-reference.json +1 -1
  16. package/guides/component-reference.md +2 -0
  17. package/guides/headless-loop.md +1 -1
  18. package/guides/importable-sdks.md +2 -1
  19. package/guides/one-app-to-himi-app.md +30 -0
  20. package/guides/one-app-viewer.md +8 -1
  21. package/guides/sdk-reference.json +92 -1
  22. package/package.json +2 -2
  23. package/templates/beauty-salon/dev/index.ts +41 -1
  24. package/templates/beauty-salon/tier5-src/himi-store-account/index.ts +90 -76
  25. package/templates/branded-lite/dev/index.ts +41 -1
  26. package/templates/branded-lite/tier5-src/himi-store-account/index.ts +90 -76
  27. package/templates/community-club/dev/index.ts +41 -1
  28. package/templates/community-club/tier5-src/himi-store-account/index.ts +90 -76
  29. package/templates/events-meetups/dev/index.ts +41 -1
  30. package/templates/events-meetups/tier5-src/himi-store-account/index.ts +90 -76
  31. package/templates/gym-fitness/dev/index.ts +41 -1
  32. package/templates/gym-fitness/tier5-src/himi-store-account/index.ts +90 -76
  33. package/templates/nonprofit-cause/dev/index.ts +41 -1
  34. package/templates/nonprofit-cause/tier5-src/himi-store-account/index.ts +90 -76
  35. package/templates/podcast-creator/dev/index.ts +41 -1
  36. package/templates/podcast-creator/tier5-src/himi-store-account/index.ts +90 -76
  37. package/templates/portfolio-creative/dev/index.ts +41 -1
  38. package/templates/portfolio-creative/tier5-src/himi-store-account/index.ts +90 -76
  39. package/templates/restaurant/dev/index.ts +41 -1
  40. package/templates/restaurant/tier5-src/himi-store-account/index.ts +90 -76
  41. package/templates/retail-store/dev/index.ts +41 -1
  42. package/templates/retail-store/tier5-src/himi-store-account/index.ts +90 -76
  43. package/templates/yoga-studio/dev/index.ts +41 -1
  44. package/templates/yoga-studio/tier5-src/himi-store-account/index.ts +90 -76
@@ -1,3 +1,4 @@
1
+ import { authFailure, authMessage, identityErrors, type AuthOperation } from "@himalaya/auth";
1
2
  // Tier-5 bundle: himi-store — visitor/member account state.
2
3
  //
3
4
  // SINGLE-SITE BY DESIGN. A Branded Lite build talks to exactly one Wix site, and the
@@ -75,23 +76,50 @@ function resumeAfterAuth(next: S): S {
75
76
  // Actions are not serialized: two quick Sign in taps both run against the pre-tap
76
77
  // state, so without this both would submit credentials and both would resume.
77
78
  let authInFlight = false;
79
+ // Never projected or persisted. The renderer retains its own masked draft for retry.
80
+ let passwordDraft = "";
81
+ let editRevision = 0;
82
+ // Descriptor configuration belongs to the worker session. Keep it even when a
83
+ // delayed profile probe is discarded because the user edited the form.
84
+ let memberAccountLinksRequested = false;
85
+ const translated = (key: string) => copyParam(key, authMessage(key));
78
86
 
79
- async function authenticate(s: S, submit: () => Promise<unknown>, notMemberMessage: string, failedMessage: string): Promise<S | undefined> {
87
+ function validateAuth(s: S, operation: AuthOperation): S {
88
+ const errors = identityErrors({ email: s.email, password: passwordDraft }, operation);
89
+ return derive({ ...s, emailError: errors.email ? translated(errors.email) : "",
90
+ passwordError: errors.password ? translated(errors.password) : "",
91
+ emailFocus: errors.email ? bumpToken(s.emailFocus) : s.emailFocus,
92
+ passwordFocus: !errors.email && errors.password ? bumpToken(s.passwordFocus) : s.passwordFocus,
93
+ message: "" });
94
+ }
95
+ function rejectResult(raw: unknown, operation: AuthOperation) {
96
+ const value = raw as { status?: string; error?: unknown; payload?: { error?: unknown } } | null;
97
+ if (value?.status === "error" || value?.error || value?.payload?.error) throw authFailure(raw, operation, translated);
98
+ }
99
+ async function authenticate(s: S, operation: "signIn" | "register"): Promise<S | undefined> {
80
100
  if (authInFlight) return undefined;
101
+ const checked = validateAuth(s, operation);
102
+ if (checked.emailError || checked.passwordError) return checked;
81
103
  authInFlight = true;
104
+ const revision = editRevision;
105
+ const password = passwordDraft;
82
106
  try {
83
- await submit();
84
- const next = await probe(optimisticMember(s, s.email));
85
- if (!next.isMember) return derive({ ...next, message: notMemberMessage });
86
- return resumeAfterAuth(derive({ ...next, message: "" }));
87
- } catch {
88
- // Clear the password: service state is snapshotted to disk (the worker-state
89
- // store), so a failed attempt must not leave it there in cleartext. The Password
90
- // field binds `valuePath` to it, so the field empties too and a retry re-types.
91
- return derive({ ...s, password: "", message: failedMessage });
92
- } finally {
93
- authInFlight = false;
94
- }
107
+ await svc.beginAuth(); // publish busy before waiting for the network
108
+ const raw = await dispatch(operation === "signIn" ? "himi.wix.signInDirect" : "himi.wix.registerDirect", {
109
+ email: s.email.trim(), password,
110
+ ...(operation === "register" ? { firstName: s.firstName, lastName: s.lastName } : {}),
111
+ });
112
+ rejectResult(raw, operation);
113
+ if (revision !== editRevision) return derive({ ...svc.getState(), authBusy: false });
114
+ const next = await probe(svc.getState());
115
+ if (revision !== editRevision) return derive({ ...svc.getState(), authBusy: false });
116
+ if (!next.isMember) throw { code: "unknown" };
117
+ passwordDraft = "";
118
+ return resumeAfterAuth(completedAuth(next));
119
+ } catch (error) {
120
+ return derive({ ...svc.getState(), authBusy: false,
121
+ message: revision === editRevision ? authFailure(error, operation, translated).reason : "" });
122
+ } finally { authInFlight = false; }
95
123
  }
96
124
 
97
125
  // Next value of a focus token: monotonic, and never empty once requested. A counter
@@ -180,10 +208,14 @@ interface S extends IapRestoreFields {
180
208
  noReturnPath: boolean;
181
209
  returnCta: string;
182
210
  email: string;
183
- /** Held only from typing to the submit. Service state is persisted, so every
184
- * submitted sign-in/register clears it (success, not-a-member, and failure), as
185
- * do recovery and sign-out. Sign-in <-> register keeps it: the field stays up. */
211
+ /** Legacy snapshot slot, always empty. The active form keeps its password in
212
+ * transient memory for retry; hydration never restores a persisted secret. */
186
213
  password: string;
214
+ authBusy: boolean;
215
+ busyMessage?: string;
216
+ emailError: string;
217
+ passwordError: string;
218
+ passwordFocus: string;
187
219
  firstName: string;
188
220
  lastName: string;
189
221
  // Focus TOKENS (`TextFieldProps.focus_path`), not flags: the bound field focuses
@@ -247,6 +279,8 @@ function derive(s: S): S {
247
279
  const hasReturnPath = !!s.isMember && !!s.returnScreenId;
248
280
  return {
249
281
  ...s,
282
+ password: "",
283
+ busyMessage: s.authBusy ? translated("auth.feedback.busy") : "",
250
284
  isLoading: s.status === "loading",
251
285
  notLoading: s.status !== "loading",
252
286
  notMember: !s.isMember,
@@ -309,9 +343,10 @@ async function orderRows(memberEmail: string): Promise<AccountActivityRow[]> {
309
343
  }
310
344
 
311
345
  async function probe(s: S, arg?: unknown): Promise<S> {
346
+ memberAccountLinksRequested ||= s.memberAccountLinks || strField(arg, "memberAccountLinks") === "1";
312
347
  const intent = authIntentFromArg(arg, s);
313
348
  const profile = await loadMemberProfile(copyParam("storeAccount.guestDetail", "Sign in to keep your cart and orders connected."));
314
- const memberAccountLinks = s.memberAccountLinks || strField(arg, "memberAccountLinks") === "1";
349
+ const memberAccountLinks = memberAccountLinksRequested;
315
350
  const restorePurchases = s.restorePurchases || strField(arg, "restorePurchases") === "1";
316
351
  if (restorePurchases) enterCommerce();
317
352
  const routes = memberAccountLinks ? await memberAccountRoutes() : { addresses: false, wallet: false };
@@ -353,21 +388,11 @@ async function probe(s: S, arg?: unknown): Promise<S> {
353
388
  });
354
389
  }
355
390
 
356
- function optimisticMember(s: S, email: string): S {
391
+ function completedAuth(s: S): S {
357
392
  return derive({
358
393
  ...s,
359
- status: "ready",
360
- isMember: true,
361
- identityLine: copyParam("storeAccount.signedIn", "Signed in"),
362
- detailLine: email,
363
- memberName: email,
364
- memberEmail: email,
365
- memberImage: "",
366
- memberInitials: "M",
367
- hasMemberImage: false,
368
- noMemberImage: true,
369
- email,
370
394
  password: "",
395
+ authBusy: false, emailError: "", passwordError: "", passwordFocus: "",
371
396
  firstName: "",
372
397
  lastName: "",
373
398
  isRegistering: false,
@@ -377,6 +402,8 @@ function optimisticMember(s: S, email: string): S {
377
402
  }
378
403
 
379
404
  const svc = defineService<S, {
405
+ beginAuth: (s: S) => S;
406
+ submit: (s: S) => Promise<S | undefined>;
380
407
  load: (s: S, arg?: unknown) => Promise<S>;
381
408
  refresh: (s: S, arg?: unknown) => Promise<S>;
382
409
  setEmail: (s: S, arg?: unknown) => S;
@@ -395,7 +422,7 @@ const svc = defineService<S, {
395
422
  restorePublish: (s: S) => S;
396
423
  iapRestore: (s: S) => Promise<undefined>;
397
424
  }>("himiStoreAccount", {
398
- notHydrated: [...IAP_RESTORE_KEYS],
425
+ notHydrated: ["password", "authBusy", "emailError", "passwordError", "passwordFocus", ...IAP_RESTORE_KEYS],
399
426
  state: derive({
400
427
  status: "loading",
401
428
  isMember: false,
@@ -420,6 +447,7 @@ const svc = defineService<S, {
420
447
  returnCta: "Continue",
421
448
  email: "",
422
449
  password: "",
450
+ authBusy: false, emailError: "", passwordError: "", passwordFocus: "",
423
451
  firstName: "",
424
452
  lastName: "",
425
453
  lastNameFocus: "",
@@ -445,12 +473,14 @@ const svc = defineService<S, {
445
473
  ...NO_IAP_RESTORE,
446
474
  }),
447
475
  actions: {
448
- load: (s, arg) => { stashCopy(arg); return probe(s, arg); },
449
- refresh: (s, arg) => { stashCopy(arg); return probe(s, arg); },
450
- setEmail: (s, arg) => derive({ ...s, email: strOf(arg), message: "" }),
451
- setPassword: (s, arg) => derive({ ...s, password: strOf(arg), message: "" }),
452
- setFirstName: (s, arg) => derive({ ...s, firstName: strOf(arg), message: "" }),
453
- setLastName: (s, arg) => derive({ ...s, lastName: strOf(arg), message: "" }),
476
+ submit: (s): Promise<S | undefined> => s.isRecovering ? svc.resetPassword() : authenticate(s, s.isRegistering ? "register" : "signIn"),
477
+ beginAuth: (s) => derive({ ...s, authBusy: true, message: "", emailError: "", passwordError: "" }),
478
+ load: async (s, arg): Promise<S> => { stashCopy(arg); const revision = editRevision; const next = await probe(s, arg); return authInFlight || revision !== editRevision ? svc.getState() : next; },
479
+ refresh: async (s, arg): Promise<S> => { stashCopy(arg); const revision = editRevision; const next = await probe(s, arg); return authInFlight || revision !== editRevision ? svc.getState() : next; },
480
+ setEmail: (s, arg) => { editRevision++; const next = { ...s, email: strOf(arg).trim(), message: "" }; return derive({ ...next, emailError: s.emailError ? (identityErrors({ email: next.email }, "reset").email ? translated(identityErrors({ email: next.email }, "reset").email!) : "") : "" }); },
481
+ setPassword: (s, arg) => { editRevision++; passwordDraft = strOf(arg); return derive({ ...s, message: "", passwordError: s.passwordError && !passwordDraft ? translated("auth.validation.passwordRequired") : "" }); },
482
+ setFirstName: (s, arg) => { editRevision++; return derive({ ...s, firstName: strOf(arg), message: "" }); },
483
+ setLastName: (s, arg) => { editRevision++; return derive({ ...s, lastName: strOf(arg), message: "" }); },
454
484
  // The register form's Next keys. Only first/last name carry one: they share a
455
485
  // single visibility gate (notRegistering) so their target is always on screen.
456
486
  // Email deliberately does NOT chain to password — see the comment on the
@@ -463,48 +493,32 @@ const svc = defineService<S, {
463
493
  // register and coming back would open focused on Last name, skipping First name.
464
494
  // Only the tokens whose fields actually unmount here are cleared: see
465
495
  // NO_FOCUS_MODE_SWITCH for why Email's is deliberately NOT among them.
466
- showRegister: (s) => derive({ ...s, isRegistering: true, isRecovering: false, message: "", ...NO_FOCUS_MODE_SWITCH }),
467
- showSignIn: (s) => derive({ ...s, isRegistering: false, isRecovering: false, message: "", ...NO_FOCUS_MODE_SWITCH }),
468
- showRecovery: (s) => derive({ ...s, isRegistering: false, isRecovering: true, password: "", message: "", ...NO_FOCUS_MODE_SWITCH }),
469
- resetPassword: async (s) => {
470
- const email = s.email.trim();
471
- if (!email || !email.includes("@")) return derive({ ...s, message: copyParam("storeAccount.validationEmail", "Enter a valid email address.") });
496
+ showRegister: (s) => { if (authInFlight) return s; editRevision++; return derive({ ...s, isRegistering: true, isRecovering: false, message: "", emailError: "", passwordError: "", ...NO_FOCUS_MODE_SWITCH }) },
497
+ showSignIn: (s) => { if (authInFlight) return s; editRevision++; return derive({ ...s, isRegistering: false, isRecovering: false, message: "", emailError: "", passwordError: "", ...NO_FOCUS_MODE_SWITCH }) },
498
+ showRecovery: (s) => { if (authInFlight) return s; editRevision++; return derive({ ...s, isRegistering: false, isRecovering: true, password: "", message: "", emailError: "", passwordError: "", ...NO_FOCUS_MODE_SWITCH }) },
499
+ resetPassword: async (s): Promise<S> => {
500
+ if (authInFlight) return s;
501
+ const checked = validateAuth(s, "reset");
502
+ if (checked.emailError) return checked;
503
+ authInFlight = true;
504
+ const revision = editRevision;
472
505
  try {
473
- await dispatch("himi.wix.resetPassword", { email });
474
- return derive({
475
- ...s,
476
- email,
477
- isRecovering: false,
478
- message: copyParam("storeAccount.resetSent", "If that email is registered, a reset link is on its way."),
479
- });
480
- } catch {
481
- return derive({ ...s, message: copyParam("storeAccount.resetFailed", "Reset email could not be sent. Try again.") });
482
- }
483
- },
484
- signIn: async (s) => {
485
- if (!s.email || !s.password) return derive({ ...s, message: copyParam("storeAccount.validationEmailPassword", "Enter your email and password.") });
486
- return authenticate(
487
- s,
488
- () => dispatch("himi.wix.signInDirect", { email: s.email, password: s.password }),
489
- copyParam("storeAccount.signInCheckDetails", "Sign in failed. Check your details."),
490
- copyParam("storeAccount.signInFailed", "Sign in failed. Try again."),
491
- );
492
- },
493
- register: async (s) => {
494
- if (!s.email || !s.password) return derive({ ...s, message: copyParam("storeAccount.validationEmailPassword", "Enter your email and password.") });
495
- return authenticate(
496
- s,
497
- () => dispatch("himi.wix.registerDirect", {
498
- email: s.email,
499
- password: s.password,
500
- firstName: s.firstName,
501
- lastName: s.lastName,
502
- }),
503
- copyParam("storeAccount.registerTrySignIn", "Account creation failed. Try signing in."),
504
- copyParam("storeAccount.registerFailed", "Account creation failed. Try another email."),
505
- );
506
+ await svc.beginAuth();
507
+ const raw = await dispatch("himi.wix.resetPassword", { email: s.email.trim() });
508
+ rejectResult(raw, "reset");
509
+ if (revision !== editRevision) return derive({ ...svc.getState(), authBusy: false });
510
+ return derive({ ...svc.getState(), authBusy: false, isRecovering: false,
511
+ message: copyParam("storeAccount.resetSent", "If that email is registered, a reset link is on its way.") });
512
+ } catch (error) {
513
+ return derive({ ...svc.getState(), authBusy: false,
514
+ message: revision === editRevision ? authFailure(error, "reset", translated).reason : "" });
515
+ } finally { authInFlight = false; }
506
516
  },
507
- signOut: async (s) => {
517
+ signIn: (s) => authenticate(s, "signIn"),
518
+ register: (s) => authenticate(s, "register"),
519
+ signOut: async (s): Promise<S> => {
520
+ if (authInFlight) return svc.getState();
521
+ passwordDraft = ""; editRevision++;
508
522
  routeInputConsumed = true;
509
523
  try { await dispatch("himi.wix.signOut"); } catch { /* ignore */ }
510
524
  // Do NOT re-probe via getMyData after logout — visitor-scoped my-data can
@@ -2197,6 +2197,16 @@ const SCREENS = runtimeSetting({
2197
2197
  "props": {
2198
2198
  "placeholder": "Email",
2199
2199
  "keyboard": "email",
2200
+ "validation": {
2201
+ "required": true,
2202
+ "type": "email",
2203
+ "mode": "on_blur"
2204
+ },
2205
+ "errorPath": "$service:himiStoreAccount.emailError",
2206
+ "submitLabel": "go",
2207
+ "onSubmit": {
2208
+ "actionId": "tier5.himiStoreAccount.submit"
2209
+ },
2200
2210
  "autoCapitalization": "none",
2201
2211
  "focusPath": "$service:himiStoreAccount.emailFocus",
2202
2212
  "onChange": {
@@ -2212,8 +2222,19 @@ const SCREENS = runtimeSetting({
2212
2222
  "type": "TextField",
2213
2223
  "props": {
2214
2224
  "placeholder": "Password",
2215
- "valuePath": "$service:himiStoreAccount.password",
2216
2225
  "secure": true,
2226
+ "visibilityToggle": true,
2227
+ "validation": {
2228
+ "required": true,
2229
+ "mode": "on_blur",
2230
+ "message": "Enter your password."
2231
+ },
2232
+ "errorPath": "$service:himiStoreAccount.passwordError",
2233
+ "focusPath": "$service:himiStoreAccount.passwordFocus",
2234
+ "submitLabel": "go",
2235
+ "onSubmit": {
2236
+ "actionId": "tier5.himiStoreAccount.submit"
2237
+ },
2217
2238
  "autoCapitalization": "none",
2218
2239
  "onChange": {
2219
2240
  "actionId": "tier5.himiStoreAccount.setPassword"
@@ -2224,6 +2245,18 @@ const SCREENS = runtimeSetting({
2224
2245
  "hiddenPath": "$service:himiStoreAccount.isRecovering"
2225
2246
  }
2226
2247
  },
2248
+ {
2249
+ "id": "",
2250
+ "body": {
2251
+ "package": "himalaya",
2252
+ "type": "Text",
2253
+ "props": {
2254
+ "text": "$service:himiStoreAccount.busyMessage",
2255
+ "style": "footnote",
2256
+ "collapseWhenEmpty": true
2257
+ }
2258
+ }
2259
+ },
2227
2260
  {
2228
2261
  "id": "himi_account.message",
2229
2262
  "body": {
@@ -2246,6 +2279,7 @@ const SCREENS = runtimeSetting({
2246
2279
  "title": "Sign in",
2247
2280
  "style": "primary",
2248
2281
  "fullWidth": true,
2282
+ "disabledPath": "$service:himiStoreAccount.authBusy",
2249
2283
  "action": {
2250
2284
  "actionId": "tier5.himiStoreAccount.signIn"
2251
2285
  }
@@ -2264,6 +2298,7 @@ const SCREENS = runtimeSetting({
2264
2298
  "title": "Create account",
2265
2299
  "style": "primary",
2266
2300
  "fullWidth": true,
2301
+ "disabledPath": "$service:himiStoreAccount.authBusy",
2267
2302
  "action": {
2268
2303
  "actionId": "tier5.himiStoreAccount.register"
2269
2304
  }
@@ -2282,6 +2317,7 @@ const SCREENS = runtimeSetting({
2282
2317
  "title": "Send reset link",
2283
2318
  "style": "primary",
2284
2319
  "fullWidth": true,
2320
+ "disabledPath": "$service:himiStoreAccount.authBusy",
2285
2321
  "action": {
2286
2322
  "actionId": "tier5.himiStoreAccount.resetPassword"
2287
2323
  }
@@ -2300,6 +2336,7 @@ const SCREENS = runtimeSetting({
2300
2336
  "title": "Forgot password?",
2301
2337
  "style": "secondary",
2302
2338
  "fullWidth": true,
2339
+ "disabledPath": "$service:himiStoreAccount.authBusy",
2303
2340
  "action": {
2304
2341
  "actionId": "tier5.himiStoreAccount.showRecovery"
2305
2342
  }
@@ -2318,6 +2355,7 @@ const SCREENS = runtimeSetting({
2318
2355
  "title": "Register",
2319
2356
  "style": "secondary",
2320
2357
  "fullWidth": true,
2358
+ "disabledPath": "$service:himiStoreAccount.authBusy",
2321
2359
  "action": {
2322
2360
  "actionId": "tier5.himiStoreAccount.showRegister"
2323
2361
  }
@@ -2336,6 +2374,7 @@ const SCREENS = runtimeSetting({
2336
2374
  "title": "Sign in instead",
2337
2375
  "style": "secondary",
2338
2376
  "fullWidth": true,
2377
+ "disabledPath": "$service:himiStoreAccount.authBusy",
2339
2378
  "action": {
2340
2379
  "actionId": "tier5.himiStoreAccount.showSignIn"
2341
2380
  }
@@ -2354,6 +2393,7 @@ const SCREENS = runtimeSetting({
2354
2393
  "title": "Back to sign in",
2355
2394
  "style": "secondary",
2356
2395
  "fullWidth": true,
2396
+ "disabledPath": "$service:himiStoreAccount.authBusy",
2357
2397
  "action": {
2358
2398
  "actionId": "tier5.himiStoreAccount.showSignIn"
2359
2399
  }
@@ -1,3 +1,4 @@
1
+ import { authFailure, authMessage, identityErrors, type AuthOperation } from "@himalaya/auth";
1
2
  // Tier-5 bundle: himi-store — visitor/member account state.
2
3
  //
3
4
  // SINGLE-SITE BY DESIGN. A Branded Lite build talks to exactly one Wix site, and the
@@ -75,23 +76,50 @@ function resumeAfterAuth(next: S): S {
75
76
  // Actions are not serialized: two quick Sign in taps both run against the pre-tap
76
77
  // state, so without this both would submit credentials and both would resume.
77
78
  let authInFlight = false;
79
+ // Never projected or persisted. The renderer retains its own masked draft for retry.
80
+ let passwordDraft = "";
81
+ let editRevision = 0;
82
+ // Descriptor configuration belongs to the worker session. Keep it even when a
83
+ // delayed profile probe is discarded because the user edited the form.
84
+ let memberAccountLinksRequested = false;
85
+ const translated = (key: string) => copyParam(key, authMessage(key));
78
86
 
79
- async function authenticate(s: S, submit: () => Promise<unknown>, notMemberMessage: string, failedMessage: string): Promise<S | undefined> {
87
+ function validateAuth(s: S, operation: AuthOperation): S {
88
+ const errors = identityErrors({ email: s.email, password: passwordDraft }, operation);
89
+ return derive({ ...s, emailError: errors.email ? translated(errors.email) : "",
90
+ passwordError: errors.password ? translated(errors.password) : "",
91
+ emailFocus: errors.email ? bumpToken(s.emailFocus) : s.emailFocus,
92
+ passwordFocus: !errors.email && errors.password ? bumpToken(s.passwordFocus) : s.passwordFocus,
93
+ message: "" });
94
+ }
95
+ function rejectResult(raw: unknown, operation: AuthOperation) {
96
+ const value = raw as { status?: string; error?: unknown; payload?: { error?: unknown } } | null;
97
+ if (value?.status === "error" || value?.error || value?.payload?.error) throw authFailure(raw, operation, translated);
98
+ }
99
+ async function authenticate(s: S, operation: "signIn" | "register"): Promise<S | undefined> {
80
100
  if (authInFlight) return undefined;
101
+ const checked = validateAuth(s, operation);
102
+ if (checked.emailError || checked.passwordError) return checked;
81
103
  authInFlight = true;
104
+ const revision = editRevision;
105
+ const password = passwordDraft;
82
106
  try {
83
- await submit();
84
- const next = await probe(optimisticMember(s, s.email));
85
- if (!next.isMember) return derive({ ...next, message: notMemberMessage });
86
- return resumeAfterAuth(derive({ ...next, message: "" }));
87
- } catch {
88
- // Clear the password: service state is snapshotted to disk (the worker-state
89
- // store), so a failed attempt must not leave it there in cleartext. The Password
90
- // field binds `valuePath` to it, so the field empties too and a retry re-types.
91
- return derive({ ...s, password: "", message: failedMessage });
92
- } finally {
93
- authInFlight = false;
94
- }
107
+ await svc.beginAuth(); // publish busy before waiting for the network
108
+ const raw = await dispatch(operation === "signIn" ? "himi.wix.signInDirect" : "himi.wix.registerDirect", {
109
+ email: s.email.trim(), password,
110
+ ...(operation === "register" ? { firstName: s.firstName, lastName: s.lastName } : {}),
111
+ });
112
+ rejectResult(raw, operation);
113
+ if (revision !== editRevision) return derive({ ...svc.getState(), authBusy: false });
114
+ const next = await probe(svc.getState());
115
+ if (revision !== editRevision) return derive({ ...svc.getState(), authBusy: false });
116
+ if (!next.isMember) throw { code: "unknown" };
117
+ passwordDraft = "";
118
+ return resumeAfterAuth(completedAuth(next));
119
+ } catch (error) {
120
+ return derive({ ...svc.getState(), authBusy: false,
121
+ message: revision === editRevision ? authFailure(error, operation, translated).reason : "" });
122
+ } finally { authInFlight = false; }
95
123
  }
96
124
 
97
125
  // Next value of a focus token: monotonic, and never empty once requested. A counter
@@ -180,10 +208,14 @@ interface S extends IapRestoreFields {
180
208
  noReturnPath: boolean;
181
209
  returnCta: string;
182
210
  email: string;
183
- /** Held only from typing to the submit. Service state is persisted, so every
184
- * submitted sign-in/register clears it (success, not-a-member, and failure), as
185
- * do recovery and sign-out. Sign-in <-> register keeps it: the field stays up. */
211
+ /** Legacy snapshot slot, always empty. The active form keeps its password in
212
+ * transient memory for retry; hydration never restores a persisted secret. */
186
213
  password: string;
214
+ authBusy: boolean;
215
+ busyMessage?: string;
216
+ emailError: string;
217
+ passwordError: string;
218
+ passwordFocus: string;
187
219
  firstName: string;
188
220
  lastName: string;
189
221
  // Focus TOKENS (`TextFieldProps.focus_path`), not flags: the bound field focuses
@@ -247,6 +279,8 @@ function derive(s: S): S {
247
279
  const hasReturnPath = !!s.isMember && !!s.returnScreenId;
248
280
  return {
249
281
  ...s,
282
+ password: "",
283
+ busyMessage: s.authBusy ? translated("auth.feedback.busy") : "",
250
284
  isLoading: s.status === "loading",
251
285
  notLoading: s.status !== "loading",
252
286
  notMember: !s.isMember,
@@ -309,9 +343,10 @@ async function orderRows(memberEmail: string): Promise<AccountActivityRow[]> {
309
343
  }
310
344
 
311
345
  async function probe(s: S, arg?: unknown): Promise<S> {
346
+ memberAccountLinksRequested ||= s.memberAccountLinks || strField(arg, "memberAccountLinks") === "1";
312
347
  const intent = authIntentFromArg(arg, s);
313
348
  const profile = await loadMemberProfile(copyParam("storeAccount.guestDetail", "Sign in to keep your cart and orders connected."));
314
- const memberAccountLinks = s.memberAccountLinks || strField(arg, "memberAccountLinks") === "1";
349
+ const memberAccountLinks = memberAccountLinksRequested;
315
350
  const restorePurchases = s.restorePurchases || strField(arg, "restorePurchases") === "1";
316
351
  if (restorePurchases) enterCommerce();
317
352
  const routes = memberAccountLinks ? await memberAccountRoutes() : { addresses: false, wallet: false };
@@ -353,21 +388,11 @@ async function probe(s: S, arg?: unknown): Promise<S> {
353
388
  });
354
389
  }
355
390
 
356
- function optimisticMember(s: S, email: string): S {
391
+ function completedAuth(s: S): S {
357
392
  return derive({
358
393
  ...s,
359
- status: "ready",
360
- isMember: true,
361
- identityLine: copyParam("storeAccount.signedIn", "Signed in"),
362
- detailLine: email,
363
- memberName: email,
364
- memberEmail: email,
365
- memberImage: "",
366
- memberInitials: "M",
367
- hasMemberImage: false,
368
- noMemberImage: true,
369
- email,
370
394
  password: "",
395
+ authBusy: false, emailError: "", passwordError: "", passwordFocus: "",
371
396
  firstName: "",
372
397
  lastName: "",
373
398
  isRegistering: false,
@@ -377,6 +402,8 @@ function optimisticMember(s: S, email: string): S {
377
402
  }
378
403
 
379
404
  const svc = defineService<S, {
405
+ beginAuth: (s: S) => S;
406
+ submit: (s: S) => Promise<S | undefined>;
380
407
  load: (s: S, arg?: unknown) => Promise<S>;
381
408
  refresh: (s: S, arg?: unknown) => Promise<S>;
382
409
  setEmail: (s: S, arg?: unknown) => S;
@@ -395,7 +422,7 @@ const svc = defineService<S, {
395
422
  restorePublish: (s: S) => S;
396
423
  iapRestore: (s: S) => Promise<undefined>;
397
424
  }>("himiStoreAccount", {
398
- notHydrated: [...IAP_RESTORE_KEYS],
425
+ notHydrated: ["password", "authBusy", "emailError", "passwordError", "passwordFocus", ...IAP_RESTORE_KEYS],
399
426
  state: derive({
400
427
  status: "loading",
401
428
  isMember: false,
@@ -420,6 +447,7 @@ const svc = defineService<S, {
420
447
  returnCta: "Continue",
421
448
  email: "",
422
449
  password: "",
450
+ authBusy: false, emailError: "", passwordError: "", passwordFocus: "",
423
451
  firstName: "",
424
452
  lastName: "",
425
453
  lastNameFocus: "",
@@ -445,12 +473,14 @@ const svc = defineService<S, {
445
473
  ...NO_IAP_RESTORE,
446
474
  }),
447
475
  actions: {
448
- load: (s, arg) => { stashCopy(arg); return probe(s, arg); },
449
- refresh: (s, arg) => { stashCopy(arg); return probe(s, arg); },
450
- setEmail: (s, arg) => derive({ ...s, email: strOf(arg), message: "" }),
451
- setPassword: (s, arg) => derive({ ...s, password: strOf(arg), message: "" }),
452
- setFirstName: (s, arg) => derive({ ...s, firstName: strOf(arg), message: "" }),
453
- setLastName: (s, arg) => derive({ ...s, lastName: strOf(arg), message: "" }),
476
+ submit: (s): Promise<S | undefined> => s.isRecovering ? svc.resetPassword() : authenticate(s, s.isRegistering ? "register" : "signIn"),
477
+ beginAuth: (s) => derive({ ...s, authBusy: true, message: "", emailError: "", passwordError: "" }),
478
+ load: async (s, arg): Promise<S> => { stashCopy(arg); const revision = editRevision; const next = await probe(s, arg); return authInFlight || revision !== editRevision ? svc.getState() : next; },
479
+ refresh: async (s, arg): Promise<S> => { stashCopy(arg); const revision = editRevision; const next = await probe(s, arg); return authInFlight || revision !== editRevision ? svc.getState() : next; },
480
+ setEmail: (s, arg) => { editRevision++; const next = { ...s, email: strOf(arg).trim(), message: "" }; return derive({ ...next, emailError: s.emailError ? (identityErrors({ email: next.email }, "reset").email ? translated(identityErrors({ email: next.email }, "reset").email!) : "") : "" }); },
481
+ setPassword: (s, arg) => { editRevision++; passwordDraft = strOf(arg); return derive({ ...s, message: "", passwordError: s.passwordError && !passwordDraft ? translated("auth.validation.passwordRequired") : "" }); },
482
+ setFirstName: (s, arg) => { editRevision++; return derive({ ...s, firstName: strOf(arg), message: "" }); },
483
+ setLastName: (s, arg) => { editRevision++; return derive({ ...s, lastName: strOf(arg), message: "" }); },
454
484
  // The register form's Next keys. Only first/last name carry one: they share a
455
485
  // single visibility gate (notRegistering) so their target is always on screen.
456
486
  // Email deliberately does NOT chain to password — see the comment on the
@@ -463,48 +493,32 @@ const svc = defineService<S, {
463
493
  // register and coming back would open focused on Last name, skipping First name.
464
494
  // Only the tokens whose fields actually unmount here are cleared: see
465
495
  // NO_FOCUS_MODE_SWITCH for why Email's is deliberately NOT among them.
466
- showRegister: (s) => derive({ ...s, isRegistering: true, isRecovering: false, message: "", ...NO_FOCUS_MODE_SWITCH }),
467
- showSignIn: (s) => derive({ ...s, isRegistering: false, isRecovering: false, message: "", ...NO_FOCUS_MODE_SWITCH }),
468
- showRecovery: (s) => derive({ ...s, isRegistering: false, isRecovering: true, password: "", message: "", ...NO_FOCUS_MODE_SWITCH }),
469
- resetPassword: async (s) => {
470
- const email = s.email.trim();
471
- if (!email || !email.includes("@")) return derive({ ...s, message: copyParam("storeAccount.validationEmail", "Enter a valid email address.") });
496
+ showRegister: (s) => { if (authInFlight) return s; editRevision++; return derive({ ...s, isRegistering: true, isRecovering: false, message: "", emailError: "", passwordError: "", ...NO_FOCUS_MODE_SWITCH }) },
497
+ showSignIn: (s) => { if (authInFlight) return s; editRevision++; return derive({ ...s, isRegistering: false, isRecovering: false, message: "", emailError: "", passwordError: "", ...NO_FOCUS_MODE_SWITCH }) },
498
+ showRecovery: (s) => { if (authInFlight) return s; editRevision++; return derive({ ...s, isRegistering: false, isRecovering: true, password: "", message: "", emailError: "", passwordError: "", ...NO_FOCUS_MODE_SWITCH }) },
499
+ resetPassword: async (s): Promise<S> => {
500
+ if (authInFlight) return s;
501
+ const checked = validateAuth(s, "reset");
502
+ if (checked.emailError) return checked;
503
+ authInFlight = true;
504
+ const revision = editRevision;
472
505
  try {
473
- await dispatch("himi.wix.resetPassword", { email });
474
- return derive({
475
- ...s,
476
- email,
477
- isRecovering: false,
478
- message: copyParam("storeAccount.resetSent", "If that email is registered, a reset link is on its way."),
479
- });
480
- } catch {
481
- return derive({ ...s, message: copyParam("storeAccount.resetFailed", "Reset email could not be sent. Try again.") });
482
- }
483
- },
484
- signIn: async (s) => {
485
- if (!s.email || !s.password) return derive({ ...s, message: copyParam("storeAccount.validationEmailPassword", "Enter your email and password.") });
486
- return authenticate(
487
- s,
488
- () => dispatch("himi.wix.signInDirect", { email: s.email, password: s.password }),
489
- copyParam("storeAccount.signInCheckDetails", "Sign in failed. Check your details."),
490
- copyParam("storeAccount.signInFailed", "Sign in failed. Try again."),
491
- );
492
- },
493
- register: async (s) => {
494
- if (!s.email || !s.password) return derive({ ...s, message: copyParam("storeAccount.validationEmailPassword", "Enter your email and password.") });
495
- return authenticate(
496
- s,
497
- () => dispatch("himi.wix.registerDirect", {
498
- email: s.email,
499
- password: s.password,
500
- firstName: s.firstName,
501
- lastName: s.lastName,
502
- }),
503
- copyParam("storeAccount.registerTrySignIn", "Account creation failed. Try signing in."),
504
- copyParam("storeAccount.registerFailed", "Account creation failed. Try another email."),
505
- );
506
+ await svc.beginAuth();
507
+ const raw = await dispatch("himi.wix.resetPassword", { email: s.email.trim() });
508
+ rejectResult(raw, "reset");
509
+ if (revision !== editRevision) return derive({ ...svc.getState(), authBusy: false });
510
+ return derive({ ...svc.getState(), authBusy: false, isRecovering: false,
511
+ message: copyParam("storeAccount.resetSent", "If that email is registered, a reset link is on its way.") });
512
+ } catch (error) {
513
+ return derive({ ...svc.getState(), authBusy: false,
514
+ message: revision === editRevision ? authFailure(error, "reset", translated).reason : "" });
515
+ } finally { authInFlight = false; }
506
516
  },
507
- signOut: async (s) => {
517
+ signIn: (s) => authenticate(s, "signIn"),
518
+ register: (s) => authenticate(s, "register"),
519
+ signOut: async (s): Promise<S> => {
520
+ if (authInFlight) return svc.getState();
521
+ passwordDraft = ""; editRevision++;
508
522
  routeInputConsumed = true;
509
523
  try { await dispatch("himi.wix.signOut"); } catch { /* ignore */ }
510
524
  // Do NOT re-probe via getMyData after logout — visitor-scoped my-data can