@wix/himalaya-cli 0.1397.0 → 0.1399.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 (88) 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 +72 -1
  12. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/screens.mjs +377 -20
  13. package/dist/cli.mjs +983 -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/beauty-salon-owner/dev/index.ts +304 -7
  26. package/templates/beauty-salon-owner/tier5-src/_shared/aria-chat.ts +39 -33
  27. package/templates/beauty-salon-owner/tier5-src/_shared/aria-widget-model.ts +26 -4
  28. package/templates/beauty-salon-owner/tier5-src/_shared/aria-widget-view.ts +63 -16
  29. package/templates/branded-lite/dev/index.ts +41 -1
  30. package/templates/branded-lite/tier5-src/himi-store-account/index.ts +90 -76
  31. package/templates/community-club/dev/index.ts +41 -1
  32. package/templates/community-club/tier5-src/himi-store-account/index.ts +90 -76
  33. package/templates/community-club-owner/dev/index.ts +304 -7
  34. package/templates/community-club-owner/tier5-src/_shared/aria-chat.ts +39 -33
  35. package/templates/community-club-owner/tier5-src/_shared/aria-widget-model.ts +26 -4
  36. package/templates/community-club-owner/tier5-src/_shared/aria-widget-view.ts +63 -16
  37. package/templates/events-meetups/dev/index.ts +41 -1
  38. package/templates/events-meetups/tier5-src/himi-store-account/index.ts +90 -76
  39. package/templates/events-meetups-owner/dev/index.ts +304 -7
  40. package/templates/events-meetups-owner/tier5-src/_shared/aria-chat.ts +39 -33
  41. package/templates/events-meetups-owner/tier5-src/_shared/aria-widget-model.ts +26 -4
  42. package/templates/events-meetups-owner/tier5-src/_shared/aria-widget-view.ts +63 -16
  43. package/templates/gym-fitness/dev/index.ts +41 -1
  44. package/templates/gym-fitness/tier5-src/himi-store-account/index.ts +90 -76
  45. package/templates/gym-fitness-owner/dev/index.ts +304 -7
  46. package/templates/gym-fitness-owner/tier5-src/_shared/aria-chat.ts +39 -33
  47. package/templates/gym-fitness-owner/tier5-src/_shared/aria-widget-model.ts +26 -4
  48. package/templates/gym-fitness-owner/tier5-src/_shared/aria-widget-view.ts +63 -16
  49. package/templates/nonprofit-cause/dev/index.ts +41 -1
  50. package/templates/nonprofit-cause/tier5-src/himi-store-account/index.ts +90 -76
  51. package/templates/nonprofit-cause-owner/dev/index.ts +304 -7
  52. package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-chat.ts +39 -33
  53. package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-widget-model.ts +26 -4
  54. package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-widget-view.ts +63 -16
  55. package/templates/owner-lite/dev/index.ts +304 -7
  56. package/templates/owner-lite/tier5-src/_shared/aria-chat.ts +39 -33
  57. package/templates/owner-lite/tier5-src/_shared/aria-widget-model.ts +26 -4
  58. package/templates/owner-lite/tier5-src/_shared/aria-widget-view.ts +63 -16
  59. package/templates/podcast-creator/dev/index.ts +41 -1
  60. package/templates/podcast-creator/tier5-src/himi-store-account/index.ts +90 -76
  61. package/templates/podcast-creator-owner/dev/index.ts +304 -7
  62. package/templates/podcast-creator-owner/tier5-src/_shared/aria-chat.ts +39 -33
  63. package/templates/podcast-creator-owner/tier5-src/_shared/aria-widget-model.ts +26 -4
  64. package/templates/podcast-creator-owner/tier5-src/_shared/aria-widget-view.ts +63 -16
  65. package/templates/portfolio-creative/dev/index.ts +41 -1
  66. package/templates/portfolio-creative/tier5-src/himi-store-account/index.ts +90 -76
  67. package/templates/portfolio-creative-owner/dev/index.ts +304 -7
  68. package/templates/portfolio-creative-owner/tier5-src/_shared/aria-chat.ts +39 -33
  69. package/templates/portfolio-creative-owner/tier5-src/_shared/aria-widget-model.ts +26 -4
  70. package/templates/portfolio-creative-owner/tier5-src/_shared/aria-widget-view.ts +63 -16
  71. package/templates/restaurant/dev/index.ts +41 -1
  72. package/templates/restaurant/tier5-src/himi-store-account/index.ts +90 -76
  73. package/templates/restaurant-owner/dev/index.ts +304 -7
  74. package/templates/restaurant-owner/tier5-src/_shared/aria-chat.ts +39 -33
  75. package/templates/restaurant-owner/tier5-src/_shared/aria-widget-model.ts +26 -4
  76. package/templates/restaurant-owner/tier5-src/_shared/aria-widget-view.ts +63 -16
  77. package/templates/retail-store/dev/index.ts +41 -1
  78. package/templates/retail-store/tier5-src/himi-store-account/index.ts +90 -76
  79. package/templates/retail-store-owner/dev/index.ts +304 -7
  80. package/templates/retail-store-owner/tier5-src/_shared/aria-chat.ts +39 -33
  81. package/templates/retail-store-owner/tier5-src/_shared/aria-widget-model.ts +26 -4
  82. package/templates/retail-store-owner/tier5-src/_shared/aria-widget-view.ts +63 -16
  83. package/templates/yoga-studio/dev/index.ts +41 -1
  84. package/templates/yoga-studio/tier5-src/himi-store-account/index.ts +90 -76
  85. package/templates/yoga-studio-owner/dev/index.ts +304 -7
  86. package/templates/yoga-studio-owner/tier5-src/_shared/aria-chat.ts +39 -33
  87. package/templates/yoga-studio-owner/tier5-src/_shared/aria-widget-model.ts +26 -4
  88. package/templates/yoga-studio-owner/tier5-src/_shared/aria-widget-view.ts +63 -16
@@ -12,10 +12,11 @@
12
12
  // every collapsible element carries its own `*Hidden` boolean — the same shape
13
13
  // the transcript bubbles already use for their three link slots.
14
14
 
15
- import type { NormalizedWidget, WidgetKind, WidgetOption } from "./aria-widget-model.js";
15
+ import type { NormalizedWidget, WidgetBenefit, WidgetKind, WidgetOption } from "./aria-widget-model.js";
16
16
  import { SELECTED_TOKEN, VALUES_TOKEN, isWebUrl, stripMarkdown } from "./aria-widget-model.js";
17
17
  import { oauthView } from "./aria-widget-oauth.js";
18
18
  import { composeGenericCard } from "./aria-generic-widget.js";
19
+ import { formatNumber } from "./engine-intl.js";
19
20
  import { normalizeWidget } from "./aria-widget-kinds.js";
20
21
  import { scanEnvelopes, widgetsOfParts } from "./aria-widgets.js";
21
22
 
@@ -90,9 +91,12 @@ export interface WidgetOptionRow {
90
91
  /** The per-option button's label, already host-disclosed. */
91
92
  buttonLabel: string;
92
93
  buttonHidden: boolean;
93
- /** PLANS' benefit list, joined — an array cannot ride on a templated row. */
94
+ /** PLANS benefit prose plus nested repeater rows carrying each icon. */
94
95
  listText: string;
96
+ benefits: WidgetBenefit[];
95
97
  listHidden: boolean;
98
+ badge: string;
99
+ badgeHidden: boolean;
96
100
  selected: boolean;
97
101
  /** This option's own button `respondText`, before substitution. Carried on
98
102
  * the row because the row is what a tap identifies. */
@@ -114,6 +118,7 @@ export interface WidgetOptionRow {
114
118
  export interface WidgetSlice {
115
119
  label: string;
116
120
  value: number;
121
+ color: string;
117
122
  }
118
123
 
119
124
  /** One step of a TODO_PLAN, shaped for `Timeline`. */
@@ -218,6 +223,9 @@ export interface WidgetView {
218
223
  * negation: an EMPTY chart is neither, so hiding each component on the
219
224
  * other's flag left both on screen as blank boxes. */
220
225
  chartPieHidden: boolean;
226
+ chartRingHidden: boolean;
227
+ chartPlainPieHidden: boolean;
228
+ chartLegend: { label: string; color: string; marker: "dot"; value: string }[];
221
229
  chartSeriesHidden: boolean;
222
230
  chartEmptyHidden: boolean;
223
231
  /** Hide the async spinner. `asyncUntracked` is not the right flag: a TRACKED
@@ -526,7 +534,7 @@ export const NO_WIDGET: WidgetView = {
526
534
  optionsGridHidden: true, optionsListHidden: false,
527
535
  footerRowHidden: false, footerColumnHidden: true,
528
536
  slices: [], values: [], chartIsPie: false, chartIsSeries: false, chartKind: "",
529
- chartPieHidden: true, chartSeriesHidden: true, chartEmptyHidden: true,
537
+ chartPieHidden: true, chartRingHidden: true, chartPlainPieHidden: true, chartLegend: [], chartSeriesHidden: true, chartEmptyHidden: true,
530
538
  asyncSpinnerHidden: true, itemsLoadingHidden: true,
531
539
  chartEmpty: true, description: "", descriptionHidden: true,
532
540
  steps: [], stepsEmpty: true, progress: 0, progressText: "",
@@ -574,11 +582,20 @@ function webOnly(url: string | undefined): string {
574
582
  /** One option, flattened. `selected` is passed in because it is worker state
575
583
  * rather than anything the payload said — except on the first build, where the
576
584
  * service's `isPreselected` seeds it. */
585
+ /** An option that would draw nothing — every line stripped to empty, no picture, badge,
586
+ * benefit or usable button — is dropped rather than rendered as a blank card or an
587
+ * unlabeled row, and does not count toward `renderable`. */
588
+ function hasVisibleContent(row: WidgetOptionRow): boolean {
589
+ return !!(row.title || row.subtitle || row.text || row.pictureUrl || row.badge
590
+ || row.benefits.length || !row.buttonHidden);
591
+ }
592
+
577
593
  function optionRow(
578
594
  o: {
579
595
  value: string; title: string; subtitle: string; text: string;
580
596
  pictureUrl: string; buttonLabel: string; actionUrl: string; listText: string;
581
- respondText: string; actionScreenId?: string; notSupportedText?: string;
597
+ respondText: string; actionScreenId?: string; notSupportedText?: string; badge?: string;
598
+ benefits?: WidgetBenefit[];
582
599
  },
583
600
  i: number,
584
601
  selected: boolean,
@@ -629,22 +646,33 @@ function optionRow(
629
646
  // RADIO_BUTTON option, which may carry `text` and no `title` at all, renders
630
647
  // as an empty row. So fold here rather than in each case's composition: the
631
648
  // raw `text` is still exported below for anything that wants both.
632
- const hasTitle = !!o.title;
633
- const rowTitle = hasTitle ? o.title : (o.text || o.subtitle);
649
+ // "Has a title" is judged on the STRIPPED title: a title of pure markup (`***`) falls back
650
+ // to the option's other prose exactly as a missing one does.
651
+ const strippedTitle = stripMarkdown(o.title);
652
+ const hasTitle = !!strippedTitle;
653
+ const rowTitle = hasTitle ? strippedTitle : stripMarkdown(o.text || o.subtitle);
634
654
  const rowSubtitle = hasTitle
635
- ? (o.subtitle || o.text)
636
- : (o.text && o.subtitle ? o.subtitle : "");
655
+ ? stripMarkdown(o.subtitle || o.text)
656
+ : stripMarkdown(o.text && o.subtitle ? o.subtitle : "");
657
+ // Every `*Hidden` flag follows the STRIPPED value: prose that is only markup
658
+ // (`---`, `***`, an image with empty alt) strips to "" and must not paint an
659
+ // empty node.
660
+ const text = stripMarkdown(o.text);
661
+ const listText = stripMarkdown(o.listText);
662
+ const benefits = (o.benefits ?? []).filter((b) => b.text);
663
+ const badge = stripMarkdown(o.badge || "");
637
664
  return {
638
665
  id: `o${i}`,
639
666
  value: o.value,
640
667
  title: rowTitle,
641
668
  subtitle: rowSubtitle, subtitleHidden: !rowSubtitle,
642
- text: o.text, textHidden: !o.text,
669
+ text, textHidden: !text,
643
670
  pictureUrl, pictureHidden: !pictureUrl,
644
671
  // A label is not enough: the button has to be able to DO something — say
645
672
  // its text, go to its url, or explain why its screen is not here.
646
673
  buttonLabel, buttonHidden: !(buttonLabel && (respondText || actionUrl || actionScreenId)),
647
- listText: o.listText, listHidden: !o.listText,
674
+ listText, benefits, listHidden: !listText && !benefits.length,
675
+ badge, badgeHidden: !badge,
648
676
  selected,
649
677
  respondText,
650
678
  actionUrl,
@@ -757,22 +785,27 @@ export function viewOf(
757
785
  // or screen destination, so that row must be removed.
758
786
  return !rawUrl || isWebUrl(rawUrl);
759
787
  })
760
- : mappedOptions;
788
+ .filter(hasVisibleContent)
789
+ : mappedOptions.filter(hasVisibleContent);
761
790
 
762
791
  // CHART. `pieSlices` is the only shape the catalogue sends, but `chartType`
763
792
  // decides the component: a pie is a `DonutChart` at zero thickness, anything
764
793
  // else a `Chart` over the same magnitudes.
765
794
  const rawSlices = Array.isArray(extra.slices) ? (extra.slices as { label?: unknown; value?: unknown }[]) : [];
766
- const slices: WidgetSlice[] = rawSlices.map((sl) => ({
767
- label: typeof sl.label === "string" ? sl.label : "",
795
+ // The first-party chart cycles the same palette for sectors and legend dots.
796
+ const chartColors = ["ariaChartBlue", "ariaChartGreen", "ariaChartPurple", "ariaChartGray"];
797
+ const slices: WidgetSlice[] = rawSlices.map((sl, i) => ({
798
+ label: typeof sl.label === "string" ? stripMarkdown(sl.label) : "Other",
768
799
  value: typeof sl.value === "number" && Number.isFinite(sl.value) ? sl.value : 0,
800
+ color: chartColors[i % chartColors.length],
769
801
  }));
770
802
  const chartKind = typeof extra.chartType === "string" ? extra.chartType : "";
771
803
  // "doughnut" is the spelling the reference catalogue uses; "donut" is the
772
804
  // component's. Accepting only the component's spelling classified the real
773
805
  // payload as a SERIES and drew it as bars.
774
806
  const isPie = !chartKind || chartKind === "pie" || chartKind === "donut" || chartKind === "doughnut";
775
- const description = typeof extra.description === "string" ? extra.description : "";
807
+ const isRing = chartKind === "donut" || chartKind === "doughnut";
808
+ const description = typeof extra.description === "string" ? stripMarkdown(extra.description) : "";
776
809
 
777
810
  // TODO_PLAN. `status` is the service's word; `done` is what `Timeline` draws.
778
811
  const rawItems = Array.isArray(extra.items) ? (extra.items as { label?: unknown; status?: unknown }[]) : [];
@@ -811,11 +844,16 @@ export function viewOf(
811
844
  // A single-select starts with at most ONE preselected choice, whatever the
812
845
  // payload marked — a radio group showing two ticks is a state the control
813
846
  // cannot otherwise reach.
847
+ // A choice with nothing to show (a markup-only title and no prose) is dropped like any
848
+ // other empty option; the preselection seed counts only choices that are drawn, and each
849
+ // keeps its original index so its id is stable.
814
850
  let seeded = false;
815
- const choices = raw.map((o, j) => {
851
+ const choices: WidgetOptionRow[] = [];
852
+ raw.forEach((o, j) => {
853
+ if (!hasVisibleContent(optionRow(o, j, false, disclose, allowPicture))) return;
816
854
  const pre = !!o.preselected && (type !== "single-select" || !seeded);
817
855
  if (pre) seeded = true;
818
- return optionRow(o, j, pre, disclose, allowPicture);
856
+ choices.push(optionRow(o, j, pre, disclose, allowPicture));
819
857
  });
820
858
  // A question with no choices and no free-text option of its own can still
821
859
  // be answered — text is the only thing it can take — so it gets the field.
@@ -947,6 +985,15 @@ export function viewOf(
947
985
  chartIsPie: w.kind === "CHART" && isPie && slices.length > 0,
948
986
  chartIsSeries: w.kind === "CHART" && !isPie && slices.length > 0,
949
987
  chartPieHidden: !(w.kind === "CHART" && isPie && slices.length > 0),
988
+ chartRingHidden: !(w.kind === "CHART" && isRing && slices.length > 0),
989
+ chartPlainPieHidden: !(w.kind === "CHART" && isPie && !isRing && slices.length > 0),
990
+ // Name AND value per row: the legend is the only place a slice's magnitude
991
+ // is spelled out, and the a11y contract reads both. The worker formats it,
992
+ // not each renderer, through the shared cross-engine formatter.
993
+ // Only the wedges the donut actually draws: every renderer drops a slice
994
+ // whose value is not positive, so its legend row would name nothing.
995
+ chartLegend: slices.filter((s) => s.value > 0).map(({ label, color, value }) => ({ label, color, marker: "dot" as const,
996
+ value: formatNumber(value) })),
950
997
  chartSeriesHidden: !(w.kind === "CHART" && !isPie && slices.length > 0),
951
998
  chartKind,
952
999
  chartEmpty: w.kind === "CHART" && slices.length === 0,
@@ -2259,6 +2259,16 @@ const SCREENS = runtimeSetting({
2259
2259
  "props": {
2260
2260
  "placeholder": "Email",
2261
2261
  "keyboard": "email",
2262
+ "validation": {
2263
+ "required": true,
2264
+ "type": "email",
2265
+ "mode": "on_blur"
2266
+ },
2267
+ "errorPath": "$service:himiStoreAccount.emailError",
2268
+ "submitLabel": "go",
2269
+ "onSubmit": {
2270
+ "actionId": "tier5.himiStoreAccount.submit"
2271
+ },
2262
2272
  "autoCapitalization": "none",
2263
2273
  "focusPath": "$service:himiStoreAccount.emailFocus",
2264
2274
  "onChange": {
@@ -2274,8 +2284,19 @@ const SCREENS = runtimeSetting({
2274
2284
  "type": "TextField",
2275
2285
  "props": {
2276
2286
  "placeholder": "Password",
2277
- "valuePath": "$service:himiStoreAccount.password",
2278
2287
  "secure": true,
2288
+ "visibilityToggle": true,
2289
+ "validation": {
2290
+ "required": true,
2291
+ "mode": "on_blur",
2292
+ "message": "Enter your password."
2293
+ },
2294
+ "errorPath": "$service:himiStoreAccount.passwordError",
2295
+ "focusPath": "$service:himiStoreAccount.passwordFocus",
2296
+ "submitLabel": "go",
2297
+ "onSubmit": {
2298
+ "actionId": "tier5.himiStoreAccount.submit"
2299
+ },
2279
2300
  "autoCapitalization": "none",
2280
2301
  "onChange": {
2281
2302
  "actionId": "tier5.himiStoreAccount.setPassword"
@@ -2286,6 +2307,18 @@ const SCREENS = runtimeSetting({
2286
2307
  "hiddenPath": "$service:himiStoreAccount.isRecovering"
2287
2308
  }
2288
2309
  },
2310
+ {
2311
+ "id": "",
2312
+ "body": {
2313
+ "package": "himalaya",
2314
+ "type": "Text",
2315
+ "props": {
2316
+ "text": "$service:himiStoreAccount.busyMessage",
2317
+ "style": "footnote",
2318
+ "collapseWhenEmpty": true
2319
+ }
2320
+ }
2321
+ },
2289
2322
  {
2290
2323
  "id": "himi_account.message",
2291
2324
  "body": {
@@ -2308,6 +2341,7 @@ const SCREENS = runtimeSetting({
2308
2341
  "title": "Sign in",
2309
2342
  "style": "primary",
2310
2343
  "fullWidth": true,
2344
+ "disabledPath": "$service:himiStoreAccount.authBusy",
2311
2345
  "action": {
2312
2346
  "actionId": "tier5.himiStoreAccount.signIn"
2313
2347
  }
@@ -2326,6 +2360,7 @@ const SCREENS = runtimeSetting({
2326
2360
  "title": "Create account",
2327
2361
  "style": "primary",
2328
2362
  "fullWidth": true,
2363
+ "disabledPath": "$service:himiStoreAccount.authBusy",
2329
2364
  "action": {
2330
2365
  "actionId": "tier5.himiStoreAccount.register"
2331
2366
  }
@@ -2344,6 +2379,7 @@ const SCREENS = runtimeSetting({
2344
2379
  "title": "Send reset link",
2345
2380
  "style": "primary",
2346
2381
  "fullWidth": true,
2382
+ "disabledPath": "$service:himiStoreAccount.authBusy",
2347
2383
  "action": {
2348
2384
  "actionId": "tier5.himiStoreAccount.resetPassword"
2349
2385
  }
@@ -2362,6 +2398,7 @@ const SCREENS = runtimeSetting({
2362
2398
  "title": "Forgot password?",
2363
2399
  "style": "secondary",
2364
2400
  "fullWidth": true,
2401
+ "disabledPath": "$service:himiStoreAccount.authBusy",
2365
2402
  "action": {
2366
2403
  "actionId": "tier5.himiStoreAccount.showRecovery"
2367
2404
  }
@@ -2380,6 +2417,7 @@ const SCREENS = runtimeSetting({
2380
2417
  "title": "Register",
2381
2418
  "style": "secondary",
2382
2419
  "fullWidth": true,
2420
+ "disabledPath": "$service:himiStoreAccount.authBusy",
2383
2421
  "action": {
2384
2422
  "actionId": "tier5.himiStoreAccount.showRegister"
2385
2423
  }
@@ -2398,6 +2436,7 @@ const SCREENS = runtimeSetting({
2398
2436
  "title": "Sign in instead",
2399
2437
  "style": "secondary",
2400
2438
  "fullWidth": true,
2439
+ "disabledPath": "$service:himiStoreAccount.authBusy",
2401
2440
  "action": {
2402
2441
  "actionId": "tier5.himiStoreAccount.showSignIn"
2403
2442
  }
@@ -2416,6 +2455,7 @@ const SCREENS = runtimeSetting({
2416
2455
  "title": "Back to sign in",
2417
2456
  "style": "secondary",
2418
2457
  "fullWidth": true,
2458
+ "disabledPath": "$service:himiStoreAccount.authBusy",
2419
2459
  "action": {
2420
2460
  "actionId": "tier5.himiStoreAccount.showSignIn"
2421
2461
  }
@@ -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