@wix/himalaya-cli 0.1573.0 → 0.1575.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 (112) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/dev-server/src/app-config.d.ts +16 -5
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/define-app.d.ts +6 -0
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/message-format.d.ts +59 -0
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/telemetry/wix-bi/src/index.d.ts +15 -0
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/telemetry/wix-bi/src/oneapp.d.ts +90 -0
  6. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/core.mjs +3 -3
  7. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/l10n.mjs +8 -3
  8. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/sdui.mjs +3 -3
  9. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-bi.mjs +165 -5
  10. package/dist/cli.mjs +5261 -2937
  11. package/dist/preview-assets.generated.bin +0 -0
  12. package/guides/designing-himi-screens.md +7 -1
  13. package/guides/hhp.md +2 -2
  14. package/guides/himi-json-output.md +53 -2
  15. package/guides/himi-mcp.md +1 -1
  16. package/guides/himi-test.md +3 -2
  17. package/guides/iap-products.md +1 -1
  18. package/guides/importing-oneapp-translations.md +115 -0
  19. package/guides/index.json +6 -0
  20. package/guides/one-app-section-mappings.md +16 -4
  21. package/guides/one-app-to-himi-app.md +167 -10
  22. package/guides/push-cli-onboarding.md +10 -0
  23. package/guides/qa-builds.md +21 -1
  24. package/guides/templates.md +3 -2
  25. package/guides/testing-himi-apps.md +1 -1
  26. package/package.json +3 -3
  27. package/templates/beauty-salon/dev/index.ts +96 -133
  28. package/templates/beauty-salon/tier5-src/_shared/exclusive-writes.ts +66 -0
  29. package/templates/beauty-salon/tier5-src/himi-branded-lite-cart/index.ts +18 -5
  30. package/templates/beauty-salon-owner/dev/index.ts +27 -6
  31. package/templates/beauty-salon-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  32. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  33. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  34. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
  35. package/templates/branded-lite/dev/index.ts +136 -142
  36. package/templates/branded-lite/l10n/en.json +570 -0
  37. package/templates/branded-lite/l10n/he.json +200 -0
  38. package/templates/branded-lite/tier5-src/_shared/business-chat-copy.ts +40 -0
  39. package/templates/branded-lite/tier5-src/_shared/business-chat-message-model.ts +1 -1
  40. package/templates/branded-lite/tier5-src/_shared/business-chat-model.ts +3 -2
  41. package/templates/branded-lite/tier5-src/_shared/business-chat-presenter.ts +26 -21
  42. package/templates/branded-lite/tier5-src/_shared/exclusive-writes.ts +66 -0
  43. package/templates/branded-lite/tier5-src/business-chat/index.ts +184 -63
  44. package/templates/branded-lite/tier5-src/himi-branded-lite-cart/index.ts +18 -5
  45. package/templates/community-club/dev/index.ts +22 -6
  46. package/templates/community-club-owner/dev/index.ts +27 -6
  47. package/templates/community-club-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  48. package/templates/community-club-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  49. package/templates/community-club-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  50. package/templates/community-club-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  51. package/templates/events-meetups/dev/index.ts +22 -6
  52. package/templates/events-meetups-owner/dev/index.ts +25 -6
  53. package/templates/events-meetups-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  54. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  55. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  56. package/templates/gym-fitness/dev/index.ts +96 -133
  57. package/templates/gym-fitness/tier5-src/_shared/exclusive-writes.ts +66 -0
  58. package/templates/gym-fitness/tier5-src/himi-branded-lite-cart/index.ts +18 -5
  59. package/templates/gym-fitness-owner/dev/index.ts +28 -6
  60. package/templates/gym-fitness-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  61. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  62. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  63. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  64. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
  65. package/templates/himi-chat/dev/index.ts +22 -6
  66. package/templates/nonprofit-cause/dev/index.ts +22 -6
  67. package/templates/nonprofit-cause-owner/dev/index.ts +26 -6
  68. package/templates/nonprofit-cause-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  69. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  70. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  71. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
  72. package/templates/owner-lite/dev/index.ts +28 -6
  73. package/templates/owner-lite/tier5-src/_shared/exclusive-writes.ts +66 -0
  74. package/templates/owner-lite/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  75. package/templates/owner-lite/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  76. package/templates/owner-lite/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  77. package/templates/owner-lite/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
  78. package/templates/podcast-creator/dev/index.ts +22 -6
  79. package/templates/podcast-creator-owner/dev/index.ts +27 -6
  80. package/templates/podcast-creator-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  81. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  82. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  83. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  84. package/templates/portfolio-creative/dev/index.ts +96 -133
  85. package/templates/portfolio-creative/tier5-src/_shared/exclusive-writes.ts +66 -0
  86. package/templates/portfolio-creative/tier5-src/himi-branded-lite-cart/index.ts +18 -5
  87. package/templates/portfolio-creative-owner/dev/index.ts +27 -6
  88. package/templates/portfolio-creative-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  89. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  90. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  91. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
  92. package/templates/restaurant/dev/index.ts +22 -6
  93. package/templates/restaurant-owner/dev/index.ts +28 -6
  94. package/templates/restaurant-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  95. package/templates/restaurant-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  96. package/templates/restaurant-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  97. package/templates/restaurant-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  98. package/templates/restaurant-owner/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
  99. package/templates/retail-store/dev/index.ts +96 -133
  100. package/templates/retail-store/tier5-src/_shared/exclusive-writes.ts +66 -0
  101. package/templates/retail-store/tier5-src/himi-branded-lite-cart/index.ts +18 -5
  102. package/templates/retail-store-owner/dev/index.ts +26 -6
  103. package/templates/retail-store-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  104. package/templates/retail-store-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  105. package/templates/retail-store-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  106. package/templates/surface-showcase/dev/index.ts +22 -6
  107. package/templates/yoga-studio/dev/index.ts +22 -6
  108. package/templates/yoga-studio-owner/dev/index.ts +26 -6
  109. package/templates/yoga-studio-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  110. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  111. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  112. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
@@ -1193,7 +1193,8 @@ himi dist --app <app> --platform android --aab --wait --target bo
1193
1193
  activates a draft release: `GET /v1/admin/site/store-build-check?app=&site=[&shell=]` answers
1194
1194
  `200 { ok: true, android: { packageName } }` or the same refusals, through `CheckIfBuildIsAllowed`,
1195
1195
  so it locks nothing. It names no release, so it cannot give
1196
- `409 release_predates_store_builds`; a draft the editor just built is on a current commit. It never
1196
+ `409 release_predates_store_builds`; a draft the editor just built is on a current commit. It is
1197
+ not sent the App name and icon either, so it cannot give their refusals (below). It never
1197
1198
  falls back to the locking call.
1198
1199
  - **A Containers app's TestFlight build is an App Store build.** For an app bound to a Wix site,
1199
1200
  `--platform ios --destination testflight --version <x.y.z>` with no `--signing` builds the app for
@@ -1226,6 +1227,25 @@ himi dist --app <app> --platform android --aab --wait --target bo
1226
1227
  502 (the prepared files failed Himalaya's checks, the check's code in `reason`: a fix on Wix's
1227
1228
  side, not a retry), `store_builds_unavailable` 503. A named `--signing byo` keeps the
1228
1229
  customer-signed path above, from the site's legacy BuildSecrets.
1230
+ - **A store build ships under the owner's App name and icon.** Both store builds above (a Containers
1231
+ app's `--aab` and its TestFlight build) need `appName` and `appIconUrl` on `POST /v1/admin/dist`
1232
+ (`himi dist --app-name <name> --app-icon-url <url>`): the App name and App icon the owner set in
1233
+ the Containers editor, which the Containers server reads off the project and sends. They win over
1234
+ the release's app-config `name` and icon, which the generation agent picked. Any other build
1235
+ takes them too when they are sent, and keeps the release's when they aren't. The name is trimmed,
1236
+ 1 to 200 characters, with no control characters and no `$` (Xcode would expand `$(NAME)` from the
1237
+ pool's environment into the binary), and can't start with `@` or `?` (Android would read it as a
1238
+ resource reference). The icon must be a Wix Media file, exactly
1239
+ `https://static.wixstatic.com/media/<file>`. Its 1024x1024 PNG rendition
1240
+ (`<url>/v1/fill/w_1024,h_1024,al_c,q_100/icon.png`) is fetched and checked last before the store
1241
+ id is locked, and again when the pool reads its contract (`GET /v1/dist/contract`), twice at most.
1242
+ If it can't be read then, the contract answers `503 app_icon_unavailable` and the build fails,
1243
+ rather than baking the release's or the shell's icon. iOS gets the icon re-encoded with no alpha
1244
+ channel, which App Store Connect refuses. The job records `appInfo`, and
1245
+ `GET /v1/owner/rebuild-reasons` reports `app_name` and `app_icon` as `unknown` for such a build.
1246
+ Refusals, each with `code` equal to `error`: `store_build_app_info_missing`, `app_info_incomplete`
1247
+ (one without the other), `invalid_app_name`, `invalid_app_icon_url` and `app_icon_invalid` (not a
1248
+ 1024x1024 PNG a platform can use) 400, `app_icon_unavailable` 503 (Wix Media couldn't be read).
1229
1249
 
1230
1250
  **Why the Wix production key, and only for the bundle.** There are two Android keys: the committed
1231
1251
  QA keystore and the Wix production key (WixOne). WixOne is the Play **upload** key every Branded
@@ -106,8 +106,9 @@ details beside the app:
106
106
  | `.himi/himi-test.stub.json` | a typed-journey skeleton per form (every field, then its submit) with `<FILL>` slots |
107
107
 
108
108
  `bootsOffline` is the consent preflight: it is `false` when any screen's `onAppear` dispatches into
109
- a worker that can reach Wix or the network, and `bootRequests` names each one. It is static and errs
110
- on the safe side ("may request"). The commands behind it:
109
+ a worker that can reach Wix or the network, or when a screen boots such a worker that calls an
110
+ action while its module evaluates (`svc.load();` at the top level). `bootRequests` names each one,
111
+ with `trigger: "onAppear"` or `"module-load"`. It is static and errs on the safe side ("may request"). The commands behind it:
111
112
 
112
113
  ```sh
113
114
  himi template recommend --text "a salon where clients book and buy products" # rank the catalog (or --verticals / --book)
@@ -267,7 +267,7 @@ render, local browser, zero network. `--no-shots` opts out (sweeps should).
267
267
 
268
268
  Absence is said, never silent: without the browser the report carries
269
269
  `shots.unavailable` and the ledger names the one-time remedy —
270
- `npx playwright install chromium`. Web fidelity, honestly named: these are the web
270
+ the install command `himi render --png` prints. Web fidelity, honestly named: these are the web
271
271
  renderer's pixels; native truth still comes from a device (`himi push` a preview).
272
272
 
273
273
  Hygiene: a **full** run owns `.himi/shots/` and clears it first, so a deleted screen
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.1573.0",
3
+ "version": "0.1575.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": [
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@modelcontextprotocol/server": "^2.0.0",
38
- "@rive-app/canvas": "^2.21.0",
38
+ "@rive-app/canvas": "2.38.3",
39
39
  "ajv": "^8.17.1",
40
40
  "ajv-formats": "^3.0.1",
41
41
  "cheerio": "^1.0.0",
@@ -71,5 +71,5 @@
71
71
  "artifactId": "himalaya-cli"
72
72
  }
73
73
  },
74
- "falconPackageHash": "39a44e1a057f7d34899d550863397025bfdec7b4f241a6e45b677246"
74
+ "falconPackageHash": "ddd3e8a75c29cf1fa48b60d543d4ba99be00f7a8a9707e3b6c88e776"
75
75
  }
@@ -15589,39 +15589,28 @@ const SCREENS = runtimeSetting({
15589
15589
  "id": "himi_cart.qty_minus",
15590
15590
  "body": {
15591
15591
  "package": "himalaya",
15592
- "type": "ZStack",
15592
+ "type": "Button",
15593
15593
  "props": {
15594
- "alignment": "center",
15595
- "children": [
15596
- {
15597
- "id": "",
15598
- "body": {
15599
- "package": "himalaya",
15600
- "type": "Icon",
15601
- "props": {
15602
- "name": "minus",
15603
- "size": 24,
15604
- "tint": "textPrimary"
15605
- }
15606
- },
15607
- "modifiers": {
15608
- "accessibilityRole": "decorative"
15609
- }
15594
+ "title": "Decrease quantity",
15595
+ "action": {
15596
+ "actionId": "tier5.himiStoreCart.decrement",
15597
+ "params": {
15598
+ "id": "$item.id"
15610
15599
  }
15611
- ]
15600
+ },
15601
+ "style": "tertiary",
15602
+ "iconOnly": true,
15603
+ "leadingIcon": "minus",
15604
+ "cornerRadius": 22,
15605
+ "minHeight": 44,
15606
+ "disabledPath": "$service:himiStoreCart.writeBusy"
15612
15607
  }
15613
15608
  },
15614
15609
  "modifiers": {
15615
15610
  "width": 44,
15616
15611
  "height": 44,
15617
- "onTap": {
15618
- "actionId": "tier5.himiStoreCart.decrement",
15619
- "params": {
15620
- "id": "$item.id"
15621
- }
15622
- },
15623
- "accessibilityRole": "button",
15624
15612
  "accessibilityLabel": "Decrease quantity",
15613
+ "accessibilityRole": "button",
15625
15614
  "haptic": "light"
15626
15615
  }
15627
15616
  },
@@ -15644,39 +15633,28 @@ const SCREENS = runtimeSetting({
15644
15633
  "id": "himi_cart.qty_plus",
15645
15634
  "body": {
15646
15635
  "package": "himalaya",
15647
- "type": "ZStack",
15636
+ "type": "Button",
15648
15637
  "props": {
15649
- "alignment": "center",
15650
- "children": [
15651
- {
15652
- "id": "",
15653
- "body": {
15654
- "package": "himalaya",
15655
- "type": "Icon",
15656
- "props": {
15657
- "name": "plus",
15658
- "size": 24,
15659
- "tint": "textPrimary"
15660
- }
15661
- },
15662
- "modifiers": {
15663
- "accessibilityRole": "decorative"
15664
- }
15638
+ "title": "Increase quantity",
15639
+ "action": {
15640
+ "actionId": "tier5.himiStoreCart.increment",
15641
+ "params": {
15642
+ "id": "$item.id"
15665
15643
  }
15666
- ]
15644
+ },
15645
+ "style": "tertiary",
15646
+ "iconOnly": true,
15647
+ "leadingIcon": "plus",
15648
+ "cornerRadius": 22,
15649
+ "minHeight": 44,
15650
+ "disabledPath": "$service:himiStoreCart.writeBusy"
15667
15651
  }
15668
15652
  },
15669
15653
  "modifiers": {
15670
15654
  "width": 44,
15671
15655
  "height": 44,
15672
- "onTap": {
15673
- "actionId": "tier5.himiStoreCart.increment",
15674
- "params": {
15675
- "id": "$item.id"
15676
- }
15677
- },
15678
- "accessibilityRole": "button",
15679
15656
  "accessibilityLabel": "Increase quantity",
15657
+ "accessibilityRole": "button",
15680
15658
  "haptic": "light"
15681
15659
  }
15682
15660
  },
@@ -15692,39 +15670,28 @@ const SCREENS = runtimeSetting({
15692
15670
  "id": "himi_cart.save_filled",
15693
15671
  "body": {
15694
15672
  "package": "himalaya",
15695
- "type": "ZStack",
15673
+ "type": "Button",
15696
15674
  "props": {
15697
- "alignment": "center",
15698
- "children": [
15699
- {
15700
- "id": "",
15701
- "body": {
15702
- "package": "himalaya",
15703
- "type": "Icon",
15704
- "props": {
15705
- "name": "heart_fill",
15706
- "size": 24,
15707
- "tint": "textPrimary"
15708
- }
15709
- },
15710
- "modifiers": {
15711
- "accessibilityRole": "decorative"
15712
- }
15675
+ "title": "Remove from wishlist",
15676
+ "action": {
15677
+ "actionId": "tier5.himiStoreCart.save",
15678
+ "params": {
15679
+ "id": "$item.id"
15713
15680
  }
15714
- ]
15681
+ },
15682
+ "style": "tertiary",
15683
+ "iconOnly": true,
15684
+ "leadingIcon": "heart_fill",
15685
+ "cornerRadius": 22,
15686
+ "minHeight": 44,
15687
+ "disabledPath": "$service:himiStoreCart.writeBusy"
15715
15688
  }
15716
15689
  },
15717
15690
  "modifiers": {
15718
15691
  "width": 44,
15719
15692
  "height": 44,
15720
- "onTap": {
15721
- "actionId": "tier5.himiStoreCart.save",
15722
- "params": {
15723
- "id": "$item.id"
15724
- }
15725
- },
15726
- "accessibilityRole": "button",
15727
15693
  "accessibilityLabel": "Remove from wishlist",
15694
+ "accessibilityRole": "button",
15728
15695
  "haptic": "light",
15729
15696
  "hiddenPath": "$item.notSaved"
15730
15697
  }
@@ -15733,39 +15700,28 @@ const SCREENS = runtimeSetting({
15733
15700
  "id": "himi_cart.save",
15734
15701
  "body": {
15735
15702
  "package": "himalaya",
15736
- "type": "ZStack",
15703
+ "type": "Button",
15737
15704
  "props": {
15738
- "alignment": "center",
15739
- "children": [
15740
- {
15741
- "id": "",
15742
- "body": {
15743
- "package": "himalaya",
15744
- "type": "Icon",
15745
- "props": {
15746
- "name": "heart",
15747
- "size": 24,
15748
- "tint": "textPrimary"
15749
- }
15750
- },
15751
- "modifiers": {
15752
- "accessibilityRole": "decorative"
15753
- }
15705
+ "title": "Add to wishlist",
15706
+ "action": {
15707
+ "actionId": "tier5.himiStoreCart.save",
15708
+ "params": {
15709
+ "id": "$item.id"
15754
15710
  }
15755
- ]
15711
+ },
15712
+ "style": "tertiary",
15713
+ "iconOnly": true,
15714
+ "leadingIcon": "heart",
15715
+ "cornerRadius": 22,
15716
+ "minHeight": 44,
15717
+ "disabledPath": "$service:himiStoreCart.writeBusy"
15756
15718
  }
15757
15719
  },
15758
15720
  "modifiers": {
15759
15721
  "width": 44,
15760
15722
  "height": 44,
15761
- "onTap": {
15762
- "actionId": "tier5.himiStoreCart.save",
15763
- "params": {
15764
- "id": "$item.id"
15765
- }
15766
- },
15767
- "accessibilityRole": "button",
15768
15723
  "accessibilityLabel": "Add to wishlist",
15724
+ "accessibilityRole": "button",
15769
15725
  "haptic": "light",
15770
15726
  "hiddenPath": "$item.saved"
15771
15727
  }
@@ -15774,39 +15730,28 @@ const SCREENS = runtimeSetting({
15774
15730
  "id": "himi_cart.remove",
15775
15731
  "body": {
15776
15732
  "package": "himalaya",
15777
- "type": "ZStack",
15733
+ "type": "Button",
15778
15734
  "props": {
15779
- "alignment": "center",
15780
- "children": [
15781
- {
15782
- "id": "",
15783
- "body": {
15784
- "package": "himalaya",
15785
- "type": "Icon",
15786
- "props": {
15787
- "name": "trash",
15788
- "size": 24,
15789
- "tint": "textPrimary"
15790
- }
15791
- },
15792
- "modifiers": {
15793
- "accessibilityRole": "decorative"
15794
- }
15735
+ "title": "Remove from cart",
15736
+ "action": {
15737
+ "actionId": "tier5.himiStoreCart.remove",
15738
+ "params": {
15739
+ "id": "$item.id"
15795
15740
  }
15796
- ]
15741
+ },
15742
+ "style": "tertiary",
15743
+ "iconOnly": true,
15744
+ "leadingIcon": "trash",
15745
+ "cornerRadius": 22,
15746
+ "minHeight": 44,
15747
+ "disabledPath": "$service:himiStoreCart.writeBusy"
15797
15748
  }
15798
15749
  },
15799
15750
  "modifiers": {
15800
15751
  "width": 44,
15801
15752
  "height": 44,
15802
- "onTap": {
15803
- "actionId": "tier5.himiStoreCart.remove",
15804
- "params": {
15805
- "id": "$item.id"
15806
- }
15807
- },
15808
- "accessibilityRole": "button",
15809
15753
  "accessibilityLabel": "Remove from cart",
15754
+ "accessibilityRole": "button",
15810
15755
  "haptic": "light"
15811
15756
  }
15812
15757
  }
@@ -16055,7 +16000,7 @@ const SCREENS = runtimeSetting({
16055
16000
  "action": {
16056
16001
  "actionId": "tier5.himiStoreCart.applyPromo"
16057
16002
  },
16058
- "disabledPath": "$service:himiStoreCart.promoBusy"
16003
+ "disabledPath": "$service:himiStoreCart.writeBusy"
16059
16004
  }
16060
16005
  },
16061
16006
  "modifiers": {
@@ -16112,7 +16057,8 @@ const SCREENS = runtimeSetting({
16112
16057
  "iconOnly": true,
16113
16058
  "leadingIcon": "close",
16114
16059
  "cornerRadius": 22,
16115
- "minHeight": 44
16060
+ "minHeight": 44,
16061
+ "disabledPath": "$service:himiStoreCart.writeBusy"
16116
16062
  }
16117
16063
  },
16118
16064
  "modifiers": {
@@ -16246,6 +16192,7 @@ const SCREENS = runtimeSetting({
16246
16192
  "title": "Checkout",
16247
16193
  "style": "primary",
16248
16194
  "fullWidth": true,
16195
+ "disabledPath": "$service:himiStoreCart.writeBusy",
16249
16196
  "action": {
16250
16197
  "actionId": "tier5.himiStoreCart.checkout"
16251
16198
  }
@@ -41003,15 +40950,31 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
41003
40950
 
41004
40951
  // Live seed-id enumeration: a published release serves only EXACT screens, so
41005
40952
  // `himi push` bakes each templated detail route's CURRENT catalog ids as exact
41006
- // screens via `seedIds`. Anonymous visitor rung against this app's own clientId;
41007
- // best-effort (offline build → no baked details, preview still live-resolves).
40953
+ // screens via `seedIds`. Anonymous visitor rung against this app's own clientId — or,
40954
+ // for an app bound through the One App visitor identity (`visitorSource: "one-app"`,
40955
+ // no client), that site's One App visitor instance. Best-effort (offline build → no
40956
+ // baked details, preview still live-resolves).
40957
+ async function liveSeedToken(WIX: string): Promise<string | undefined> {
40958
+ const auth = (config as { auth?: { config?: { clientId?: string; visitorSource?: string; metaSiteId?: string } } }).auth?.config;
40959
+ // A One App binding boots as the site's One App visitor, so the seed reads as that visitor too —
40960
+ // even beside a member-login clientId, and never repointed by a stray local WIX_CLIENT_ID.
40961
+ const oneApp = auth?.visitorSource === "one-app";
40962
+ const clientId = oneApp ? "" : process.env.WIX_CLIENT_ID ?? auth?.clientId ?? "";
40963
+ if (clientId && !clientId.startsWith("YOUR_")) {
40964
+ const mint = await fetch(WIX + "/oauth2/token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ clientId, grantType: "anonymous" }) });
40965
+ return ((await mint.json()) as { access_token?: string }).access_token;
40966
+ }
40967
+ const msid = oneApp ? auth?.metaSiteId ?? "" : "";
40968
+ if (!/^[0-9a-fA-F-]{10,64}$/.test(msid)) return undefined;
40969
+ const mint = await fetch(`https://apps.wix.com/_api/one-app-session-web/v3/businesses/${msid}/signed-instance?signed_instance_type=Visitor`, { headers: { Accept: "application/json" }, redirect: "error" });
40970
+ const visitor = (await mint.json()) as { signedInstance?: string; signedInstanceType?: string };
40971
+ return visitor.signedInstanceType === "Visitor" ? visitor.signedInstance : undefined;
40972
+ }
40973
+
41008
40974
  async function liveSeedIds(spec: LiveSeed): Promise<string[]> {
41009
40975
  try {
41010
40976
  const WIX = "https://www.wixapis.com";
41011
- const clientId = process.env.WIX_CLIENT_ID ?? (config as { auth?: { config?: { clientId?: string } } }).auth?.config?.clientId ?? "";
41012
- if (!clientId || clientId.startsWith("YOUR_")) return [];
41013
- const mint = await fetch(WIX + "/oauth2/token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ clientId, grantType: "anonymous" }) });
41014
- const token = ((await mint.json()) as { access_token?: string }).access_token;
40977
+ const token = await liveSeedToken(WIX);
41015
40978
  if (!token) return [];
41016
40979
  const res = await fetch(WIX + spec.path, { method: "POST", headers: { Authorization: token, "Content-Type": "application/json" }, body: JSON.stringify(spec.body) });
41017
40980
  const json = (await res.json()) as Record<string, unknown>;
@@ -0,0 +1,66 @@
1
+ import { defineService as register, type ServiceDefinition } from '@himalaya/state';
2
+
3
+ /** Reject repeated taps until the accepted write's result is committed. A promise
4
+ * queue alone releases before the host applies the reducer result, so the next
5
+ * write can still read an old revision/id. The private synchronous reducer below
6
+ * publishes busy and commits completion through the ordinary service handle;
7
+ * awaiting that handle guarantees the host has stored the result before unlock.
8
+ * Skipped actions return undefined, so they cannot roll back the accepted write.
9
+ */
10
+ export function withExclusiveWrites<S extends object = Record<string, unknown>>(
11
+ registerService: typeof register, names: readonly string[],
12
+ options: { prepare?: (action: string, args: readonly unknown[]) => void; coalesce?: readonly string[]; settle?: (before: S, next: S, live: S) => S } = {},
13
+ ): typeof register {
14
+ return ((name: string, definition: ServiceDefinition<Record<string, unknown>>) => {
15
+ let busy = false;
16
+ let activeAction = "";
17
+ let completion = Promise.resolve();
18
+ let complete = () => {};
19
+ let handle: { getState(): Record<string, unknown>; exclusiveWriteState(next: Record<string, unknown>): Promise<unknown> };
20
+ const wrapped: ServiceDefinition<Record<string, unknown>> = {
21
+ ...definition,
22
+ state: { ...definition.state, writeBusy: false },
23
+ notHydrated: [...(definition.notHydrated ?? []), 'writeBusy'],
24
+ actions: {
25
+ ...Object.fromEntries(Object.entries(definition.actions ?? {}).map(([action, reduce]) => [action,
26
+ names.includes(action) ? async (state: Record<string, unknown>, ...args: never[]) => {
27
+ // Intake request metadata even when its state replacement is skipped.
28
+ options.prepare?.(action, args);
29
+ if (busy) {
30
+ // Repeated initial loads must await the accepted load, so callers
31
+ // observe its committed rows rather than the initial skeleton.
32
+ if (activeAction === action && options.coalesce?.includes(action)) await completion;
33
+ return;
34
+ }
35
+ busy = true;
36
+ activeAction = action;
37
+ completion = new Promise<void>(resolve => { complete = resolve; });
38
+ try {
39
+ await handle.exclusiveWriteState({ ...handle.getState(), writeBusy: true });
40
+ const next = await reduce(state, ...args);
41
+ const live = handle.getState();
42
+ // Non-write actions (for example editing a draft field) can still run
43
+ // during I/O. Let the owning service preserve those newer edits.
44
+ const settled = options.settle ? options.settle(state as S, (next ?? live) as S, live as S) : next ?? live;
45
+ await handle.exclusiveWriteState({ ...settled, writeBusy: false });
46
+ } catch (error) {
47
+ await handle.exclusiveWriteState({ ...handle.getState(), writeBusy: false });
48
+ throw error;
49
+ } finally {
50
+ busy = false;
51
+ complete();
52
+ }
53
+ } : reduce,
54
+ ])),
55
+ exclusiveWriteState: (_state, next: Record<string, unknown>) => next,
56
+ },
57
+ };
58
+ const registered = registerService(name, wrapped);
59
+ // Registration preserves this private action, but the dynamic action map
60
+ // cannot express its payload to the generic service handle.
61
+ handle = registered as unknown as typeof handle;
62
+ // Older hosts ignore notHydrated. A fresh worker has no pending write.
63
+ handle.getState().writeBusy = false;
64
+ return registered;
65
+ }) as typeof register;
66
+ }
@@ -115,6 +115,7 @@ interface State {
115
115
  checkoutHint: string;
116
116
  itemCount: number;
117
117
  promoCode: string;
118
+ promoEditRevision: number;
118
119
  appliedCode: string;
119
120
  appliedLabel: string;
120
121
  couponId: string;
@@ -380,11 +381,24 @@ function couponDetails(result: unknown): { id: string; code: string } {
380
381
  return { id: coupon?.coupon?.id ?? coupon?.id ?? "", code: coupon?.coupon?.code ?? coupon?.code ?? "" };
381
382
  }
382
383
 
384
+ import { withExclusiveWrites } from "../_shared/exclusive-writes.js";
385
+
383
386
  let cartPricing: IapCommerce;
384
387
  const defineService = withIapPresentation<State>({
385
- register: withOnlineWrites(registerState, ["remove", "increment", "decrement", "checkout", "beginPromo", "applyPromo", "removePromo"], { derive, messageFields: action => action.includes("Promo") ? ["promoError"] : ["checkoutHint"] }),
388
+ register: withExclusiveWrites<State>(withOnlineWrites(registerState, ["remove", "increment", "decrement", "checkout", "beginPromo", "applyPromo", "removePromo"], { derive, messageFields: action => action.includes("Promo") ? ["promoError"] : ["checkoutHint"] }), ["load", "remove", "increment", "decrement", "checkout", "applyPromo", "removePromo", "save"], {
389
+ prepare: (action, args) => { if (action === "load") stashCopy(args[0]); },
390
+ coalesce: ["load"],
391
+ // Full cart reloads share the transaction, since they also replace rows and
392
+ // reset promo fields. Only setPromo increments the edit revision: beginPromo's internal error reset
393
+ // must not masquerade as a user edit when a rejected code is retried.
394
+ settle: (before, next, live) => {
395
+ if ((before.promoEditRevision ?? 0) === (live.promoEditRevision ?? 0)) return next;
396
+ return derive({ ...next, promoEditRevision: live.promoEditRevision,
397
+ ...(next.items.length ? { promoCode: live.promoCode, promoError: live.promoError } : {}) });
398
+ },
399
+ }),
386
400
  onCreate: commerce => { cartPricing = commerce; },
387
- prefix: "cart", transientKeys: ["items", "subtotal", "promoBusy"], contentActions: ["load", "remove", "increment", "decrement"],
401
+ prefix: "cart", transientKeys: ["items", "subtotal", "promoBusy", "promoEditRevision"], contentActions: ["load", "remove", "increment", "decrement"],
388
402
  items: state => state.items.flatMap(row => row.catalogReference?.catalogItemId ? [{ reference: { kind: "storesProduct", id: row.catalogReference.catalogItemId }, commerceDisplayPrice: row.commerceTotal ?? row.total }] : []),
389
403
  present: (state, price, allowed) => ({ ...state,
390
404
  items: state.items.map(row => ({ ...row, total: row.catalogReference?.catalogItemId ? price(row.catalogReference.catalogItemId, row.commerceTotal ?? row.total) : row.total })),
@@ -404,10 +418,9 @@ const svc = defineService<State, {
404
418
  decrement: (state: State, arg?: unknown) => Promise<State>;
405
419
  checkout: (state: State) => Promise<State>;
406
420
  }>("himiStoreCart", {
407
- state: derive({ status: "loading", errorKind: "", items: [], subtotal: "", subtotalLabel: "", emptyText: "", checkoutHint: "", showPaidToast: false, noPaidToast: true, itemCount: 0, promoCode: "", appliedCode: "", appliedLabel: "", couponId: "", promoError: "", promoBusy: false, noPromoError: true, hasCoupon: false, discount: "", noDiscount: true, noCheckoutHint: true, noCoupon: true, isLoading: true, notLoading: false, isErrored: false, notErrored: true, noOfflineError: true, noServerError: true, hideContent: true, noEmpty: true, noItems: true }),
421
+ state: derive({ status: "loading", errorKind: "", items: [], subtotal: "", subtotalLabel: "", emptyText: "", checkoutHint: "", showPaidToast: false, noPaidToast: true, itemCount: 0, promoCode: "", promoEditRevision: 0, appliedCode: "", appliedLabel: "", couponId: "", promoError: "", promoBusy: false, noPromoError: true, hasCoupon: false, discount: "", noDiscount: true, noCheckoutHint: true, noCoupon: true, isLoading: true, notLoading: false, isErrored: false, notErrored: true, noOfflineError: true, noServerError: true, hideContent: true, noEmpty: true, noItems: true }),
408
422
  actions: {
409
423
  load: async (state, arg) => {
410
- stashCopy(arg);
411
424
  // 1) Instant paint from the on-device cache (offline-safe; empty on first-ever run).
412
425
  const cached = await readCachedItems();
413
426
  const base = cached.length
@@ -423,7 +436,7 @@ const svc = defineService<State, {
423
436
  : derive({ ...state, status: "error", errorKind: isOfflineFailure(error) ? "offline" : "server", items: [], subtotal: "", emptyText: copyParam("cart.loadErrorText", "Cart could not load."), checkoutHint: "" });
424
437
  }
425
438
  },
426
- setPromo: (state, arg) => derive({ ...state, promoCode: (typeof arg === "string" ? arg : String((arg as { value?: unknown } | undefined)?.value ?? "")).slice(0, 100), promoError: "" }),
439
+ setPromo: (state, arg) => derive({ ...state, promoEditRevision: (state.promoEditRevision ?? 0) + 1, promoCode: (typeof arg === "string" ? arg : String((arg as { value?: unknown } | undefined)?.value ?? "")).slice(0, 100), promoError: "" }),
427
440
  beginPromo: state => derive({ ...state, promoBusy: true, promoError: "" }),
428
441
  applyPromo: async (state): Promise<State> => {
429
442
  const code = state.promoCode.trim();