@wix/himalaya-cli 0.1574.0 → 0.1576.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 (90) 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/stdlib/telemetry/wix-bi/src/index.d.ts +15 -0
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/telemetry/wix-bi/src/oneapp.d.ts +90 -0
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-bi.mjs +165 -5
  6. package/dist/cli.mjs +3489 -2601
  7. package/dist/preview-assets.generated.bin +0 -0
  8. package/guides/designing-himi-screens.md +7 -1
  9. package/guides/hhp.md +2 -2
  10. package/guides/himi-json-output.md +20 -0
  11. package/guides/push-cli-onboarding.md +10 -0
  12. package/package.json +2 -2
  13. package/templates/beauty-salon/dev/index.ts +74 -127
  14. package/templates/beauty-salon/tier5-src/_shared/exclusive-writes.ts +66 -0
  15. package/templates/beauty-salon/tier5-src/himi-branded-lite-cart/index.ts +18 -5
  16. package/templates/beauty-salon-owner/dev/index.ts +5 -0
  17. package/templates/beauty-salon-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  18. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  19. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  20. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
  21. package/templates/branded-lite/dev/index.ts +114 -136
  22. package/templates/branded-lite/l10n/en.json +570 -0
  23. package/templates/branded-lite/l10n/he.json +200 -0
  24. package/templates/branded-lite/tier5-src/_shared/business-chat-copy.ts +40 -0
  25. package/templates/branded-lite/tier5-src/_shared/business-chat-message-model.ts +1 -1
  26. package/templates/branded-lite/tier5-src/_shared/business-chat-model.ts +3 -2
  27. package/templates/branded-lite/tier5-src/_shared/business-chat-presenter.ts +26 -21
  28. package/templates/branded-lite/tier5-src/_shared/exclusive-writes.ts +66 -0
  29. package/templates/branded-lite/tier5-src/business-chat/index.ts +184 -63
  30. package/templates/branded-lite/tier5-src/himi-branded-lite-cart/index.ts +18 -5
  31. package/templates/community-club-owner/dev/index.ts +5 -0
  32. package/templates/community-club-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  33. package/templates/community-club-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  34. package/templates/community-club-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  35. package/templates/community-club-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  36. package/templates/events-meetups-owner/dev/index.ts +3 -0
  37. package/templates/events-meetups-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  38. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  39. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  40. package/templates/gym-fitness/dev/index.ts +74 -127
  41. package/templates/gym-fitness/tier5-src/_shared/exclusive-writes.ts +66 -0
  42. package/templates/gym-fitness/tier5-src/himi-branded-lite-cart/index.ts +18 -5
  43. package/templates/gym-fitness-owner/dev/index.ts +6 -0
  44. package/templates/gym-fitness-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  45. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  46. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  47. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  48. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
  49. package/templates/nonprofit-cause-owner/dev/index.ts +4 -0
  50. package/templates/nonprofit-cause-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  51. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  52. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  53. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
  54. package/templates/owner-lite/dev/index.ts +6 -0
  55. package/templates/owner-lite/tier5-src/_shared/exclusive-writes.ts +66 -0
  56. package/templates/owner-lite/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  57. package/templates/owner-lite/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  58. package/templates/owner-lite/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  59. package/templates/owner-lite/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
  60. package/templates/podcast-creator-owner/dev/index.ts +5 -0
  61. package/templates/podcast-creator-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  62. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  63. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  64. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  65. package/templates/portfolio-creative/dev/index.ts +74 -127
  66. package/templates/portfolio-creative/tier5-src/_shared/exclusive-writes.ts +66 -0
  67. package/templates/portfolio-creative/tier5-src/himi-branded-lite-cart/index.ts +18 -5
  68. package/templates/portfolio-creative-owner/dev/index.ts +5 -0
  69. package/templates/portfolio-creative-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  70. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  71. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  72. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
  73. package/templates/restaurant-owner/dev/index.ts +6 -0
  74. package/templates/restaurant-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  75. package/templates/restaurant-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  76. package/templates/restaurant-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  77. package/templates/restaurant-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  78. package/templates/restaurant-owner/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
  79. package/templates/retail-store/dev/index.ts +74 -127
  80. package/templates/retail-store/tier5-src/_shared/exclusive-writes.ts +66 -0
  81. package/templates/retail-store/tier5-src/himi-branded-lite-cart/index.ts +18 -5
  82. package/templates/retail-store-owner/dev/index.ts +4 -0
  83. package/templates/retail-store-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  84. package/templates/retail-store-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  85. package/templates/retail-store-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  86. package/templates/yoga-studio-owner/dev/index.ts +4 -0
  87. package/templates/yoga-studio-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  88. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  89. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  90. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
Binary file
@@ -331,7 +331,13 @@ build/validate/render cycles because the failure looks like a content bug rather
331
331
  of the scrolling `VStack`. Put the action row in `stickyFooter` with its own
332
332
  `padding` + `background: { token: "surfacePrimary" }` so it reads as a bar rather than as floating
333
333
  content, and keep the scroll root's `padding.bottom` generous enough that the last row is not
334
- hidden behind it.
334
+ hidden behind it. Buttons in `stickyHeader` and `stickyFooter` use the same action-binding
335
+ contract as the body: non-worker actions resolve `$service:` values in `params` and `args`
336
+ against live state when tapped. Native iOS, macOS and Android also resolve `$app:` values
337
+ through the app service scope. Missing/null bindings are dropped; `params` accepts scalars
338
+ only, while `args` preserves typed values. `tier5.*` actions keep their
339
+ params/args as authored (read the state in the reducer); their declared `nav` resolves
340
+ through the existing navigation contract.
335
341
 
336
342
  ---
337
343
 
package/guides/hhp.md CHANGED
@@ -121,8 +121,8 @@ The portable compiler stores its generated catalog as a deterministic gzip/base6
121
121
  envelope with a SHA-256 check. `himi hhp profile --json` returns the complete decoded
122
122
  metadata; the installed component guides and support decision source remain readable.
123
123
  `gen-v2.ts` builds the envelope from the canonical schemas and SDK exports, and refuses while any
124
- new field or export lacks a decision in `v2-support-decisions.json`. The catalog itself is refreshed
125
- on master (`authoring-refresh.yml`), so a PR commits the decisions file but not the catalog.
124
+ new field or export lacks a decision in `v2-support-decisions.json`. The catalog itself is gitignored
125
+ and generated before use, so a PR commits the decisions file and there is no catalog to commit.
126
126
 
127
127
  ## CSS
128
128
 
@@ -14,6 +14,14 @@ require those fields. Existing error-only push refusals now also carry `ok: fals
14
14
  Output channels and process exit codes are unchanged. Read JSON failures on both
15
15
  stdout and stderr; progress/warnings are not failure envelopes.
16
16
 
17
+ ## `himi store preflight` / `himi store upload` / `himi testflight` config failures
18
+
19
+ | Code | Meaning |
20
+ | --- | --- |
21
+ | `store_config_unavailable` | Loading the local store configuration threw an error, such as a missing or unreadable file. Exit 2; no build or upload runs. The existing `error` remains on stderr, alongside `findings` from the app preflight, including developer identity diagnostics available without store configuration. If preflight itself fails, its diagnostic is included as an error finding. |
22
+
23
+ Other legacy store failures, including `store status`, remain outside this migration.
24
+
17
25
  `code` is the machine contract. `error`/`errors`/`message` describe the failure for
18
26
  people and `hint`, when present, suggests recovery. Neither their wording nor the
19
27
  presence of one of those legacy shapes identifies the failure class. The complete
@@ -105,6 +113,18 @@ then carries a `ring_pinned` entry in `warnings` (also printed to stderr) and
105
113
  that moves the ring to the new release. When the push passed `--control-url`, `promote` repeats it. It stays `ok: true`. When the rings cannot be read, push
106
114
  prints neither.
107
115
 
116
+ ## `himi notify` owner-login codes
117
+
118
+ `himi notify setup|status` and `himi notify send --provider ping` carry `code` on these refusals
119
+ only; their other refusals keep the legacy `error`-only envelope for now. Login hints name the
120
+ target the command read, e.g. `himi login --browser --target wix`.
121
+
122
+ | Code | Meaning |
123
+ |---|---|
124
+ | `control_token_expired` | The stored control token is past its `exp`. Decided locally before any network call; run `himi login`. |
125
+ | `owner_login_required` | No usable owner login: the control token is missing or has no subject, or the plane answered 401/403 to the owner-apps read (`GET /v1/owner/apps`). Run `himi login`. Other failures of that read stay `app_ownership_unknown`. |
126
+ | `flag_not_supported_with_ping` | A local-sender flag was passed with `--provider ping`; `flag` names it. `--badge` is one: Ping has no badge field, and for a Branded app Ping sets the iOS badge itself from the member's unseen Ping feed items. |
127
+
108
128
  ## `himi cms seed` codes
109
129
 
110
130
  | Code | Meaning |
@@ -343,6 +343,16 @@ goes to the member's subscribed devices for this site's Branded-family app, whic
343
343
  than one device. It does not go to their other Wix apps. Confirm receipt and the tap on the
344
344
  device itself.
345
345
 
346
+ **There is no `--badge`.** Ping's send request has no badge field. For a Branded app, Ping sets
347
+ the iOS app-icon badge itself from the member's unseen items in their Ping feed, so the CLI refuses
348
+ `--badge` (`flag_not_supported_with_ping`) rather than send a number Ping would ignore.
349
+
350
+ **An expired login is named as one.** When the stored control token has expired, `notify send`
351
+ and `notify setup|status` refuse with `control_token_expired` before any network call; when the
352
+ plane rejects the token, they refuse with `owner_login_required`. Both mean log in again, not
353
+ an ownership problem: the hint names the target the command read (`himi login --browser --target
354
+ <target>`), because a bare `himi login` renews the default target instead.
355
+
346
356
  Every send carries a request ID. If the outcome is unknown (`send_outcome_unknown`, or a lost
347
357
  response), the CLI prints the ID and never retries on its own. Re-run the same command with
348
358
  `--request-id <id>`: Ping runs one request at most once per site for 24 hours, so a retry
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.1574.0",
3
+ "version": "0.1576.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": "3931950af337466648e08fa667462e52b854969f6441f512f82b9241"
74
+ "falconPackageHash": "b3770b71ba8570708ab97e43a149a8ee2880f9fea27a618d80902862"
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
  }
@@ -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();
@@ -79742,6 +79742,7 @@ const SCREENS = runtimeSetting({
79742
79742
  "title": "Create product",
79743
79743
  "style": "primary",
79744
79744
  "fullWidth": true,
79745
+ "disabledPath": "$service:himiOwnerLiteProductForm.writeBusy",
79745
79746
  "action": {
79746
79747
  "actionId": "tier5.himiOwnerLiteProductForm.submit"
79747
79748
  }
@@ -80061,6 +80062,7 @@ const SCREENS = runtimeSetting({
80061
80062
  "title": "Create post",
80062
80063
  "style": "primary",
80063
80064
  "fullWidth": true,
80065
+ "disabledPath": "$service:himiOwnerLitePostForm.writeBusy",
80064
80066
  "action": {
80065
80067
  "actionId": "tier5.himiOwnerLitePostForm.submit"
80066
80068
  }
@@ -80576,6 +80578,7 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
80576
80578
  "title": "Save changes",
80577
80579
  "style": "primary",
80578
80580
  "fullWidth": true,
80581
+ "disabledPath": "$service:himiOwnerLiteProductForm.writeBusy",
80579
80582
  "action": {
80580
80583
  "actionId": "tier5.himiOwnerLiteProductForm.submit"
80581
80584
  }
@@ -80823,6 +80826,7 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
80823
80826
  "title": "Save changes",
80824
80827
  "style": "primary",
80825
80828
  "fullWidth": true,
80829
+ "disabledPath": "$service:himiOwnerLiteServiceForm.writeBusy",
80826
80830
  "action": {
80827
80831
  "actionId": "tier5.himiOwnerLiteServiceForm.submit"
80828
80832
  }
@@ -81147,6 +81151,7 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
81147
81151
  "title": "Save changes",
81148
81152
  "style": "primary",
81149
81153
  "fullWidth": true,
81154
+ "disabledPath": "$service:himiOwnerLitePostForm.writeBusy",
81150
81155
  "action": {
81151
81156
  "actionId": "tier5.himiOwnerLitePostForm.submit"
81152
81157
  }