@wix/himalaya-cli 0.1324.0 → 0.1326.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 (177) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/checkout/src/index.d.ts +6 -10
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/order-refs.d.ts +1 -0
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/purchase-fields.d.ts +1 -0
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/screens/src/index.d.ts +9 -35
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/stores-v1/src/display.d.ts +1 -0
  6. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/checkout.mjs +30 -13
  7. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap-checkout.mjs +20 -2
  8. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/screens.mjs +2890 -54
  9. package/dist/cli.mjs +4 -5
  10. package/fixtures/wix-canned.json +61 -6
  11. package/guides/iap-pricing.md +9 -0
  12. package/guides/iap-screen-adoption.md +77 -0
  13. package/guides/importable-sdks.md +2 -0
  14. package/guides/index.json +6 -0
  15. package/guides/sdk-reference.json +339 -1
  16. package/package.json +2 -2
  17. package/templates/beauty-salon/config.ts +1 -1
  18. package/templates/beauty-salon/dev/index.ts +46 -18
  19. package/templates/beauty-salon/tier5-src/_shared/collection-contract.ts +1 -1
  20. package/templates/beauty-salon/tier5-src/_shared/collection-service.ts +3 -2
  21. package/templates/beauty-salon/tier5-src/_shared/himi-store-collection.ts +1 -1
  22. package/templates/beauty-salon/tier5-src/_shared/home-service.ts +2 -1
  23. package/templates/beauty-salon/tier5-src/_shared/iap-presentation.ts +53 -0
  24. package/templates/beauty-salon/tier5-src/_shared/iap-worker.ts +42 -0
  25. package/templates/beauty-salon/tier5-src/himi-branded-lite-cart/index.ts +48 -1
  26. package/templates/beauty-salon/tier5-src/himi-branded-lite-home/index.ts +16 -2
  27. package/templates/beauty-salon/tier5-src/himi-branded-lite-order/index.ts +24 -3
  28. package/templates/beauty-salon/tier5-src/himi-branded-lite-plans/index.ts +153 -64
  29. package/templates/beauty-salon/tier5-src/himi-store-product/index.ts +24 -3
  30. package/templates/beauty-salon/tier5-src/himi-store-saved/index.ts +8 -3
  31. package/templates/beauty-salon/tier5-src/himi-store-shop/index.ts +10 -0
  32. package/templates/beauty-salon/tier5-src/wix-spaces-plan-detail/index.ts +8 -3
  33. package/templates/beauty-salon-owner/tier5-src/_shared/collection-contract.ts +1 -1
  34. package/templates/beauty-salon-owner/tier5-src/_shared/collection-service.ts +3 -2
  35. package/templates/beauty-salon-owner/tier5-src/_shared/home-service.ts +2 -1
  36. package/templates/branded-lite/config.ts +1 -1
  37. package/templates/branded-lite/dev/index.ts +46 -18
  38. package/templates/branded-lite/tier5-src/_shared/collection-contract.ts +1 -1
  39. package/templates/branded-lite/tier5-src/_shared/collection-service.ts +3 -2
  40. package/templates/branded-lite/tier5-src/_shared/himi-store-collection.ts +1 -1
  41. package/templates/branded-lite/tier5-src/_shared/home-service.ts +2 -1
  42. package/templates/branded-lite/tier5-src/_shared/iap-presentation.ts +53 -0
  43. package/templates/branded-lite/tier5-src/himi-branded-lite-cart/index.ts +48 -1
  44. package/templates/branded-lite/tier5-src/himi-branded-lite-home/index.ts +16 -2
  45. package/templates/branded-lite/tier5-src/himi-branded-lite-order/index.ts +24 -3
  46. package/templates/branded-lite/tier5-src/himi-branded-lite-plans/index.ts +153 -64
  47. package/templates/branded-lite/tier5-src/himi-store-product/index.ts +24 -3
  48. package/templates/branded-lite/tier5-src/himi-store-saved/index.ts +8 -3
  49. package/templates/branded-lite/tier5-src/himi-store-shop/index.ts +10 -0
  50. package/templates/branded-lite/tier5-src/wix-spaces-plan-detail/index.ts +8 -3
  51. package/templates/community-club/config.ts +1 -1
  52. package/templates/community-club/dev/index.ts +3 -1
  53. package/templates/community-club/tier5-src/_shared/collection-contract.ts +1 -1
  54. package/templates/community-club/tier5-src/_shared/collection-service.ts +3 -2
  55. package/templates/community-club/tier5-src/_shared/home-service.ts +2 -1
  56. package/templates/community-club/tier5-src/_shared/iap-presentation.ts +53 -0
  57. package/templates/community-club/tier5-src/_shared/iap-worker.ts +42 -0
  58. package/templates/community-club/tier5-src/himi-branded-lite-home/index.ts +16 -2
  59. package/templates/community-club-owner/tier5-src/_shared/collection-contract.ts +1 -1
  60. package/templates/community-club-owner/tier5-src/_shared/collection-service.ts +3 -2
  61. package/templates/community-club-owner/tier5-src/_shared/home-service.ts +2 -1
  62. package/templates/events-meetups/config.ts +1 -1
  63. package/templates/events-meetups/dev/index.ts +3 -1
  64. package/templates/events-meetups/tier5-src/_shared/collection-contract.ts +1 -1
  65. package/templates/events-meetups/tier5-src/_shared/collection-service.ts +3 -2
  66. package/templates/events-meetups/tier5-src/_shared/home-service.ts +2 -1
  67. package/templates/events-meetups/tier5-src/_shared/iap-presentation.ts +53 -0
  68. package/templates/events-meetups/tier5-src/_shared/iap-worker.ts +42 -0
  69. package/templates/events-meetups/tier5-src/himi-branded-lite-home/index.ts +16 -2
  70. package/templates/events-meetups-owner/tier5-src/_shared/collection-contract.ts +1 -1
  71. package/templates/events-meetups-owner/tier5-src/_shared/collection-service.ts +3 -2
  72. package/templates/events-meetups-owner/tier5-src/_shared/home-service.ts +2 -1
  73. package/templates/gym-fitness/config.ts +1 -1
  74. package/templates/gym-fitness/dev/index.ts +46 -18
  75. package/templates/gym-fitness/tier5-src/_shared/collection-contract.ts +1 -1
  76. package/templates/gym-fitness/tier5-src/_shared/collection-service.ts +3 -2
  77. package/templates/gym-fitness/tier5-src/_shared/himi-store-collection.ts +1 -1
  78. package/templates/gym-fitness/tier5-src/_shared/home-service.ts +2 -1
  79. package/templates/gym-fitness/tier5-src/_shared/iap-presentation.ts +53 -0
  80. package/templates/gym-fitness/tier5-src/_shared/iap-worker.ts +42 -0
  81. package/templates/gym-fitness/tier5-src/himi-branded-lite-cart/index.ts +48 -1
  82. package/templates/gym-fitness/tier5-src/himi-branded-lite-home/index.ts +16 -2
  83. package/templates/gym-fitness/tier5-src/himi-branded-lite-order/index.ts +24 -3
  84. package/templates/gym-fitness/tier5-src/himi-branded-lite-plans/index.ts +153 -64
  85. package/templates/gym-fitness/tier5-src/himi-store-product/index.ts +24 -3
  86. package/templates/gym-fitness/tier5-src/himi-store-saved/index.ts +8 -3
  87. package/templates/gym-fitness/tier5-src/himi-store-shop/index.ts +10 -0
  88. package/templates/gym-fitness/tier5-src/wix-spaces-plan-detail/index.ts +8 -3
  89. package/templates/gym-fitness-owner/tier5-src/_shared/collection-contract.ts +1 -1
  90. package/templates/gym-fitness-owner/tier5-src/_shared/collection-service.ts +3 -2
  91. package/templates/gym-fitness-owner/tier5-src/_shared/home-service.ts +2 -1
  92. package/templates/nonprofit-cause/config.ts +1 -1
  93. package/templates/nonprofit-cause/dev/index.ts +29 -13
  94. package/templates/nonprofit-cause/tier5-src/_shared/collection-contract.ts +1 -1
  95. package/templates/nonprofit-cause/tier5-src/_shared/collection-service.ts +3 -2
  96. package/templates/nonprofit-cause/tier5-src/_shared/home-service.ts +2 -1
  97. package/templates/nonprofit-cause/tier5-src/_shared/iap-presentation.ts +53 -0
  98. package/templates/nonprofit-cause/tier5-src/_shared/iap-worker.ts +42 -0
  99. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-home/index.ts +16 -2
  100. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-plans/index.ts +153 -64
  101. package/templates/nonprofit-cause/tier5-src/wix-spaces-plan-detail/index.ts +8 -3
  102. package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-contract.ts +1 -1
  103. package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-service.ts +3 -2
  104. package/templates/nonprofit-cause-owner/tier5-src/_shared/home-service.ts +2 -1
  105. package/templates/owner-lite/tier5-src/_shared/collection-contract.ts +1 -1
  106. package/templates/owner-lite/tier5-src/_shared/collection-service.ts +3 -2
  107. package/templates/owner-lite/tier5-src/_shared/home-service.ts +2 -1
  108. package/templates/podcast-creator/config.ts +1 -1
  109. package/templates/podcast-creator/dev/index.ts +29 -13
  110. package/templates/podcast-creator/tier5-src/_shared/collection-contract.ts +1 -1
  111. package/templates/podcast-creator/tier5-src/_shared/collection-service.ts +3 -2
  112. package/templates/podcast-creator/tier5-src/_shared/home-service.ts +2 -1
  113. package/templates/podcast-creator/tier5-src/_shared/iap-presentation.ts +53 -0
  114. package/templates/podcast-creator/tier5-src/_shared/iap-worker.ts +42 -0
  115. package/templates/podcast-creator/tier5-src/himi-branded-lite-home/index.ts +16 -2
  116. package/templates/podcast-creator/tier5-src/himi-branded-lite-plans/index.ts +153 -64
  117. package/templates/podcast-creator/tier5-src/wix-spaces-plan-detail/index.ts +8 -3
  118. package/templates/podcast-creator-owner/tier5-src/_shared/collection-contract.ts +1 -1
  119. package/templates/podcast-creator-owner/tier5-src/_shared/collection-service.ts +3 -2
  120. package/templates/podcast-creator-owner/tier5-src/_shared/home-service.ts +2 -1
  121. package/templates/portfolio-creative/config.ts +1 -1
  122. package/templates/portfolio-creative/dev/index.ts +20 -6
  123. package/templates/portfolio-creative/tier5-src/_shared/collection-contract.ts +1 -1
  124. package/templates/portfolio-creative/tier5-src/_shared/collection-service.ts +3 -2
  125. package/templates/portfolio-creative/tier5-src/_shared/himi-store-collection.ts +1 -1
  126. package/templates/portfolio-creative/tier5-src/_shared/home-service.ts +2 -1
  127. package/templates/portfolio-creative/tier5-src/_shared/iap-presentation.ts +53 -0
  128. package/templates/portfolio-creative/tier5-src/_shared/iap-worker.ts +42 -0
  129. package/templates/portfolio-creative/tier5-src/himi-branded-lite-cart/index.ts +48 -1
  130. package/templates/portfolio-creative/tier5-src/himi-branded-lite-home/index.ts +16 -2
  131. package/templates/portfolio-creative/tier5-src/himi-branded-lite-order/index.ts +24 -3
  132. package/templates/portfolio-creative/tier5-src/himi-store-product/index.ts +24 -3
  133. package/templates/portfolio-creative/tier5-src/himi-store-saved/index.ts +8 -3
  134. package/templates/portfolio-creative/tier5-src/himi-store-shop/index.ts +10 -0
  135. package/templates/portfolio-creative-owner/tier5-src/_shared/collection-contract.ts +1 -1
  136. package/templates/portfolio-creative-owner/tier5-src/_shared/collection-service.ts +3 -2
  137. package/templates/portfolio-creative-owner/tier5-src/_shared/home-service.ts +2 -1
  138. package/templates/restaurant/config.ts +1 -1
  139. package/templates/restaurant/dev/index.ts +3 -1
  140. package/templates/restaurant/tier5-src/_shared/collection-contract.ts +1 -1
  141. package/templates/restaurant/tier5-src/_shared/collection-service.ts +3 -2
  142. package/templates/restaurant/tier5-src/_shared/home-service.ts +2 -1
  143. package/templates/restaurant/tier5-src/_shared/iap-presentation.ts +53 -0
  144. package/templates/restaurant/tier5-src/_shared/iap-worker.ts +42 -0
  145. package/templates/restaurant/tier5-src/himi-branded-lite-home/index.ts +16 -2
  146. package/templates/restaurant-owner/tier5-src/_shared/collection-contract.ts +1 -1
  147. package/templates/restaurant-owner/tier5-src/_shared/collection-service.ts +3 -2
  148. package/templates/restaurant-owner/tier5-src/_shared/home-service.ts +2 -1
  149. package/templates/retail-store/config.ts +1 -1
  150. package/templates/retail-store/dev/index.ts +20 -6
  151. package/templates/retail-store/tier5-src/_shared/collection-contract.ts +1 -1
  152. package/templates/retail-store/tier5-src/_shared/collection-service.ts +3 -2
  153. package/templates/retail-store/tier5-src/_shared/himi-store-collection.ts +1 -1
  154. package/templates/retail-store/tier5-src/_shared/home-service.ts +2 -1
  155. package/templates/retail-store/tier5-src/_shared/iap-presentation.ts +53 -0
  156. package/templates/retail-store/tier5-src/_shared/iap-worker.ts +42 -0
  157. package/templates/retail-store/tier5-src/himi-branded-lite-cart/index.ts +48 -1
  158. package/templates/retail-store/tier5-src/himi-branded-lite-home/index.ts +16 -2
  159. package/templates/retail-store/tier5-src/himi-branded-lite-order/index.ts +24 -3
  160. package/templates/retail-store/tier5-src/himi-store-product/index.ts +24 -3
  161. package/templates/retail-store/tier5-src/himi-store-saved/index.ts +8 -3
  162. package/templates/retail-store/tier5-src/himi-store-shop/index.ts +10 -0
  163. package/templates/retail-store-owner/tier5-src/_shared/collection-contract.ts +1 -1
  164. package/templates/retail-store-owner/tier5-src/_shared/collection-service.ts +3 -2
  165. package/templates/retail-store-owner/tier5-src/_shared/home-service.ts +2 -1
  166. package/templates/yoga-studio/config.ts +1 -1
  167. package/templates/yoga-studio/dev/index.ts +29 -13
  168. package/templates/yoga-studio/tier5-src/_shared/collection-contract.ts +1 -1
  169. package/templates/yoga-studio/tier5-src/_shared/collection-service.ts +3 -2
  170. package/templates/yoga-studio/tier5-src/_shared/home-service.ts +2 -1
  171. package/templates/yoga-studio/tier5-src/_shared/iap-presentation.ts +53 -0
  172. package/templates/yoga-studio/tier5-src/himi-branded-lite-home/index.ts +16 -2
  173. package/templates/yoga-studio/tier5-src/himi-branded-lite-plans/index.ts +153 -64
  174. package/templates/yoga-studio/tier5-src/wix-spaces-plan-detail/index.ts +8 -3
  175. package/templates/yoga-studio-owner/tier5-src/_shared/collection-contract.ts +1 -1
  176. package/templates/yoga-studio-owner/tier5-src/_shared/collection-service.ts +3 -2
  177. package/templates/yoga-studio-owner/tier5-src/_shared/home-service.ts +2 -1
@@ -1,5 +1,7 @@
1
1
  // Tier-5 bundle: himi-store — product detail.
2
- import { defineService } from "@himalaya/state";
2
+ import { withIapPresentation } from "../_shared/iap-presentation.js";
3
+ import { type IapCommerce } from "@himalaya/iap-checkout";
4
+ import { iapPriceCopy } from "../_shared/iap-worker.js";
3
5
  import { share as shareSheet } from "@himalaya/share";
4
6
  import { addToCurrentCart, getCurrentCart } from "@himalaya/wix-ecom";
5
7
  import { get, upsert, enqueue, type DbEntity } from "@himalaya/local-db";
@@ -74,6 +76,7 @@ let lastProduct: StoreProduct | undefined;
74
76
  * `refreshServices()` → `load()`, so this is what tells a genuine navigation to a
75
77
  * DIFFERENT product (drop the picks) from the same product coming back (keep them). */
76
78
  let lastLoadedId: string | undefined;
79
+ let productLoadSequence = 0;
77
80
 
78
81
  interface OptionSlots {
79
82
  opt1Label: string; opt1Chips: OptionChip[]; opt1Selected: string[]; noOpt1: boolean;
@@ -136,6 +139,7 @@ interface S {
136
139
  status: "loading" | "ready" | "error";
137
140
  name: string;
138
141
  price: string;
142
+ commercePrice?: string;
139
143
  /** The plain-text description: RichContent's fallback, and what shows with no document. */
140
144
  description: string;
141
145
  /** [rico] #3372 — a V3 product's Ricos `description` (read with `fields=DESCRIPTION`) as ONE document string
@@ -305,6 +309,14 @@ function derive(s: S): S {
305
309
  // read answering late — must not replace the badge the add set (#3468 review).
306
310
  let cartBadgeEpoch = 0;
307
311
 
312
+ let productPricing: IapCommerce;
313
+ const defineService = withIapPresentation<S>({
314
+ onCreate: commerce => { productPricing = commerce; }, prefix: "product", transientKeys: ["price", "optionsIncomplete"], contentActions: ["load", "paintCached"],
315
+ items: state => inputId() ? [{ reference: { kind: "storesProduct", id: inputId() }, commerceDisplayPrice: state.price }] : [],
316
+ present: (state, price, allowed) => ({ ...state, price: price(inputId()), optionsIncomplete: derive(state).optionsIncomplete || !allowed(inputId()) || lastProduct?.productType?.toLowerCase() === "digital",
317
+ cartHint: lastProduct?.productType?.toLowerCase() === "digital" || (!state.optionsUnknown && !allowed(inputId())) ? iapPriceCopy("product").purchaseUnavailable : state.cartHint === iapPriceCopy("product").purchaseUnavailable ? "" : state.cartHint }),
318
+ });
319
+
308
320
  const svc = defineService<S, {
309
321
  load: (s: S, arg?: unknown) => Promise<S>;
310
322
  setQuantity: (s: S, arg?: unknown) => S;
@@ -365,8 +377,10 @@ const svc = defineService<S, {
365
377
  opt3Label: "", opt3Chips: [], opt3Selected: [], noOpt3: true,
366
378
  }),
367
379
  actions: {
368
- load: async (s, arg) => {
380
+ load: async (s, arg): Promise<S> => {
369
381
  stashCopy(arg);
382
+ const sequence = ++productLoadSequence;
383
+ lastProduct = undefined;
370
384
  const id = inputId();
371
385
  // Storage problems must not prevent browsing or buying a product.
372
386
  let saved = false;
@@ -403,6 +417,7 @@ const svc = defineService<S, {
403
417
  painted = true;
404
418
  }
405
419
  const outcome = await live;
420
+ if (sequence !== productLoadSequence || id !== inputId()) return svc.getState() as S;
406
421
  // Once painted the page is INTERACTIVE while the live read is on the wire: the visitor may
407
422
  // save, change the quantity or add to cart. Build on the state as it is NOW, and keep what
408
423
  // they did — overwriting it would untick a stored save, reset the quantity, and drop the
@@ -448,6 +463,7 @@ const svc = defineService<S, {
448
463
  optionsUnknown: false,
449
464
  name,
450
465
  price: productPrice(p),
466
+ commercePrice: productPrice(p),
451
467
  ...productBody(p),
452
468
  image,
453
469
  gallery: productGallery(p),
@@ -543,6 +559,7 @@ const svc = defineService<S, {
543
559
  status: "ready",
544
560
  name,
545
561
  price: String(row.subtitle ?? ""),
562
+ commercePrice: String(row.subtitle ?? ""),
546
563
  description: "",
547
564
  descriptionJson: "",
548
565
  image: String(row.image ?? ""),
@@ -559,6 +576,7 @@ const svc = defineService<S, {
559
576
  },
560
577
  addToCart: async (s) => {
561
578
  const id = inputId();
579
+ if (!productPricing.pricing.checkoutGuard({ kind: "storesProduct", id }).allowed || lastProduct?.productType?.toLowerCase() === "digital") return derive({ ...s, cartHint: iapPriceCopy("product").purchaseUnavailable });
562
580
  if (!id) return derive({ ...s, cartHint: copyParam("product.cartHintNoProduct", "No product to add.") });
563
581
  // A second tap while the first write is in flight must not add the line twice.
564
582
  if (s.adding) return s;
@@ -571,6 +589,9 @@ const svc = defineService<S, {
571
589
  ? copyParam("product.cartHintOffline", "Reconnect to choose options and add this to your cart.")
572
590
  : optionsHint(s.optionGroups, s.selection) || copyParam("product.cartHintNoVariant", "That combination is not available. Try another.") });
573
591
  }
592
+ if (lastLoadedId !== id || lastProduct?.productType?.toLowerCase() !== "physical") {
593
+ return derive({ ...s, cartHint: copyParam("product.cartHintOffline", "Reconnect to choose options and add this to your cart.") });
594
+ }
574
595
  // Which variant the choice resolves to is the SDK's job — V3 addresses the
575
596
  // choice by variantId, V1 by a name→choice map, and it throws rather than
576
597
  // hand back a payload that would add a line with no variant.
@@ -613,7 +634,7 @@ const svc = defineService<S, {
613
634
  toggleSave: (s) => {
614
635
  if (s.status !== "ready" || !s.name || !inputId()) return s;
615
636
  try {
616
- const saved = toggleSavedProduct({ id: inputId(), name: s.name, price: s.price, image: s.image });
637
+ const saved = toggleSavedProduct({ id: inputId(), name: s.name, price: lastProduct ? productPrice(lastProduct) : s.commercePrice ?? "", image: s.image });
617
638
  return derive({ ...s, saved, saveHint: saved
618
639
  ? copyParam("product.saveSuccess", "Saved on this device.")
619
640
  : copyParam("product.saveRemoved", "Removed from saved products.") });
@@ -1,9 +1,9 @@
1
- import { defineService } from "@himalaya/state";
1
+ import { withIapPresentation } from "../_shared/iap-presentation.js";
2
2
  import { withEmptyFlags } from "../_shared/empty-flags.js";
3
3
  import { readSavedProducts, removeSavedProduct, type SavedProduct } from "../_shared/himi-store-saved.js";
4
4
  import { stashCopy, copyParam } from "../_shared/himi-copy.js";
5
5
 
6
- interface Row extends SavedProduct { initial: string; noImage: boolean; hasImage: boolean; detailScreenId: string }
6
+ interface Row extends SavedProduct { commercePrice?: string; initial: string; noImage: boolean; hasImage: boolean; detailScreenId: string }
7
7
  interface S {
8
8
  status: "loading" | "ready" | "error";
9
9
  rows: Row[];
@@ -18,11 +18,16 @@ function shape(s: Pick<S, "status" | "rows" | "error">): S {
18
18
  return { ...s, isLoading: s.status === "loading", notLoading: s.status !== "loading", notErrored: s.status !== "error", noEmpty: s.status !== "ready" || s.rows.length > 0, emptyText: copyParam("savedProducts.empty", "Save a product to find it here later.") };
19
19
  }
20
20
  function loadRows(): Row[] {
21
- return readSavedProducts().map(row => ({ ...row, initial: row.name.slice(0, 1).toUpperCase(), hasImage: !!row.image, noImage: !row.image, detailScreenId: "himi_product" }));
21
+ return readSavedProducts().map(row => ({ ...row, commercePrice: row.price, initial: row.name.slice(0, 1).toUpperCase(), hasImage: !!row.image, noImage: !row.image, detailScreenId: "himi_product" }));
22
22
  }
23
23
  function failed(s: S): S {
24
24
  return shape({ ...s, status: "error", error: copyParam("savedProducts.error", "Could not update saved products. Try again.") });
25
25
  }
26
+ const defineService = withIapPresentation<S>({
27
+ prefix: "savedProducts", transientKeys: ["rows"], contentActions: ["load", "remove"],
28
+ items: state => state.rows.map(row => ({ reference: { kind: "storesProduct", id: row.id }, commerceDisplayPrice: row.commercePrice ?? row.price })),
29
+ present: (state, price) => ({ ...state, rows: state.rows.map(row => ({ ...row, price: price(row.id, row.commercePrice ?? row.price) })) }),
30
+ });
26
31
  const svc = defineService<S, {
27
32
  load: (s: S, arg?: unknown) => S;
28
33
  remove: (s: S, arg?: unknown) => S;
@@ -1,4 +1,5 @@
1
1
  // Tier-5 bundle: himi-store — product catalog.
2
+ import { withIapPresentation } from "../_shared/iap-presentation.js";
2
3
  import { storeCollectionSaving } from "../_shared/himi-store-collection.js";
3
4
  import { defineBackendCollection } from "../_shared/collection-service.js";
4
5
  import { getCurrentCart } from "@himalaya/wix-ecom";
@@ -37,6 +38,7 @@ interface Row {
37
38
  id: string;
38
39
  name: string;
39
40
  subtitle: string;
41
+ commercePrice: string;
40
42
  image: string;
41
43
  initial: string;
42
44
  hasImage: boolean;
@@ -53,6 +55,7 @@ interface S {
53
55
  fromCache?: boolean;
54
56
  query: string;
55
57
  all: Row[];
58
+ collection?: { items: Array<Row & { collectionSubtitle: string }> };
56
59
  results: Row[];
57
60
  /** RFC 0008 cursor paging — non-empty while more pages exist ("" ⇒ exhausted). */
58
61
  nextCursor: string;
@@ -74,6 +77,7 @@ function row(p: StoreProduct): Row {
74
77
  id,
75
78
  name,
76
79
  subtitle: productPrice(p),
80
+ commercePrice: productPrice(p),
77
81
  image,
78
82
  initial: initials(name),
79
83
  hasImage: !!image,
@@ -99,6 +103,12 @@ function derive(s: S): S {
99
103
 
100
104
  let cacheWrite = Promise.resolve();
101
105
  const svc = defineBackendCollection("himiStoreShop", {
106
+ register: withIapPresentation<S>({
107
+ prefix: "shop", transientKeys: ["all", "results", "collection"], contentActions: ["collectionDataChanged"],
108
+ items: state => state.results.map(row => ({ reference: { kind: "storesProduct", id: row.id }, commerceDisplayPrice: row.commercePrice ?? row.subtitle })),
109
+ present: (state, price) => ({ ...state, results: state.results.map(row => ({ ...row, subtitle: price(row.id) })), all: state.all.map(row => ({ ...row, subtitle: price(row.id) })),
110
+ ...(state.collection ? { collection: { ...state.collection, items: state.collection.items.map(row => ({ ...row, collectionSubtitle: price(row.id) })) } } : {}) }),
111
+ }),
102
112
  state: derive({ status: "loading", query: "", all: [], results: [], nextCursor: "",
103
113
  emptyText: "", countLabel: "", cartCount: 0, isLoading: true, notLoading: false,
104
114
  isErrored: false, notErrored: true, noEmpty: true }),
@@ -87,7 +87,7 @@ export const COLLECTIONS: Record<string, CollectionConfig> = {
87
87
  himiBrandedOrders: { rows: 'rows', title: 'title', subtitle: 'subtitle', view: 'list', backendQuery: true,
88
88
  searchPlaceholder: 'Search by order number', countNoun: ['order', 'orders'], filterValues: ['Not fulfilled', 'Partially fulfilled', 'Fulfilled'],
89
89
  sortLabels: { default: 'Newest', az: 'Oldest', za: 'Order # ↓' } },
90
- himiBrandedPlans: { rows: 'rows', title: 'name', subtitle: 'description', metadata: ['badge', 'priceLine', 'periodLine', 'trialBadge', 'perk1', 'perk2', 'perk3'], view: 'grid', gridColumns: 1, metaMaxLines: 8, countNoun: ['plan', 'plans'] },
90
+ himiBrandedPlans: { rows: 'rows', title: 'name', subtitle: 'description', metadata: ['badge', 'priceLine', 'periodLine', 'trialBadge', 'perk1', 'perk2', 'perk3', 'joinNote'], view: 'grid', gridColumns: 1, metaMaxLines: 8, countNoun: ['plan', 'plans'] },
91
91
  himiBrandedPrograms: { rows: 'rows', title: 'name', subtitle: 'description', image: 'image', view: 'grid' },
92
92
  himiBrandedCoupons: { rows: 'coupons', title: 'name', subtitle: 'subtitle', view: 'list', primaryVerb: { label: 'Get code' } },
93
93
  himiBrandedNotifications: { rows: 'items', title: 'name', subtitle: 'subtitle', image: 'photo', metadata: ['unreadDot'], view: 'list' },
@@ -323,6 +323,7 @@ export function decorateCollection(name: string, def: { state: State; actions?:
323
323
  /** Domain-owned page source with shared query controls. Publishing uses a normal
324
324
  * reducer and reads the private controller snapshot, never an SDUI payload. */
325
325
  export function defineBackendCollection<S extends object, Row extends object>(name: string, options: {
326
+ register?: typeof register;
326
327
  /** Revalidate already opened pages on navigation return; explicit refresh resets. */
327
328
  restorePagesOnLoad?: boolean;
328
329
  /** The load action's argument (onAppear params beyond `copy`), before the query runs — for a
@@ -342,7 +343,7 @@ export function defineBackendCollection<S extends object, Row extends object>(na
342
343
  controller: controller as unknown as BackendCollection['controller'],
343
344
  apply: (state, page) => options.apply(state as S, page as unknown as CollectionQueryState<Row>) as State,
344
345
  });
345
- const handle = register(name, definition);
346
+ const handle = (options.register ?? register)(name, definition);
346
347
  publish = () => handle.collectionDataChanged();
347
348
  return handle;
348
349
  }
@@ -350,7 +351,7 @@ export function defineBackendCollection<S extends object, Row extends object>(na
350
351
  /** Keep the domain service's existing TypeScript API. Extra collection verbs are
351
352
  * dispatched through SDUI and do not widen the native capability surface. */
352
353
  export const defineService: typeof register = ((name: string, def: ServiceDefinition<unknown>) =>
353
- register(name, decorateCollection(name, def as unknown as { state: State; actions?: Record<string, Reducer> }))) as typeof register;
354
+ register(name, { ...def, ...decorateCollection(name, def as unknown as { state: State; actions?: Record<string, Reducer> }) } as ServiceDefinition<State>)) as typeof register;
354
355
 
355
356
  /** Domains can supply their existing saved-items store without putting domain
356
357
  * data or mutations in the shared collection layer. */
@@ -87,4 +87,5 @@ export function decorateHome(def: { state: State; actions?: Record<string, Actio
87
87
  actions.homeMoveDown = change(id => { const index = prefs.order.indexOf(id); if (index >= 0 && index < prefs.order.length - 1) [prefs.order[index], prefs.order[index + 1]] = [prefs.order[index + 1], prefs.order[index]]; });
88
88
  return { state: derive(def.state), actions };
89
89
  }
90
- export const defineService: typeof register = ((name: string, def: ServiceDefinition<unknown>) => register(name, decorateHome(def as unknown as { state: State; actions?: Record<string, Action> }))) as typeof register;
90
+ export const defineService: typeof register = ((name: string, def: ServiceDefinition<unknown>) =>
91
+ register(name, { ...def, ...decorateHome(def as unknown as { state: State; actions?: Record<string, Action> }) })) as typeof register;
@@ -37,7 +37,7 @@ export default defineApp({
37
37
  actions: "http://localhost:8190/v1/actions",
38
38
  sync: "http://localhost:8190/v1/sync",
39
39
  },
40
- capabilities: ["live_activity","push","share","secure_storage","open_url","calendar","local_database","wix_api","wix_platform","app_info","release_control","clipboard"],
40
+ capabilities: ["live_activity","push","share","secure_storage","open_url","calendar","local_database","wix_api","wix_platform","app_info","iap","release_control","clipboard"],
41
41
  designTokensPath: "./tokens.json",
42
42
  navigation: {
43
43
  "navKind": "responsive",
@@ -2399,7 +2399,9 @@ const SCREENS = runtimeSetting({
2399
2399
  "integrity": "",
2400
2400
  "contentType": "application/javascript+ts-bundle",
2401
2401
  "declaredCapabilities": [
2402
- "wix_api"
2402
+ "wix_api",
2403
+ "iap",
2404
+ "app_info"
2403
2405
  ],
2404
2406
  "minBinaryVersion": "0.2.0",
2405
2407
  "bundleVersion": "0.0.1"
@@ -14483,7 +14485,8 @@ const SCREENS = runtimeSetting({
14483
14485
  "package": "himalaya",
14484
14486
  "type": "Button",
14485
14487
  "props": {
14486
- "title": "Join on website",
14488
+ "title": "$item.joinLabel",
14489
+ "disabledPath": "$item.joinDisabled",
14487
14490
  "style": "primary",
14488
14491
  "fullWidth": true,
14489
14492
  "action": {
@@ -14504,7 +14507,8 @@ const SCREENS = runtimeSetting({
14504
14507
  "package": "himalaya",
14505
14508
  "type": "Button",
14506
14509
  "props": {
14507
- "title": "Join on website",
14510
+ "title": "$item.joinLabel",
14511
+ "disabledPath": "$item.joinDisabled",
14508
14512
  "style": "secondary",
14509
14513
  "fullWidth": true,
14510
14514
  "action": {
@@ -14685,7 +14689,8 @@ const SCREENS = runtimeSetting({
14685
14689
  "package": "himalaya",
14686
14690
  "type": "Button",
14687
14691
  "props": {
14688
- "title": "Join on website",
14692
+ "title": "$item.joinLabel",
14693
+ "disabledPath": "$item.joinDisabled",
14689
14694
  "style": "primary",
14690
14695
  "fullWidth": true,
14691
14696
  "action": {
@@ -14706,7 +14711,8 @@ const SCREENS = runtimeSetting({
14706
14711
  "package": "himalaya",
14707
14712
  "type": "Button",
14708
14713
  "props": {
14709
- "title": "Join on website",
14714
+ "title": "$item.joinLabel",
14715
+ "disabledPath": "$item.joinDisabled",
14710
14716
  "style": "secondary",
14711
14717
  "fullWidth": true,
14712
14718
  "action": {
@@ -15111,7 +15117,8 @@ const SCREENS = runtimeSetting({
15111
15117
  "package": "himalaya",
15112
15118
  "type": "Button",
15113
15119
  "props": {
15114
- "title": "Join on website",
15120
+ "title": "$item.joinLabel",
15121
+ "disabledPath": "$item.joinDisabled",
15115
15122
  "style": "primary",
15116
15123
  "fullWidth": true,
15117
15124
  "action": {
@@ -15132,7 +15139,8 @@ const SCREENS = runtimeSetting({
15132
15139
  "package": "himalaya",
15133
15140
  "type": "Button",
15134
15141
  "props": {
15135
- "title": "Join on website",
15142
+ "title": "$item.joinLabel",
15143
+ "disabledPath": "$item.joinDisabled",
15136
15144
  "style": "secondary",
15137
15145
  "fullWidth": true,
15138
15146
  "action": {
@@ -15313,7 +15321,8 @@ const SCREENS = runtimeSetting({
15313
15321
  "package": "himalaya",
15314
15322
  "type": "Button",
15315
15323
  "props": {
15316
- "title": "Join on website",
15324
+ "title": "$item.joinLabel",
15325
+ "disabledPath": "$item.joinDisabled",
15317
15326
  "style": "primary",
15318
15327
  "fullWidth": true,
15319
15328
  "action": {
@@ -15334,7 +15343,8 @@ const SCREENS = runtimeSetting({
15334
15343
  "package": "himalaya",
15335
15344
  "type": "Button",
15336
15345
  "props": {
15337
- "title": "Join on website",
15346
+ "title": "$item.joinLabel",
15347
+ "disabledPath": "$item.joinDisabled",
15338
15348
  "style": "secondary",
15339
15349
  "fullWidth": true,
15340
15350
  "action": {
@@ -15912,7 +15922,8 @@ const SCREENS = runtimeSetting({
15912
15922
  "package": "himalaya",
15913
15923
  "type": "Button",
15914
15924
  "props": {
15915
- "title": "Join on website",
15925
+ "title": "$item.joinLabel",
15926
+ "disabledPath": "$item.joinDisabled",
15916
15927
  "style": "primary",
15917
15928
  "fullWidth": true,
15918
15929
  "action": {
@@ -15933,7 +15944,8 @@ const SCREENS = runtimeSetting({
15933
15944
  "package": "himalaya",
15934
15945
  "type": "Button",
15935
15946
  "props": {
15936
- "title": "Join on website",
15947
+ "title": "$item.joinLabel",
15948
+ "disabledPath": "$item.joinDisabled",
15937
15949
  "style": "secondary",
15938
15950
  "fullWidth": true,
15939
15951
  "action": {
@@ -16211,7 +16223,9 @@ const SCREENS = runtimeSetting({
16211
16223
  "wix_api",
16212
16224
  "wix_cashier",
16213
16225
  "open_url",
16214
- "wix_platform"
16226
+ "wix_platform",
16227
+ "iap",
16228
+ "app_info"
16215
16229
  ],
16216
16230
  "minBinaryVersion": "0.1.0",
16217
16231
  "bundleVersion": "0.0.1"
@@ -16467,7 +16481,9 @@ const SCREENS = runtimeSetting({
16467
16481
  "integrity": "",
16468
16482
  "contentType": "application/javascript+ts-bundle",
16469
16483
  "declaredCapabilities": [
16470
- "wix_api"
16484
+ "wix_api",
16485
+ "iap",
16486
+ "app_info"
16471
16487
  ],
16472
16488
  "minBinaryVersion": "0.1.0",
16473
16489
  "bundleVersion": "0.0.1"
@@ -87,7 +87,7 @@ export const COLLECTIONS: Record<string, CollectionConfig> = {
87
87
  himiBrandedOrders: { rows: 'rows', title: 'title', subtitle: 'subtitle', view: 'list', backendQuery: true,
88
88
  searchPlaceholder: 'Search by order number', countNoun: ['order', 'orders'], filterValues: ['Not fulfilled', 'Partially fulfilled', 'Fulfilled'],
89
89
  sortLabels: { default: 'Newest', az: 'Oldest', za: 'Order # ↓' } },
90
- himiBrandedPlans: { rows: 'rows', title: 'name', subtitle: 'description', metadata: ['badge', 'priceLine', 'periodLine', 'trialBadge', 'perk1', 'perk2', 'perk3'], view: 'grid', gridColumns: 1, metaMaxLines: 8, countNoun: ['plan', 'plans'] },
90
+ himiBrandedPlans: { rows: 'rows', title: 'name', subtitle: 'description', metadata: ['badge', 'priceLine', 'periodLine', 'trialBadge', 'perk1', 'perk2', 'perk3', 'joinNote'], view: 'grid', gridColumns: 1, metaMaxLines: 8, countNoun: ['plan', 'plans'] },
91
91
  himiBrandedPrograms: { rows: 'rows', title: 'name', subtitle: 'description', image: 'image', view: 'grid' },
92
92
  himiBrandedCoupons: { rows: 'coupons', title: 'name', subtitle: 'subtitle', view: 'list', primaryVerb: { label: 'Get code' } },
93
93
  himiBrandedNotifications: { rows: 'items', title: 'name', subtitle: 'subtitle', image: 'photo', metadata: ['unreadDot'], view: 'list' },
@@ -323,6 +323,7 @@ export function decorateCollection(name: string, def: { state: State; actions?:
323
323
  /** Domain-owned page source with shared query controls. Publishing uses a normal
324
324
  * reducer and reads the private controller snapshot, never an SDUI payload. */
325
325
  export function defineBackendCollection<S extends object, Row extends object>(name: string, options: {
326
+ register?: typeof register;
326
327
  /** Revalidate already opened pages on navigation return; explicit refresh resets. */
327
328
  restorePagesOnLoad?: boolean;
328
329
  /** The load action's argument (onAppear params beyond `copy`), before the query runs — for a
@@ -342,7 +343,7 @@ export function defineBackendCollection<S extends object, Row extends object>(na
342
343
  controller: controller as unknown as BackendCollection['controller'],
343
344
  apply: (state, page) => options.apply(state as S, page as unknown as CollectionQueryState<Row>) as State,
344
345
  });
345
- const handle = register(name, definition);
346
+ const handle = (options.register ?? register)(name, definition);
346
347
  publish = () => handle.collectionDataChanged();
347
348
  return handle;
348
349
  }
@@ -350,7 +351,7 @@ export function defineBackendCollection<S extends object, Row extends object>(na
350
351
  /** Keep the domain service's existing TypeScript API. Extra collection verbs are
351
352
  * dispatched through SDUI and do not widen the native capability surface. */
352
353
  export const defineService: typeof register = ((name: string, def: ServiceDefinition<unknown>) =>
353
- register(name, decorateCollection(name, def as unknown as { state: State; actions?: Record<string, Reducer> }))) as typeof register;
354
+ register(name, { ...def, ...decorateCollection(name, def as unknown as { state: State; actions?: Record<string, Reducer> }) } as ServiceDefinition<State>)) as typeof register;
354
355
 
355
356
  /** Domains can supply their existing saved-items store without putting domain
356
357
  * data or mutations in the shared collection layer. */
@@ -87,4 +87,5 @@ export function decorateHome(def: { state: State; actions?: Record<string, Actio
87
87
  actions.homeMoveDown = change(id => { const index = prefs.order.indexOf(id); if (index >= 0 && index < prefs.order.length - 1) [prefs.order[index], prefs.order[index + 1]] = [prefs.order[index + 1], prefs.order[index]]; });
88
88
  return { state: derive(def.state), actions };
89
89
  }
90
- export const defineService: typeof register = ((name: string, def: ServiceDefinition<unknown>) => register(name, decorateHome(def as unknown as { state: State; actions?: Record<string, Action> }))) as typeof register;
90
+ export const defineService: typeof register = ((name: string, def: ServiceDefinition<unknown>) =>
91
+ register(name, { ...def, ...decorateHome(def as unknown as { state: State; actions?: Record<string, Action> }) })) as typeof register;
@@ -0,0 +1,53 @@
1
+ import { defineService, type ServiceDefinition } from "@himalaya/state";
2
+ import { createIapCommerce, type IapCommerce } from "@himalaya/iap-checkout";
3
+ import { iapPriceFields, type IapCatalogItem } from "@himalaya/iap";
4
+ import { iapPriceCopy, resetUnhydratedKeysOnOlderRuntime } from "./iap-worker.js";
5
+
6
+ // Presentation only. Checkout, mapping, cache, session and refresh policy stay in the shared flow.
7
+ export function withIapPresentation<S extends object>(options: {
8
+ register?: typeof defineService;
9
+ onCreate?(commerce: IapCommerce): void;
10
+ onContent?(state: S): void;
11
+ items(state: S): IapCatalogItem[];
12
+ present(state: S, price: (id: string, commercePrice?: string) => string, allowed: (id: string) => boolean): S;
13
+ contentActions: string[];
14
+ transientKeys: (keyof S & string)[];
15
+ prefix: string;
16
+ }): typeof defineService {
17
+ return ((name: string, definition: ServiceDefinition<S>) => {
18
+ const commerce = createIapCommerce({ onChange: () => { void service.iapPublish(); } });
19
+ options.onCreate?.(commerce);
20
+ let items: IapCatalogItem[] = [];
21
+ let contentVersion = 0;
22
+ const project = (state: S): S => {
23
+ const price = (id: string, commercePrice?: string) => {
24
+ const item = items.find(item => item.reference.id === id);
25
+ if (!item) return "";
26
+ const view = commerce.pricing.view(item.reference);
27
+ return view.route === "disabled" || view.route === "regular" ? commercePrice ?? item.commerceDisplayPrice ?? "" : iapPriceFields(view, iapPriceCopy(options.prefix)).priceText;
28
+ };
29
+ const allowed = (id: string) => commerce.pricing.checkoutGuard({ kind: "storesProduct", id }).allowed;
30
+ const poll = commerce.pollDirective(`${name}.iapPoll`);
31
+ return { ...options.present(state, price, allowed), ...(poll ? { _poll: poll } : {}) };
32
+ };
33
+ const actions = Object.fromEntries(Object.entries(definition.actions ?? {}).map(([action, reduce]) => [action, async (state: S, ...args: never[]) => {
34
+ const content = options.contentActions.includes(action);
35
+ const version = action === "load" ? ++contentVersion : contentVersion;
36
+ const next = await reduce(state, ...args);
37
+ if (!next || (content && version !== contentVersion)) return undefined;
38
+ if (content) {
39
+ options.onContent?.(next);
40
+ items = options.items(next);
41
+ void commerce.loadPrices(items);
42
+ }
43
+ return project(next);
44
+ }]));
45
+ const service = (options.register ?? defineService)(name, {
46
+ ...definition, notHydrated: [...(definition.notHydrated ?? []), ...options.transientKeys],
47
+ actions: { ...actions, iapPublish: project, iapPoll: async () => { await commerce.poll(); return undefined; },
48
+ iapRefresh: async () => { await commerce.loadPrices(items); return undefined; } },
49
+ });
50
+ resetUnhydratedKeysOnOlderRuntime(service, definition.state, options.transientKeys);
51
+ return service;
52
+ }) as unknown as typeof defineService;
53
+ }
@@ -1,6 +1,7 @@
1
1
  import { fetchMemberBookings, type MemberBookingRow } from "../_shared/member-bookings.js";
2
2
  import { memberAccess, assertMemberAccess } from "../_shared/member-access.js";
3
- import { defineService } from "../_shared/home-service.js";
3
+ import { defineService as registerHome } from "../_shared/home-service.js";
4
+ import { withIapPresentation } from "../_shared/iap-presentation.js";
4
5
  import { queryServices, type WixService } from "@himalaya/wix-bookings";
5
6
  import { getPost, queryPosts, type WixPost } from "@himalaya/wix-blog";
6
7
  import { queryGroups, type WixGroup } from "@himalaya/wix-groups";
@@ -68,6 +69,7 @@ interface ProductFeature {
68
69
  /** Device-local catalog saves are useful to visitors too; they are never
69
70
  * presented as account-synced data or mixed with member-only bookings. */
70
71
  interface SavedProductCard {
72
+ commercePrice: string;
71
73
  id: string;
72
74
  name: string;
73
75
  price: string;
@@ -573,7 +575,7 @@ function postCard(post: WixPost): PostCard {
573
575
  function savedProductCard(product: SavedProduct): SavedProductCard {
574
576
  const name = cleanTitle(product.name) || copyParam("home.savedProductFallbackName", "Saved item");
575
577
  const image = product.image ? wixMediaUrl(product.image) : "";
576
- return { id: product.id, name, price: product.price, image, initial: initials(name), hasImage: !!image, noImage: !image, detailScreenId: "himi_product" };
578
+ return { id: product.id, name, price: product.price, commercePrice: product.price, image, initial: initials(name), hasImage: !!image, noImage: !image, detailScreenId: "himi_product" };
577
579
  }
578
580
 
579
581
  function savedPostCard(post: WixPost): SavedPostCard {
@@ -1111,6 +1113,18 @@ async function refresh(state: State, arg?: unknown): Promise<State | undefined>
1111
1113
  weeklyClassCount: current.weeklyClassCount, weeklyClassGoalFraction: current.weeklyClassGoalFraction, weeklyGoalLabel: current.weeklyGoalLabel });
1112
1114
  }
1113
1115
 
1116
+ const defineService = withIapPresentation<State>({
1117
+ register: registerHome, prefix: "home", transientKeys: ["featuredProducts", "savedProducts", "heroPrice"], contentActions: ["refresh"],
1118
+ items: state => [
1119
+ ...state.featuredProducts.map(row => ({ reference: { kind: "storesProduct", id: row.id }, commerceDisplayPrice: row.price })),
1120
+ ...state.savedProducts.map(row => ({ reference: { kind: "storesProduct", id: row.id }, commerceDisplayPrice: row.commercePrice })),
1121
+ ],
1122
+ present: (state, price) => derive({ ...state,
1123
+ featuredProducts: state.featuredProducts.map(row => ({ ...row, price: price(row.id) })),
1124
+ savedProducts: state.savedProducts.map(row => ({ ...row, price: price(row.id, row.commercePrice) })),
1125
+ }),
1126
+ });
1127
+
1114
1128
  const svc = defineService<State, {
1115
1129
  // `State | undefined`, matching the implementation: `refresh` returns undefined on a
1116
1130
  // STALE generation (`if (gen !== refreshGen) return undefined`) so the runtime keeps