@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.
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/serve-runtime/src/wix-bridge.d.ts +30 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/icon-catalog.generated.d.ts +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/schema.d.ts +2 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/validation.d.ts +1 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/auth/src/copy.generated.d.ts +109 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/auth/src/feedback.d.ts +15 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/auth/src/index.d.ts +3 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/screens/src/index.d.ts +44 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/auth.mjs +304 -18
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/core/validation.mjs +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/core.mjs +72 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/screens.mjs +377 -20
- package/dist/cli.mjs +983 -342
- package/dist/preview-assets.generated.bin +0 -0
- package/guides/component-reference.json +1 -1
- package/guides/component-reference.md +2 -0
- package/guides/headless-loop.md +1 -1
- package/guides/importable-sdks.md +2 -1
- package/guides/one-app-to-himi-app.md +30 -0
- package/guides/one-app-viewer.md +8 -1
- package/guides/sdk-reference.json +92 -1
- package/package.json +2 -2
- package/templates/beauty-salon/dev/index.ts +41 -1
- package/templates/beauty-salon/tier5-src/himi-store-account/index.ts +90 -76
- package/templates/beauty-salon-owner/dev/index.ts +304 -7
- package/templates/beauty-salon-owner/tier5-src/_shared/aria-chat.ts +39 -33
- package/templates/beauty-salon-owner/tier5-src/_shared/aria-widget-model.ts +26 -4
- package/templates/beauty-salon-owner/tier5-src/_shared/aria-widget-view.ts +63 -16
- package/templates/branded-lite/dev/index.ts +41 -1
- package/templates/branded-lite/tier5-src/himi-store-account/index.ts +90 -76
- package/templates/community-club/dev/index.ts +41 -1
- package/templates/community-club/tier5-src/himi-store-account/index.ts +90 -76
- package/templates/community-club-owner/dev/index.ts +304 -7
- package/templates/community-club-owner/tier5-src/_shared/aria-chat.ts +39 -33
- package/templates/community-club-owner/tier5-src/_shared/aria-widget-model.ts +26 -4
- package/templates/community-club-owner/tier5-src/_shared/aria-widget-view.ts +63 -16
- package/templates/events-meetups/dev/index.ts +41 -1
- package/templates/events-meetups/tier5-src/himi-store-account/index.ts +90 -76
- package/templates/events-meetups-owner/dev/index.ts +304 -7
- package/templates/events-meetups-owner/tier5-src/_shared/aria-chat.ts +39 -33
- package/templates/events-meetups-owner/tier5-src/_shared/aria-widget-model.ts +26 -4
- package/templates/events-meetups-owner/tier5-src/_shared/aria-widget-view.ts +63 -16
- package/templates/gym-fitness/dev/index.ts +41 -1
- package/templates/gym-fitness/tier5-src/himi-store-account/index.ts +90 -76
- package/templates/gym-fitness-owner/dev/index.ts +304 -7
- package/templates/gym-fitness-owner/tier5-src/_shared/aria-chat.ts +39 -33
- package/templates/gym-fitness-owner/tier5-src/_shared/aria-widget-model.ts +26 -4
- package/templates/gym-fitness-owner/tier5-src/_shared/aria-widget-view.ts +63 -16
- package/templates/nonprofit-cause/dev/index.ts +41 -1
- package/templates/nonprofit-cause/tier5-src/himi-store-account/index.ts +90 -76
- package/templates/nonprofit-cause-owner/dev/index.ts +304 -7
- package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-chat.ts +39 -33
- package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-widget-model.ts +26 -4
- package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-widget-view.ts +63 -16
- package/templates/owner-lite/dev/index.ts +304 -7
- package/templates/owner-lite/tier5-src/_shared/aria-chat.ts +39 -33
- package/templates/owner-lite/tier5-src/_shared/aria-widget-model.ts +26 -4
- package/templates/owner-lite/tier5-src/_shared/aria-widget-view.ts +63 -16
- package/templates/podcast-creator/dev/index.ts +41 -1
- package/templates/podcast-creator/tier5-src/himi-store-account/index.ts +90 -76
- package/templates/podcast-creator-owner/dev/index.ts +304 -7
- package/templates/podcast-creator-owner/tier5-src/_shared/aria-chat.ts +39 -33
- package/templates/podcast-creator-owner/tier5-src/_shared/aria-widget-model.ts +26 -4
- package/templates/podcast-creator-owner/tier5-src/_shared/aria-widget-view.ts +63 -16
- package/templates/portfolio-creative/dev/index.ts +41 -1
- package/templates/portfolio-creative/tier5-src/himi-store-account/index.ts +90 -76
- package/templates/portfolio-creative-owner/dev/index.ts +304 -7
- package/templates/portfolio-creative-owner/tier5-src/_shared/aria-chat.ts +39 -33
- package/templates/portfolio-creative-owner/tier5-src/_shared/aria-widget-model.ts +26 -4
- package/templates/portfolio-creative-owner/tier5-src/_shared/aria-widget-view.ts +63 -16
- package/templates/restaurant/dev/index.ts +41 -1
- package/templates/restaurant/tier5-src/himi-store-account/index.ts +90 -76
- package/templates/restaurant-owner/dev/index.ts +304 -7
- package/templates/restaurant-owner/tier5-src/_shared/aria-chat.ts +39 -33
- package/templates/restaurant-owner/tier5-src/_shared/aria-widget-model.ts +26 -4
- package/templates/restaurant-owner/tier5-src/_shared/aria-widget-view.ts +63 -16
- package/templates/retail-store/dev/index.ts +41 -1
- package/templates/retail-store/tier5-src/himi-store-account/index.ts +90 -76
- package/templates/retail-store-owner/dev/index.ts +304 -7
- package/templates/retail-store-owner/tier5-src/_shared/aria-chat.ts +39 -33
- package/templates/retail-store-owner/tier5-src/_shared/aria-widget-model.ts +26 -4
- package/templates/retail-store-owner/tier5-src/_shared/aria-widget-view.ts +63 -16
- package/templates/yoga-studio/dev/index.ts +41 -1
- package/templates/yoga-studio/tier5-src/himi-store-account/index.ts +90 -76
- package/templates/yoga-studio-owner/dev/index.ts +304 -7
- package/templates/yoga-studio-owner/tier5-src/_shared/aria-chat.ts +39 -33
- package/templates/yoga-studio-owner/tier5-src/_shared/aria-widget-model.ts +26 -4
- 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
|
|
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
|
-
|
|
633
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
767
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
@@ -3435,6 +3435,16 @@ const SCREENS = runtimeSetting({
|
|
|
3435
3435
|
"props": {
|
|
3436
3436
|
"placeholder": "Email",
|
|
3437
3437
|
"keyboard": "email",
|
|
3438
|
+
"validation": {
|
|
3439
|
+
"required": true,
|
|
3440
|
+
"type": "email",
|
|
3441
|
+
"mode": "on_blur"
|
|
3442
|
+
},
|
|
3443
|
+
"errorPath": "$service:himiStoreAccount.emailError",
|
|
3444
|
+
"submitLabel": "go",
|
|
3445
|
+
"onSubmit": {
|
|
3446
|
+
"actionId": "tier5.himiStoreAccount.submit"
|
|
3447
|
+
},
|
|
3438
3448
|
"autoCapitalization": "none",
|
|
3439
3449
|
"focusPath": "$service:himiStoreAccount.emailFocus",
|
|
3440
3450
|
"onChange": {
|
|
@@ -3450,8 +3460,19 @@ const SCREENS = runtimeSetting({
|
|
|
3450
3460
|
"type": "TextField",
|
|
3451
3461
|
"props": {
|
|
3452
3462
|
"placeholder": "Password",
|
|
3453
|
-
"valuePath": "$service:himiStoreAccount.password",
|
|
3454
3463
|
"secure": true,
|
|
3464
|
+
"visibilityToggle": true,
|
|
3465
|
+
"validation": {
|
|
3466
|
+
"required": true,
|
|
3467
|
+
"mode": "on_blur",
|
|
3468
|
+
"message": "Enter your password."
|
|
3469
|
+
},
|
|
3470
|
+
"errorPath": "$service:himiStoreAccount.passwordError",
|
|
3471
|
+
"focusPath": "$service:himiStoreAccount.passwordFocus",
|
|
3472
|
+
"submitLabel": "go",
|
|
3473
|
+
"onSubmit": {
|
|
3474
|
+
"actionId": "tier5.himiStoreAccount.submit"
|
|
3475
|
+
},
|
|
3455
3476
|
"autoCapitalization": "none",
|
|
3456
3477
|
"onChange": {
|
|
3457
3478
|
"actionId": "tier5.himiStoreAccount.setPassword"
|
|
@@ -3462,6 +3483,18 @@ const SCREENS = runtimeSetting({
|
|
|
3462
3483
|
"hiddenPath": "$service:himiStoreAccount.isRecovering"
|
|
3463
3484
|
}
|
|
3464
3485
|
},
|
|
3486
|
+
{
|
|
3487
|
+
"id": "",
|
|
3488
|
+
"body": {
|
|
3489
|
+
"package": "himalaya",
|
|
3490
|
+
"type": "Text",
|
|
3491
|
+
"props": {
|
|
3492
|
+
"text": "$service:himiStoreAccount.busyMessage",
|
|
3493
|
+
"style": "footnote",
|
|
3494
|
+
"collapseWhenEmpty": true
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3497
|
+
},
|
|
3465
3498
|
{
|
|
3466
3499
|
"id": "himi_account.message",
|
|
3467
3500
|
"body": {
|
|
@@ -3484,6 +3517,7 @@ const SCREENS = runtimeSetting({
|
|
|
3484
3517
|
"title": "Sign in",
|
|
3485
3518
|
"style": "primary",
|
|
3486
3519
|
"fullWidth": true,
|
|
3520
|
+
"disabledPath": "$service:himiStoreAccount.authBusy",
|
|
3487
3521
|
"action": {
|
|
3488
3522
|
"actionId": "tier5.himiStoreAccount.signIn"
|
|
3489
3523
|
}
|
|
@@ -3502,6 +3536,7 @@ const SCREENS = runtimeSetting({
|
|
|
3502
3536
|
"title": "Create account",
|
|
3503
3537
|
"style": "primary",
|
|
3504
3538
|
"fullWidth": true,
|
|
3539
|
+
"disabledPath": "$service:himiStoreAccount.authBusy",
|
|
3505
3540
|
"action": {
|
|
3506
3541
|
"actionId": "tier5.himiStoreAccount.register"
|
|
3507
3542
|
}
|
|
@@ -3520,6 +3555,7 @@ const SCREENS = runtimeSetting({
|
|
|
3520
3555
|
"title": "Send reset link",
|
|
3521
3556
|
"style": "primary",
|
|
3522
3557
|
"fullWidth": true,
|
|
3558
|
+
"disabledPath": "$service:himiStoreAccount.authBusy",
|
|
3523
3559
|
"action": {
|
|
3524
3560
|
"actionId": "tier5.himiStoreAccount.resetPassword"
|
|
3525
3561
|
}
|
|
@@ -3538,6 +3574,7 @@ const SCREENS = runtimeSetting({
|
|
|
3538
3574
|
"title": "Forgot password?",
|
|
3539
3575
|
"style": "secondary",
|
|
3540
3576
|
"fullWidth": true,
|
|
3577
|
+
"disabledPath": "$service:himiStoreAccount.authBusy",
|
|
3541
3578
|
"action": {
|
|
3542
3579
|
"actionId": "tier5.himiStoreAccount.showRecovery"
|
|
3543
3580
|
}
|
|
@@ -3556,6 +3593,7 @@ const SCREENS = runtimeSetting({
|
|
|
3556
3593
|
"title": "Register",
|
|
3557
3594
|
"style": "secondary",
|
|
3558
3595
|
"fullWidth": true,
|
|
3596
|
+
"disabledPath": "$service:himiStoreAccount.authBusy",
|
|
3559
3597
|
"action": {
|
|
3560
3598
|
"actionId": "tier5.himiStoreAccount.showRegister"
|
|
3561
3599
|
}
|
|
@@ -3574,6 +3612,7 @@ const SCREENS = runtimeSetting({
|
|
|
3574
3612
|
"title": "Sign in instead",
|
|
3575
3613
|
"style": "secondary",
|
|
3576
3614
|
"fullWidth": true,
|
|
3615
|
+
"disabledPath": "$service:himiStoreAccount.authBusy",
|
|
3577
3616
|
"action": {
|
|
3578
3617
|
"actionId": "tier5.himiStoreAccount.showSignIn"
|
|
3579
3618
|
}
|
|
@@ -3592,6 +3631,7 @@ const SCREENS = runtimeSetting({
|
|
|
3592
3631
|
"title": "Back to sign in",
|
|
3593
3632
|
"style": "secondary",
|
|
3594
3633
|
"fullWidth": true,
|
|
3634
|
+
"disabledPath": "$service:himiStoreAccount.authBusy",
|
|
3595
3635
|
"action": {
|
|
3596
3636
|
"actionId": "tier5.himiStoreAccount.showSignIn"
|
|
3597
3637
|
}
|
|
@@ -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
|
-
|
|
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
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return derive({ ...
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
/**
|
|
184
|
-
*
|
|
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 =
|
|
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
|
|
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
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
|
|
471
|
-
|
|
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
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
message: copyParam("storeAccount.resetSent", "If that email is registered, a reset link is on its way.")
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
-
|
|
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
|