@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
Binary file
@@ -205,7 +205,7 @@
205
205
  ]
206
206
  }
207
207
  ],
208
- "propsSource": "export interface TextFieldProps {\n placeholder?: string;\n /** LITERAL initial text. Renderers paint it verbatim — a `$service:` sentinel\n * here shows the raw token to the user. Bind `valuePath` instead. */\n value?: string;\n /** `$service:`/`$item.` binding → live text from the worker (overrides `value`).\n * Adopted while the field is NOT focused; an empty worker value (a draft reset\n * after send/post/submit) is adopted even mid-focus, so a cleared draft empties\n * the field without the worker ever fighting the caret. */\n valuePath?: string;\n onChange?: Action;\n /** Text input enrichments. */\n keyboard?: Keyboard;\n secure?: boolean;\n multilineMaxLines?: number;\n maxLength?: number;\n autoCapitalization?: AutoCapitalization;\n onSubmit?: Action;\n /** Keyboard control. `suggestions: \"off\"` kills autocorrect + the suggestion\n * strip. `submitLabel` paints the return key and, with `onSubmit`, fires it —\n * submitting dismisses the keyboard except for `\"next\"`, which keeps it up so\n * a worker can move focus on. */\n suggestions?: Suggestions;\n submitLabel?: SubmitLabel;\n /** `$service:` binding → a TOKEN the worker changes to focus this field. Focus\n * is requested whenever the resolved value becomes a new NON-EMPTY value\n * (the first seed included, so a non-empty init focuses on open). Use a\n * monotonic nonce — `String(Date.now())` — since a latched boolean cannot\n * re-fire. Blur is the keyboard-dismiss capability, not a token here. */\n focusPath?: string;\n /** Accessory & state enrichments. */\n leadingIcon?: IconRef;\n trailingIcon?: IconRef;\n clearButton?: boolean;\n helperText?: string;\n error?: boolean;\n prefix?: string;\n suffix?: string;\n /** Drop the default rounded-border chrome so a caller background shows (composer pill). */\n borderless?: boolean;\n /** Declarative syntactic rules the RENDERER evaluates, so they hold with no\n * worker running (Tier 0-4) and with no bridge round-trip. Before this,\n * validation was reachable only by wrapping the field in `Form` -> `FormField`\n * - the shape the design guide steers you away from for hero flows. */\n validation?: Validation;\n /** `$service:`/`$item.` binding -> a worker-computed error string. Non-empty =\n * invalid. This is how SEMANTIC and CROSS-FIELD errors (\"passwords don't\n * match\", \"email taken\") reach the same error slot, with the same styling and\n * a11y wiring, as the declarative ones. Wins over `validation.message`. */\n errorPath?: string;\n /** LITERAL error string for a server-rendered screen with no worker to bind.\n * Lower precedence than `errorPath`, higher than the rules. */\n errorText?: string;\n}"
208
+ "propsSource": "export interface TextFieldProps {\n placeholder?: string;\n /** LITERAL initial text. Renderers paint it verbatim — a `$service:` sentinel\n * here shows the raw token to the user. Bind `valuePath` instead. */\n value?: string;\n /** `$service:`/`$item.` binding → live text from the worker (overrides `value`).\n * Adopted while the field is NOT focused; an empty worker value (a draft reset\n * after send/post/submit) is adopted even mid-focus, so a cleared draft empties\n * the field without the worker ever fighting the caret. */\n valuePath?: string;\n onChange?: Action;\n /** Text input enrichments. */\n keyboard?: Keyboard;\n secure?: boolean;\n multilineMaxLines?: number;\n maxLength?: number;\n autoCapitalization?: AutoCapitalization;\n onSubmit?: Action;\n /** Keyboard control. `suggestions: \"off\"` kills autocorrect + the suggestion\n * strip. `submitLabel` paints the return key and, with `onSubmit`, fires it —\n * submitting dismisses the keyboard except for `\"next\"`, which keeps it up so\n * a worker can move focus on. */\n suggestions?: Suggestions;\n submitLabel?: SubmitLabel;\n /** `$service:` binding → a TOKEN the worker changes to focus this field. Focus\n * is requested whenever the resolved value becomes a new NON-EMPTY value\n * (the first seed included, so a non-empty init focuses on open). Use a\n * monotonic nonce — `String(Date.now())` — since a latched boolean cannot\n * re-fire. Blur is the keyboard-dismiss capability, not a token here. */\n focusPath?: string;\n /** Accessory & state enrichments. */\n leadingIcon?: IconRef;\n trailingIcon?: IconRef;\n clearButton?: boolean;\n helperText?: string;\n error?: boolean;\n prefix?: string;\n suffix?: string;\n /** Drop the default rounded-border chrome so a caller background shows (composer pill). */\n borderless?: boolean;\n /** Declarative syntactic rules the RENDERER evaluates, so they hold with no\n * worker running (Tier 0-4) and with no bridge round-trip. Before this,\n * validation was reachable only by wrapping the field in `Form` -> `FormField`\n * - the shape the design guide steers you away from for hero flows. */\n validation?: Validation;\n /** `$service:`/`$item.` binding -> a worker-computed error string. Non-empty =\n * invalid. This is how SEMANTIC and CROSS-FIELD errors (\"passwords don't\n * match\", \"email taken\") reach the same error slot, with the same styling and\n * a11y wiring, as the declarative ones. Wins over `validation.message`. */\n errorPath?: string;\n /** LITERAL error string for a server-rendered screen with no worker to bind.\n * Lower precedence than `errorPath`, higher than the rules. */\n errorText?: string;\n /** Opt-in accessible show/hide button. Only applies to secure fields; initially masked. */\n visibilityToggle?: boolean;\n}"
209
209
  },
210
210
  {
211
211
  "name": "Toggle",
@@ -601,6 +601,8 @@ export interface TextFieldProps {
601
601
  /** LITERAL error string for a server-rendered screen with no worker to bind.
602
602
  * Lower precedence than `errorPath`, higher than the rules. */
603
603
  errorText?: string;
604
+ /** Opt-in accessible show/hide button. Only applies to secure fields; initially masked. */
605
+ visibilityToggle?: boolean;
604
606
  }
605
607
  ```
606
608
 
@@ -229,7 +229,7 @@ A few flags worth knowing up front (all discoverable via `himi render --help`):
229
229
  which over-stretches a phone layout.
230
230
  - **`himi icons`** lists the valid `Icon.name` vocabulary — an unknown name silently falls back to a
231
231
  generic glyph on device (and `validate` warns), so pick from this list rather than guessing SF-Symbol
232
- names. The full set (aliases included): ai, arrow_down, arrow_left, arrow_right, arrow_up, back, bag, bag_fill, barcode, bell, bell_fill, bell_off, bell_slash, book, bookmark, bookmark_fill, boxes, branch, calendar, camera, cart, cart_fill, chart, chat, check, checklist, checkmark, chevron_down, chevron_left, chevron_right, chevron_up, circle, circle_fill, clock, close, cookie, copy, dashboard, doc, document, download, edit, ellipsis, error, expand, external_link, filter, folder, forward, fullscreen, gear, globe, grid, grid_fill, grip, heart, heart_fill, help, home, home_fill, house, image, inbox, info, layers, link, list, location, lock, logout, mail, map, menu, message, mic, mic_fill, microphone, microphone_fill, minus, moon, more, more_vertical, music, newspaper, newspaper_fill, next, pause, pencil, people, person, person_fill, phone, photo, pin, play, play_circle, play_circle_fill, plus, plus_circle, plus_circle_fill, prev, question, refresh, repeat, rss, search, send, send_arrow, settings, settings_fill, share, shuffle, skip_back, skip_forward, skip_next, skip_previous, star, star_fill, stop, success, tag, ticket, ticket_fill, trash, tray, undo, user, visible, volume, warning, wifi, xmark.
232
+ names. The full set (aliases included): ai, arrow_down, arrow_left, arrow_right, arrow_up, back, bag, bag_fill, barcode, bell, bell_fill, bell_off, bell_slash, book, bookmark, bookmark_fill, boxes, branch, calendar, camera, cart, cart_fill, chart, chat, check, checklist, checkmark, chevron_down, chevron_left, chevron_right, chevron_up, circle, circle_fill, clock, close, concealed, cookie, copy, dashboard, doc, document, download, edit, ellipsis, error, expand, external_link, eye, eye_slash, filter, folder, forward, fullscreen, gear, gift, globe, grid, grid_fill, grip, heart, heart_fill, help, home, home_fill, house, image, inbox, info, layers, link, list, location, lock, logout, mail, map, menu, message, mic, mic_fill, microphone, microphone_fill, minus, moon, more, more_vertical, music, newspaper, newspaper_fill, next, paper_plane, pause, pencil, people, person, person_fill, phone, photo, pin, play, play_circle, play_circle_fill, plus, plus_circle, plus_circle_fill, prev, question, refresh, repeat, rss, search, send, send_arrow, settings, settings_fill, share, shuffle, skip_back, skip_forward, skip_next, skip_previous, star, star_fill, stop, success, tag, ticket, ticket_fill, trash, tray, undo, user, visible, volume, warning, wifi, xmark.
233
233
 
234
234
  ## 4b. Prove it WORKS, not just that it renders (`himi test`, when installed)
235
235
 
@@ -16,6 +16,7 @@ package in one greppable file) or `GET /v1/sdk/<name>` over HTTP; in the CLI,
16
16
  - `@himalaya/agent` — Tier-5 agent integration.
17
17
  - `@himalaya/app-info` — Tier-5 typed binding for this app's OWN build + OTA metadata.
18
18
  - `@himalaya/audio-record` — Tier-5 typed binding for microphone recording.
19
+ - `@himalaya/auth` — —
19
20
  - `@himalaya/base44` — Tier-5 typed binding for a Base44 app's backend.
20
21
  - `@himalaya/biometrics` — Tier-5 typed binding to the platform's biometric authentication capability.
21
22
  - `@himalaya/browser-auth` — Tier-5 typed binding for native browser SSO.
@@ -50,7 +51,7 @@ package in one greppable file) or `GET /v1/sdk/<name>` over HTTP; in the CLI,
50
51
  - `@himalaya/presigned-upload` — Tier-5 typed binding for a RAW binary upload to a pre-signed, self-authorizing URL.
51
52
  - `@himalaya/push` — Tier-5 typed binding to the platform's push capability.
52
53
  - `@himalaya/release-control` — Tier-5 typed binding for the on-device OTA release picker.
53
- - `@himalaya/screens` — @wix/himalaya/screens — ready-made sign-in and paywall screens.
54
+ - `@himalaya/screens` — @wix/himalaya/screens — ready-made screens and shared header compositions.
54
55
  - `@himalaya/sdk-types` — shared types for the Tier-5 SDK capability surface.
55
56
  - `@himalaya/secure-storage` — Tier-5 typed binding to the platform's secure-storage capability.
56
57
  - `@himalaya/share` — Tier-5 typed binding for the system share sheet.
@@ -110,11 +110,41 @@ Apply these interpretation rules:
110
110
  - Reconstruct effective theme, fonts, header, wrapper titles/CTAs, spacing,
111
111
  image behavior and settings versions using the viewer rules. Distinguish a
112
112
  wrapper heading from a Title section, and margin sentinels from negative padding.
113
+ - Resolve the effective top bar for every target screen. Reuse one `TopBarConfig`
114
+ value or builder for screens with the same actions and style. Use a screen title
115
+ only when the resolved source top-bar settings show a title and Structure supplies
116
+ its value; leave it empty when the source does not show a title or has no title.
117
+ Do not create a title from the app name, business name, route ID, tab label or
118
+ section content. Keep a custom page header separate from the shell top bar.
113
119
  - Keep navigation visibility, reachability and data access separate. Reevaluate
114
120
  membership/plan/badge gates at runtime; an operator's successful read is not an
115
121
  end-user access policy. Never expose protected content beneath a visual gate.
116
122
  - Exclude `ntt.*` definitions and placements from coverage. Preserve configured
117
123
  header/top-bar intent; do not add unconfigured drawer or header surfaces.
124
+ - For each configured Detailed header, import and call the shared
125
+ `detailedHeader` builder from `@wix/himalaya/screens`. Do not recreate its
126
+ component tree in the app. Resolve the source fallback and visibility rules
127
+ first, then pass the title, subtitle, first cover image, logo, permitted member
128
+ data, location label and configured actions. Set `collapsible: false` when the
129
+ source header is not collapsible. The shared hero supports one cover image; list
130
+ additional source carousel images as an adaptation gap.
131
+
132
+ ```ts
133
+ import { detailedHeader } from "@wix/himalaya/screens";
134
+
135
+ const header = detailedHeader({
136
+ id: "home.header",
137
+ title: "Studio name",
138
+ subtitle: "Source subtitle",
139
+ imageUrl: "https://example.test/cover.jpg",
140
+ primaryAction: {
141
+ label: "Join",
142
+ action: { actionId: "himi.open", params: { screenId: "membership" } },
143
+ },
144
+ });
145
+ ```
146
+
147
+ Add this returned component to the screen tree at the source header position.
118
148
  - Business context contains only `phoneNumber`, `website`, and `services`, with
119
149
  their permitted nested values. Structure wins on authored phone/URL/design.
120
150
  Installed services do not add features. Installation IDs are not record IDs.
@@ -122,6 +122,13 @@ Follow section 5.1 of the Structure inference guide in full. In particular:
122
122
 
123
123
  Translate the resolved result to Himi. Its inspected schema says a per-screen `top_bar` **fully replaces** the app `top_bar_default`. Therefore compute a complete effective bar before emitting an override; do not emit a partial object expecting a field-by-field merge. The target shell owns its back/menu slot. [Himi navigation schema](https://github.com/wix-private/himalaya/blob/d3bc68c77151b678d337323615eb9889fa8e38c3/core/schema/core.proto)
124
124
 
125
+ ### R08a. Reuse matching bars and keep titles optional
126
+
127
+ - Resolve the effective source bar for every target screen, including screens created by a navigation conversion. Do not assume that a shared source header means every screen has the same effective bar.
128
+ - When screens have the same effective actions and style, use one shared `TopBarConfig` value or builder for them. Use it as `top_bar_default` when it is the app-wide bar. Use a complete per-screen `top_bar` only when a screen differs.
129
+ - Himi reads the bar title from `ScreenDescriptor.title`. Set this field only when the resolved source top-bar settings show a title and Structure supplies that title. For a dynamic source title, use the title resolved by the viewer rules. If the source does not show a title or has no title value, leave the target title empty. Do not invent a title from the app name, business name, route ID, tab label, or section content.
130
+ - A source custom header is page content. It is not the shell's top bar. Keep the two surfaces separate. Reuse a page-header component only when multiple screens share that configured header content.
131
+
125
132
  ### R09. Treat header content as conditional, configured behavior
126
133
 
127
134
  Use supplied header inputs within the agreed shared-header scope. If the source config requires content beyond that scope, describe it and the needed target adaptation before expanding the implementation.
@@ -135,7 +142,7 @@ Relevant source behavior to preserve when applicable:
135
142
  - Logo, invite button, CTA, member count/avatars, and business-profile entry point have separate controls. A CTA needs both its configuration and its show flag.
136
143
  - Member visibility and restaurant/location behavior depend on runtime services and permissions. Structure does not contain the member records needed to populate them; the restricted business export is not a member directory.
137
144
 
138
- Himi's `CollapsingHeaderProps` is a useful primitive with one hero image, title/subtitle, heights, and children. Its existence does not establish support for a source cover carousel, member strip, invite flow, or location switcher. Compose verified capabilities or record the gap. [Header domain](https://github.com/wix-private/mobile-modules/blob/3a9713cf8bbb014ce4d3006d356a82a0f02db007/packages/libraries/pilates/src/domain/Header.ts) · [Header view model](https://github.com/wix-private/mobile-modules/blob/3a9713cf8bbb014ce4d3006d356a82a0f02db007/packages/libraries/pilates/src/elements/PilatesHeader/useViewModel.ts) · [Detailed header presenter](https://github.com/wix-private/mobile-modules/blob/3a9713cf8bbb014ce4d3006d356a82a0f02db007/packages/modules/wix-one-app-member-view/src/newStructure/elements/headers/detailedHeader/usePresenter.ts) · [Himi schema](https://github.com/wix-private/himalaya/blob/d3bc68c77151b678d337323615eb9889fa8e38c3/core/schema/core.proto)
145
+ For a configured Detailed header, use `detailedHeader` from `@wix/himalaya/screens`. This shared builder uses Himalaya's cross-platform `CollapsingHeader`, a rounded detail card, and standard logo, member, and button components. Every migration must call this builder. Do not copy its component tree into an app. Pass the resolved title, subtitle, first cover image, logo, allowed member data, location label, configured actions, and the page nodes in `content`. Put the remaining screen content in `content` so it scrolls beneath the hero and the hero collapses with the page. Do not place those nodes as siblings of the returned header. Keep member data hidden when the source permissions or runtime services do not allow it. The current hero supports one cover image; keep the source order and record extra carousel images as an adaptation gap. If `isCollapsible` is false, pass `collapsible: false` so the hero keeps one height. [Header domain](https://github.com/wix-private/mobile-modules/blob/3a9713cf8bbb014ce4d3006d356a82a0f02db007/packages/libraries/pilates/src/domain/Header.ts) · [Header view model](https://github.com/wix-private/mobile-modules/blob/3a9713cf8bbb014ce4d3006d356a82a0f02db007/packages/libraries/pilates/src/elements/PilatesHeader/useViewModel.ts) · [Detailed header presenter](https://github.com/wix-private/mobile-modules/blob/3a9713cf8bbb014ce4d3006d356a82a0f02db007/packages/modules/wix-one-app-member-view/src/newStructure/elements/headers/detailedHeader/usePresenter.ts) · [Himi schema](https://github.com/wix-private/himalaya/blob/d3bc68c77151b678d337323615eb9889fa8e38c3/core/schema/core.proto)
139
146
 
140
147
  ## Step 4 — Recreate each section envelope before its content
141
148
 
@@ -309,6 +309,79 @@
309
309
  }
310
310
  ]
311
311
  },
312
+ {
313
+ "name": "@himalaya/auth",
314
+ "source": "stdlib/flows/auth/src/index.ts",
315
+ "description": "",
316
+ "api": [
317
+ {
318
+ "name": "DEFAULT_AUTH_PREFIX",
319
+ "kind": "const",
320
+ "jsdoc": "/** Action-id prefix the host registered the Wix auth actions under. Matches\n * `WixConsumerAuthActions.register(actionIdPrefix:)` — default `himi.wix`. */",
321
+ "decl": "export const DEFAULT_AUTH_PREFIX = \"himi.wix\";"
322
+ },
323
+ {
324
+ "name": "AuthOptions",
325
+ "kind": "interface",
326
+ "jsdoc": "",
327
+ "decl": "export interface AuthOptions {\n /** Override when the host registered a non-default prefix. */\n prefix?: string;\n}"
328
+ },
329
+ {
330
+ "name": "AuthResult",
331
+ "kind": "type",
332
+ "jsdoc": "/** The outcome of an auth attempt. `ok: false` always carries a showable reason. */",
333
+ "decl": "export type AuthResult =\n | { ok: true; email: string }\n | { ok: false; reason: string; code?: AuthFailureCode };"
334
+ },
335
+ {
336
+ "name": "AuthState",
337
+ "kind": "interface",
338
+ "jsdoc": "/** Who the session belongs to right now. `signedIn` is false for a visitor. */",
339
+ "decl": "export interface AuthState {\n signedIn: boolean;\n /** \"\" for a visitor — never a stale or typed-in address. */\n email: string;\n}"
340
+ },
341
+ {
342
+ "name": "authState",
343
+ "kind": "function",
344
+ "jsdoc": "/**\n * Who is signed in, according to the HOST — not according to what the user typed.\n *\n * This is the source of truth for every gate. It asks the native session, so it\n * stays right across screens and across a sign-out that happened elsewhere.\n * Never throws: an unreachable host means \"visitor\", which fails closed.\n */",
345
+ "decl": "export async function authState(opts?: AuthOptions): Promise<AuthState>;"
346
+ },
347
+ {
348
+ "name": "signedInEmail",
349
+ "kind": "function",
350
+ "jsdoc": "/** Convenience: the signed-in member's email, or \"\" for a visitor. */",
351
+ "decl": "export async function signedInEmail(opts?: AuthOptions): Promise<string>;"
352
+ },
353
+ {
354
+ "name": "signInDirect",
355
+ "kind": "function",
356
+ "jsdoc": "/**\n * Sign in with an email + password the member typed.\n *\n * The credentials go straight to the native handler and never touch worker\n * state — a T5 bundle has no sanctioned path to any other credential endpoint,\n * and this module does not add one.\n */",
357
+ "decl": "export function signInDirect(\n email: string,\n password: string,\n opts?: AuthOptions,\n): Promise<AuthResult>;"
358
+ },
359
+ {
360
+ "name": "signIn",
361
+ "kind": "function",
362
+ "jsdoc": "/** Sign in through the host's own auth UI (redirect / hosted flow). */",
363
+ "decl": "export function signIn(opts?: AuthOptions): Promise<AuthResult>;"
364
+ },
365
+ {
366
+ "name": "registerMember",
367
+ "kind": "function",
368
+ "jsdoc": "/** Create a member and sign them in, in one step. */",
369
+ "decl": "export function registerMember(\n email: string,\n password: string,\n profile?: { firstName?: string; lastName?: string },\n opts?: AuthOptions,\n): Promise<AuthResult>;"
370
+ },
371
+ {
372
+ "name": "resetPassword",
373
+ "kind": "function",
374
+ "jsdoc": "/** Send the member a reset link. Reports whether the request was accepted —\n * never whether an account exists, which is not ours to disclose. */",
375
+ "decl": "export async function resetPassword(email: string, opts?: AuthOptions): Promise<AuthResult>;"
376
+ },
377
+ {
378
+ "name": "signOut",
379
+ "kind": "function",
380
+ "jsdoc": "/**\n * Sign out, and confirm the session is actually gone.\n *\n * The host is expected to purge per-account worker state as part of this (the\n * `onSignOutPurge` contract) so the next member never renders the last one's\n * rows. This returns false if a session somehow survives, so a caller does not\n * paint a signed-out UI over a live session.\n */",
381
+ "decl": "export async function signOut(opts?: AuthOptions): Promise<boolean>;"
382
+ }
383
+ ]
384
+ },
312
385
  {
313
386
  "name": "@himalaya/base44",
314
387
  "source": "core/sdk/base44/src/index.ts",
@@ -3865,8 +3938,26 @@
3865
3938
  {
3866
3939
  "name": "@himalaya/screens",
3867
3940
  "source": "stdlib/flows/screens/src/index.ts",
3868
- "description": "@wix/himalaya/screens — ready-made sign-in and paywall screens.",
3941
+ "description": "@wix/himalaya/screens — ready-made screens and shared header compositions.",
3869
3942
  "api": [
3943
+ {
3944
+ "name": "DetailedHeaderAction",
3945
+ "kind": "interface",
3946
+ "jsdoc": "/** An action shown by `detailedHeader`. */",
3947
+ "decl": "export interface DetailedHeaderAction {\n label: string;\n action: Node;\n style?: \"primary\" | \"secondary\" | \"plain\";\n}"
3948
+ },
3949
+ {
3950
+ "name": "DetailedHeaderOptions",
3951
+ "kind": "interface",
3952
+ "jsdoc": "/** Options for the shared One App-style detailed header composition. */",
3953
+ "decl": "export interface DetailedHeaderOptions {\n /** Prefix for stable SDUI node IDs. Use a unique value for each header. */\n id?: string;\n /** Page nodes that scroll beneath the hero and detail card. */\n content?: Node[];\n /** Cover image selected by the migration from the source cover list. */\n imageUrl?: string;\n title: string;\n subtitle?: string;\n logoUrl?: string;\n locationLabel?: string;\n locationAction?: DetailedHeaderAction;\n memberCaption?: string;\n memberAvatars?: Array<{ imageUrl?: string; initials?: string }>;\n /** Live member rows from a service. This takes precedence over `memberAvatars`. */\n memberAvatarsPath?: string;\n inviteAction?: DetailedHeaderAction;\n primaryAction?: DetailedHeaderAction;\n aboutAction?: DetailedHeaderAction;\n /** Keep the hero at one height when the source header is not collapsible. */\n collapsible?: boolean;\n expandedHeight?: number;\n collapsedHeight?: number;\n}"
3954
+ },
3955
+ {
3956
+ "name": "detailedHeader",
3957
+ "kind": "function",
3958
+ "jsdoc": "/**\n * Build the shared Detailed header used by One App migrations.\n *\n * The header uses Himalaya's cross-platform `CollapsingHeader` and standard\n * components for the detail card, logo, member strip, and actions. A migration\n * calls this builder instead of copying header layout into each app. Himalaya's\n * hero accepts one image, so pass the first source cover image here.\n */",
3959
+ "decl": "export function detailedHeader(opts: DetailedHeaderOptions): Node;"
3960
+ },
3870
3961
  {
3871
3962
  "name": "ScreenEnvelope",
3872
3963
  "kind": "interface",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.1397.0",
3
+ "version": "0.1399.0",
4
4
  "type": "module",
5
5
  "description": "himi — the Himalaya authoring CLI. Build a Tier-5 content package on your own machine, push it to the production serve as a draft, preview it on web + Himi Player, and read worker logs back — the remote dev loop. Wraps @himalaya/serve-cli for transport.",
6
6
  "keywords": [
@@ -71,5 +71,5 @@
71
71
  "artifactId": "himalaya-cli"
72
72
  }
73
73
  },
74
- "falconPackageHash": "753d96dbe50af40fa4d14f676d64000cbc0d6e3510f65668e958c3e4"
74
+ "falconPackageHash": "5602cafea0f9e92feb51eb02932446cafa4658b6e7397be9ae1377cb"
75
75
  }
@@ -2518,6 +2518,16 @@ const SCREENS = runtimeSetting({
2518
2518
  "props": {
2519
2519
  "placeholder": "Email",
2520
2520
  "keyboard": "email",
2521
+ "validation": {
2522
+ "required": true,
2523
+ "type": "email",
2524
+ "mode": "on_blur"
2525
+ },
2526
+ "errorPath": "$service:himiStoreAccount.emailError",
2527
+ "submitLabel": "go",
2528
+ "onSubmit": {
2529
+ "actionId": "tier5.himiStoreAccount.submit"
2530
+ },
2521
2531
  "autoCapitalization": "none",
2522
2532
  "focusPath": "$service:himiStoreAccount.emailFocus",
2523
2533
  "onChange": {
@@ -2533,8 +2543,19 @@ const SCREENS = runtimeSetting({
2533
2543
  "type": "TextField",
2534
2544
  "props": {
2535
2545
  "placeholder": "Password",
2536
- "valuePath": "$service:himiStoreAccount.password",
2537
2546
  "secure": true,
2547
+ "visibilityToggle": true,
2548
+ "validation": {
2549
+ "required": true,
2550
+ "mode": "on_blur",
2551
+ "message": "Enter your password."
2552
+ },
2553
+ "errorPath": "$service:himiStoreAccount.passwordError",
2554
+ "focusPath": "$service:himiStoreAccount.passwordFocus",
2555
+ "submitLabel": "go",
2556
+ "onSubmit": {
2557
+ "actionId": "tier5.himiStoreAccount.submit"
2558
+ },
2538
2559
  "autoCapitalization": "none",
2539
2560
  "onChange": {
2540
2561
  "actionId": "tier5.himiStoreAccount.setPassword"
@@ -2545,6 +2566,18 @@ const SCREENS = runtimeSetting({
2545
2566
  "hiddenPath": "$service:himiStoreAccount.isRecovering"
2546
2567
  }
2547
2568
  },
2569
+ {
2570
+ "id": "",
2571
+ "body": {
2572
+ "package": "himalaya",
2573
+ "type": "Text",
2574
+ "props": {
2575
+ "text": "$service:himiStoreAccount.busyMessage",
2576
+ "style": "footnote",
2577
+ "collapseWhenEmpty": true
2578
+ }
2579
+ }
2580
+ },
2548
2581
  {
2549
2582
  "id": "himi_account.message",
2550
2583
  "body": {
@@ -2567,6 +2600,7 @@ const SCREENS = runtimeSetting({
2567
2600
  "title": "Sign in",
2568
2601
  "style": "primary",
2569
2602
  "fullWidth": true,
2603
+ "disabledPath": "$service:himiStoreAccount.authBusy",
2570
2604
  "action": {
2571
2605
  "actionId": "tier5.himiStoreAccount.signIn"
2572
2606
  }
@@ -2585,6 +2619,7 @@ const SCREENS = runtimeSetting({
2585
2619
  "title": "Create account",
2586
2620
  "style": "primary",
2587
2621
  "fullWidth": true,
2622
+ "disabledPath": "$service:himiStoreAccount.authBusy",
2588
2623
  "action": {
2589
2624
  "actionId": "tier5.himiStoreAccount.register"
2590
2625
  }
@@ -2603,6 +2638,7 @@ const SCREENS = runtimeSetting({
2603
2638
  "title": "Send reset link",
2604
2639
  "style": "primary",
2605
2640
  "fullWidth": true,
2641
+ "disabledPath": "$service:himiStoreAccount.authBusy",
2606
2642
  "action": {
2607
2643
  "actionId": "tier5.himiStoreAccount.resetPassword"
2608
2644
  }
@@ -2621,6 +2657,7 @@ const SCREENS = runtimeSetting({
2621
2657
  "title": "Forgot password?",
2622
2658
  "style": "secondary",
2623
2659
  "fullWidth": true,
2660
+ "disabledPath": "$service:himiStoreAccount.authBusy",
2624
2661
  "action": {
2625
2662
  "actionId": "tier5.himiStoreAccount.showRecovery"
2626
2663
  }
@@ -2639,6 +2676,7 @@ const SCREENS = runtimeSetting({
2639
2676
  "title": "Register",
2640
2677
  "style": "secondary",
2641
2678
  "fullWidth": true,
2679
+ "disabledPath": "$service:himiStoreAccount.authBusy",
2642
2680
  "action": {
2643
2681
  "actionId": "tier5.himiStoreAccount.showRegister"
2644
2682
  }
@@ -2657,6 +2695,7 @@ const SCREENS = runtimeSetting({
2657
2695
  "title": "Sign in instead",
2658
2696
  "style": "secondary",
2659
2697
  "fullWidth": true,
2698
+ "disabledPath": "$service:himiStoreAccount.authBusy",
2660
2699
  "action": {
2661
2700
  "actionId": "tier5.himiStoreAccount.showSignIn"
2662
2701
  }
@@ -2675,6 +2714,7 @@ const SCREENS = runtimeSetting({
2675
2714
  "title": "Back to sign in",
2676
2715
  "style": "secondary",
2677
2716
  "fullWidth": true,
2717
+ "disabledPath": "$service:himiStoreAccount.authBusy",
2678
2718
  "action": {
2679
2719
  "actionId": "tier5.himiStoreAccount.showSignIn"
2680
2720
  }
@@ -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