@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,21 +1,13 @@
1
- // Tier-5 bundle: himi-branded-lite — public pricing plans (memberships).
2
- //
3
- // Visitors can browse the site's public pricing plans (design §3.5 `plans`
4
- // vertical). The public GA route works on the anonymous visitor rung, so the
5
- // list paints with no login. Purchasing is NATIVE (Phase 2b): "Join" creates the
6
- // member's plan order via createOnlineOrder, then settles any balance on the shared
7
- // in-app pay rail (@wix/himalaya/checkout) — wallet where the app has one, in-app Wix
8
- // cashier otherwise. A FREE / fully-discounted plan activates with no payment step at
9
- // all. A plan-carried purchasable URL still wins when present, and any failure degrades
10
- // gracefully to the site-directing message rather than a dead button.
11
- // Four-state (loading / error / rows / empty) like the sibling branded workers.
12
1
  import { defineService } from "../_shared/collection-service.js";
13
2
  import { createOnlineOrder, listPublicPlans, type WixPlan } from "@himalaya/wix-pricing-plans";
14
3
  import { createRedirectSession } from "@himalaya/wix-redirects";
15
4
  import { open as openUrl } from "@himalaya/open-url";
16
5
  import { session as wixPlatformSession } from "@himalaya/wix-platform";
17
6
  import { copyParam, copyTemplate, stashCopy } from "../_shared/himi-copy.js";
18
- import { payCashierOrder } from "@himalaya/checkout";
7
+ import { activePlanIds, entitlement, payCashierOrder } from "@himalaya/checkout";
8
+ import { hostSessionFromGlobal, iapPriceFields, type IapCatalogItem } from "@himalaya/iap";
9
+ import { createIapCommerce, pricingPlanOrders } from "@himalaya/iap-checkout";
10
+ import { iapPriceCopy, iapPurchaseCopy, resetUnhydratedKeysOnOlderRuntime } from "../_shared/iap-worker.js";
19
11
  import { formatCurrency } from "../_shared/engine-intl.js";
20
12
 
21
13
  /** Wix Pricing Plans appDefId — the vertical that owns a plan order, so the `appId` its
@@ -57,6 +49,11 @@ interface PlanRow {
57
49
  hasBadge: boolean;
58
50
  /** Purchasable URL when the plan data carries one; "" otherwise. */
59
51
  joinUrl: string;
52
+ joined: boolean;
53
+ appleSupported: boolean;
54
+ joinLabel: string;
55
+ joinDisabled: boolean;
56
+ joinNote: string;
60
57
  }
61
58
 
62
59
  interface State {
@@ -130,7 +127,7 @@ function planRow(plan: WixPlan): PlanRow {
130
127
  const trialDays = plan.pricing?.freeTrialDays ?? 0;
131
128
  const trialBadge = trialDays > 0 ? copyTemplate("plans.trialBadge", "{days}-day free trial", { days: trialDays }) : "";
132
129
  return {
133
- id: plan.id || name,
130
+ id: plan.id || "",
134
131
  name,
135
132
  description: (plan.description ?? "").trim(),
136
133
  trialBadge,
@@ -145,6 +142,8 @@ function planRow(plan: WixPlan): PlanRow {
145
142
  noBadge: !badge,
146
143
  hasBadge: !!badge,
147
144
  joinUrl: joinUrl(plan),
145
+ appleSupported: plan.pricing?.singlePaymentUnlimited === true,
146
+ joined: false, joinLabel: copyParam("plans.joinCta", "Join"), joinDisabled: true, joinNote: "",
148
147
  };
149
148
  }
150
149
 
@@ -170,9 +169,97 @@ function idOf(arg: unknown): string {
170
169
  return typeof value === "string" ? value : "";
171
170
  }
172
171
 
172
+ const commerce = createIapCommerce({ adapters: [pricingPlanOrders], onChange: () => { void svc.pricingPublish(); } });
173
+ let rows: PlanRow[] = [];
174
+ let items: IapCatalogItem[] = [];
175
+ let message = "";
176
+ let started = false;
177
+ let loading = 0;
178
+ let contentReady = false;
179
+ const joining = new Set<string>();
180
+ const memberSession = hostSessionFromGlobal();
181
+ let regularGeneration = -1;
182
+ let regularAccess = new Set<string>();
183
+
184
+
185
+ function pricedState(state: State): State {
186
+ if (!contentReady) return state;
187
+ const priced = rows.map(row => {
188
+ const reference = { kind: "pricingPlan", id: row.id };
189
+ const view = commerce.pricing.view(reference);
190
+ const price = iapPriceFields(view, iapPriceCopy("plans"));
191
+ const purchase = commerce.purchaseFields(reference, iapPurchaseCopy("plans"));
192
+ const apple = purchase.appleRoute;
193
+ const regularReady = regularGeneration === memberSession.current().generation && (!memberSession.identityBound || memberSession.current().kind === "member");
194
+ const joined = apple ? purchase.accessReady : regularReady && regularAccess.has(row.id);
195
+ return { ...row, priceLine: view.route === "disabled" ? row.priceLine : price.priceText,
196
+ periodLine: apple ? price.priceNote : view.route === "regular" || view.route === "disabled" ? row.periodLine : "",
197
+ trialBadge: apple || price.actionDisabled ? "" : row.trialBadge,
198
+ noTrialBadge: apple || price.actionDisabled || row.noTrialBadge,
199
+ joined,
200
+ joinLabel: joined ? copyParam("plans.joinedLabel", "Current plan") : apple ? purchase.buyLabel : row.joinLabel,
201
+ joinDisabled: joined || (apple ? purchase.buyDisabled || !row.appleSupported : price.actionDisabled || !regularReady) || joining.has(row.id),
202
+ joinNote: apple ? row.appleSupported ? purchase.buyNote : iapPriceCopy("plans").purchaseUnavailable : price.actionNote };
203
+ });
204
+ const owned = priced.find(row => row.joined);
205
+ const poll = commerce.pollDirective("himiBrandedPlans.iapPoll");
206
+ return { ...state, ...ready(priced, owned ? copyTemplate("plans.joinedMessage", "You've joined {name}.", { name: owned.name }) : message), ...(poll ? { _poll: poll } : {}) };
207
+ }
208
+
209
+ async function regularJoinMessage(row: PlanRow, reference: { kind: string; id: string }): Promise<string> {
210
+ const generation = memberSession.current().generation;
211
+ if (row.joinUrl) {
212
+ try {
213
+ const result = await openUrl(row.joinUrl, "in_app_browser");
214
+ if (result.opened) { return ""; }
215
+ } catch {
216
+ }
217
+ }
218
+ try {
219
+ const created = await createOnlineOrder(row.id);
220
+ const snapshotId = created.purchaseFlowId;
221
+ if (generation !== memberSession.current().generation) return "";
222
+ if (created.order?.status === "ACTIVE") {
223
+ regularAccess.add(row.id);
224
+ return copyTemplate("plans.joinedMessage", "You've joined {name}.", { name: row.name });
225
+ }
226
+ const pay = await payCashierOrder({
227
+ appId: WIX_PRICING_PLANS_APP_ID,
228
+ snapshotId: snapshotId ?? "",
229
+ msid: await resolveMsid(),
230
+ mayCharge: () => commerce.pricing.continueCheckoutGuard(reference).allowed,
231
+ cashierUrl: async () => {
232
+ if (!commerce.pricing.continueCheckoutGuard(reference).allowed) return undefined;
233
+ const redirect = await createRedirectSession({
234
+ paidPlansCheckout: { planId: row.id },
235
+ callbacks: { postFlowUrl: "himalaya-himi-branded-lite://screen/himi_plans" },
236
+ });
237
+ return redirect.redirectSession?.fullUrl;
238
+ },
239
+ });
240
+ if (pay.kind === "paid") {
241
+ const access = await entitlement(row.id);
242
+ if (generation !== memberSession.current().generation) return "";
243
+ if (access.entitled) regularAccess.add(row.id);
244
+ return access.entitled
245
+ ? copyTemplate("plans.joinedMessage", "You've joined {name}.", { name: row.name })
246
+ : copyParam("plans.purchaseProcessingText", "Processing your purchase…");
247
+ }
248
+ if (pay.kind === "handoff") { return ""; }
249
+ if (pay.kind === "cancelled") {
250
+ return copyParam("plans.joinCancelledMessage", "Payment cancelled — you haven't joined.");
251
+ }
252
+ } catch {
253
+ }
254
+ return copyTemplate("plans.joinOnSiteMessage", "Memberships are purchased on the website — visit the site to join {name}.", { name: row.name });
255
+ }
256
+
173
257
  const svc = defineService<State, {
174
- load: (state: State, arg?: unknown) => Promise<State>;
175
- join: (state: State, arg?: unknown) => Promise<State>;
258
+ load: (state: State, arg?: unknown) => Promise<State | undefined>;
259
+ pricingPublish: (state: State) => State;
260
+ pricingRefresh: (state: State) => Promise<undefined>;
261
+ iapPoll: (state: State) => Promise<undefined>;
262
+ join: (state: State, arg?: unknown) => Promise<State | undefined>;
176
263
  }>("himiBrandedPlans", {
177
264
  state: {
178
265
  status: "loading",
@@ -187,13 +274,38 @@ const svc = defineService<State, {
187
274
  noRows: true,
188
275
  noEmpty: true,
189
276
  },
277
+ notHydrated: ["rows", "actionMessage"],
190
278
  actions: {
279
+ pricingPublish: pricedState,
280
+ pricingRefresh: async () => { await commerce.loadPrices(items); return undefined; },
281
+ iapPoll: async () => { await commerce.poll(); void svc.pricingPublish(); return undefined; },
191
282
  load: async (state, arg) => {
192
283
  stashCopy(arg);
284
+ const seq = ++loading;
285
+ const generation = memberSession.current().generation;
286
+ regularGeneration = -1;
287
+ regularAccess = new Set();
193
288
  try {
194
289
  const response = await listPublicPlans();
195
- return ready((response.plans ?? []).filter((plan) => !plan.archived).map(planRow));
290
+ if (seq !== loading) return undefined;
291
+ message = "";
292
+ contentReady = true;
293
+ rows = (response.plans ?? []).filter(plan => !plan.archived).map(planRow);
294
+ items = rows.map(row => ({ reference: { kind: "pricingPlan", id: row.id }, commerceDisplayPrice: row.priceLine }));
295
+ void svc.pricingPublish();
296
+ const access = rows.length > 0 && memberSession.current().kind === "member" ? await activePlanIds() : [];
297
+ if (seq !== loading || generation !== memberSession.current().generation) return undefined;
298
+ regularAccess = new Set(access);
299
+ regularGeneration = generation;
300
+ await commerce.loadPrices(items);
301
+ await (started ? commerce.reconcile() : commerce.start());
302
+ if (seq !== loading || generation !== memberSession.current().generation) return undefined;
303
+ started = true;
304
+ void svc.pricingPublish();
305
+ return undefined;
196
306
  } catch {
307
+ if (seq !== loading || generation !== memberSession.current().generation) return undefined;
308
+ contentReady = false;
197
309
  // Bridge errors (e.g. a 501 from a serve bridge without the route) land
198
310
  // here — a graceful error card with retry, never a dead screen.
199
311
  return {
@@ -209,57 +321,26 @@ const svc = defineService<State, {
209
321
  },
210
322
  join: async (state, arg) => {
211
323
  const row = state.rows.find((candidate) => candidate.id === idOf(arg));
212
- if (!row) return state;
213
- // A plan-carried purchasable URL still takes precedence (defensive: no
214
- // documented field emits one today, but if a future payload does we honor
215
- // it before minting a redirect session).
216
- if (row.joinUrl) {
217
- try {
218
- const result = await openUrl(row.joinUrl, "in_app_browser");
219
- if (result.opened) return { ...state, actionMessage: "" };
220
- } catch {
221
- // fall through to the redirect-session rail below
222
- }
324
+ if (!row || joining.has(row.id)) return undefined;
325
+ const reference = { kind: "pricingPlan", id: row.id };
326
+ const purchase = commerce.purchaseFields(reference, iapPurchaseCopy("plans"));
327
+ if (purchase.accessReady) return undefined;
328
+ if (purchase.appleRoute) {
329
+ if (!row.appleSupported) return undefined;
330
+ await commerce.buy({ reference });
331
+ void svc.pricingPublish();
332
+ return undefined;
223
333
  }
224
- // Phase 2b — NATIVE join: create the member's plan order, then settle any
225
- // balance on the shared in-app pay rail. A FREE (or fully-discounted) plan comes
226
- // back active with no `purchaseFlowId` and needs no payment step at all, which is
227
- // the case the hosted redirect used to make needlessly heavy.
334
+ if (regularGeneration !== memberSession.current().generation || (memberSession.identityBound && memberSession.current().kind !== "member") || regularAccess.has(row.id) || !commerce.pricing.checkoutGuard(reference).allowed) return undefined;
335
+ joining.add(row.id);
336
+ void svc.pricingPublish();
228
337
  try {
229
- const created = await createOnlineOrder(row.id);
230
- const snapshotId = created.purchaseFlowId;
231
- if (!snapshotId) {
232
- // Nothing to charge ⇒ the membership is already active.
233
- return { ...state, actionMessage: copyTemplate("plans.joinedMessage", "You've joined {name}.", { name: row.name }) };
234
- }
235
- const pay = await payCashierOrder({
236
- appId: WIX_PRICING_PLANS_APP_ID,
237
- snapshotId,
238
- msid: await resolveMsid(),
239
- // Wallet-less apps keep the hosted plans checkout, IN-APP — so a site with no
240
- // provisioned wallet can still sell memberships (see @wix/himalaya/checkout).
241
- cashierUrl: async () => {
242
- const redirect = await createRedirectSession({
243
- paidPlansCheckout: { planId: row.id },
244
- callbacks: { postFlowUrl: "himalaya-himi-branded-lite://screen/himi_plans" },
245
- });
246
- return redirect.redirectSession?.fullUrl;
247
- },
248
- });
249
- if (pay.kind === "paid") {
250
- return { ...state, actionMessage: copyTemplate("plans.joinedMessage", "You've joined {name}.", { name: row.name }) };
251
- }
252
- if (pay.kind === "handoff") return { ...state, actionMessage: "" };
253
- if (pay.kind === "cancelled") {
254
- return { ...state, actionMessage: copyParam("plans.joinCancelledMessage", "Payment cancelled — you haven't joined.") };
255
- }
256
- } catch {
257
- // Order/pay failure → graceful site-directing fallback below.
338
+ message = await regularJoinMessage(row, reference);
339
+ return undefined;
340
+ } finally {
341
+ joining.delete(row.id);
342
+ void svc.pricingPublish();
258
343
  }
259
- return {
260
- ...state,
261
- actionMessage: copyTemplate("plans.joinOnSiteMessage", "Memberships are purchased on the website — visit the site to join {name}.", { name: row.name }),
262
- };
263
344
  },
264
345
  },
265
346
  });
@@ -270,5 +351,13 @@ const svc = defineService<State, {
270
351
  // loading skeleton indefinitely on iOS. onAppear still fires afterwards and
271
352
  // re-loads with the screen's copy pack; it passes no required params here
272
353
  // (`appear(..., undefined, "<prefix>.")`), so a param-less first load is safe.
354
+ resetUnhydratedKeysOnOlderRuntime(svc, { ...svc.getState(), rows: [], actionMessage: "" }, ["rows", "actionMessage"]);
355
+ memberSession.subscribe(() => {
356
+ message = "";
357
+ regularGeneration = -1;
358
+ regularAccess = new Set();
359
+ void svc.pricingPublish();
360
+ void svc.load();
361
+ });
273
362
  svc.load();
274
363
  export { svc };
@@ -1,17 +1,22 @@
1
1
  // Tier-5 bundle: Wix Spaces clone — Pricing plan detail (ON-DEVICE, site-scoped).
2
2
  // himalaya.input.id carries the plan id; fetched via getPlan.
3
- import { defineService } from "@himalaya/state";
3
+ import { withIapPresentation } from "../_shared/iap-presentation.js";
4
4
  import { getPlan, type WixPlan } from "@himalaya/wix-pricing-plans";
5
5
  import { presence, presenceAfterError, detailInputId, type PresenceFlags } from "../_shared/detail-presence.js";
6
6
 
7
7
  interface S extends PresenceFlags { status: "loading" | "ready"; name: string; price: string; description: string; perks: string }
8
8
 
9
9
  function priceOf(p: WixPlan): string {
10
- const m = p.pricing?.price as unknown as { formattedAmount?: string; amount?: string; currency?: string } | undefined;
10
+ const m = p.pricing?.price as unknown as { formattedAmount?: string; amount?: string; value?: string; currency?: string } | undefined;
11
11
  if (!m) return p.pricing?.freeTrialDays ? "Free trial" : "";
12
- return m.formattedAmount || [m.amount, m.currency].filter(Boolean).join(" ");
12
+ return m.formattedAmount || [m.amount ?? m.value, m.currency].filter(Boolean).join(" ");
13
13
  }
14
14
 
15
+ const defineService = withIapPresentation<S>({
16
+ prefix: "plans", transientKeys: ["price"], contentActions: ["load"],
17
+ items: state => detailInputId() ? [{ reference: { kind: "pricingPlan", id: detailInputId() }, commerceDisplayPrice: state.price }] : [],
18
+ present: (state, price) => ({ ...state, price: price(detailInputId()) }),
19
+ });
15
20
  const svc = defineService<S, { load: (s: S) => Promise<S> }>("spacesPlanDetail", {
16
21
  state: { status: "loading", name: "", price: "", description: "", perks: "", ...presence("loading") },
17
22
  actions: {
@@ -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",
@@ -1934,7 +1934,9 @@ const SCREENS = runtimeSetting({
1934
1934
  "integrity": "",
1935
1935
  "contentType": "application/javascript+ts-bundle",
1936
1936
  "declaredCapabilities": [
1937
- "wix_api"
1937
+ "wix_api",
1938
+ "iap",
1939
+ "app_info"
1938
1940
  ],
1939
1941
  "minBinaryVersion": "0.2.0",
1940
1942
  "bundleVersion": "0.0.1"
@@ -7248,7 +7250,9 @@ const SCREENS = runtimeSetting({
7248
7250
  "contentType": "application/javascript+ts-bundle",
7249
7251
  "declaredCapabilities": [
7250
7252
  "wix_api",
7251
- "local_database"
7253
+ "local_database",
7254
+ "iap",
7255
+ "app_info"
7252
7256
  ],
7253
7257
  "minBinaryVersion": "0.1.0",
7254
7258
  "bundleVersion": "0.0.1"
@@ -7716,7 +7720,11 @@ const SCREENS = runtimeSetting({
7716
7720
  "url": "http://localhost:8190/v1/bundles/himi-store-saved",
7717
7721
  "integrity": "",
7718
7722
  "contentType": "application/javascript+ts-bundle",
7719
- "declaredCapabilities": [],
7723
+ "declaredCapabilities": [
7724
+ "wix_api",
7725
+ "iap",
7726
+ "app_info"
7727
+ ],
7720
7728
  "minBinaryVersion": "0.1.0",
7721
7729
  "bundleVersion": "0.0.1"
7722
7730
  }
@@ -9242,7 +9250,9 @@ const SCREENS = runtimeSetting({
9242
9250
  "wix_cashier",
9243
9251
  "open_url",
9244
9252
  "local_database",
9245
- "wix_platform"
9253
+ "wix_platform",
9254
+ "iap",
9255
+ "app_info"
9246
9256
  ],
9247
9257
  "minBinaryVersion": "0.1.0",
9248
9258
  "bundleVersion": "0.0.1"
@@ -22466,7 +22476,9 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
22466
22476
  "declaredCapabilities": [
22467
22477
  "wix_api",
22468
22478
  "local_database",
22469
- "live_activity"
22479
+ "live_activity",
22480
+ "iap",
22481
+ "app_info"
22470
22482
  ],
22471
22483
  "minBinaryVersion": "0.1.0",
22472
22484
  "bundleVersion": "0.0.1"
@@ -23348,7 +23360,9 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
23348
23360
  "wix_api",
23349
23361
  "share",
23350
23362
  "local_database",
23351
- "open_url"
23363
+ "open_url",
23364
+ "iap",
23365
+ "app_info"
23352
23366
  ],
23353
23367
  "minBinaryVersion": "0.1.0",
23354
23368
  "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. */
@@ -8,7 +8,7 @@ export const storeCollectionSaving: CollectionSaving = {
8
8
  for (const row of rows) {
9
9
  if (typeof row.id !== 'string' || typeof row.name !== 'string') continue;
10
10
  products.set(row.id, { id: row.id, name: row.name,
11
- price: typeof row.subtitle === 'string' ? row.subtitle : '',
11
+ price: typeof row.commercePrice === 'string' ? row.commercePrice : typeof row.subtitle === 'string' ? row.subtitle : '',
12
12
  image: typeof row.image === 'string' ? row.image : '' });
13
13
  }
14
14
  const selected = ids.map(id => products.get(id));
@@ -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
+ }
@@ -0,0 +1,42 @@
1
+ // Worker-side glue for the shared IAP layer (`@himalaya/iap` catalog pricing and
2
+ // `@himalaya/iap-checkout` → createIapCommerce), shared by every screen that shows or sells an
3
+ // IAP-backed item. Mapping, Apple prices, payment guards, the host scope and the sale live in the
4
+ // SDK; this only supplies the copy and the hydration reset.
5
+ import type { IapPriceCopy } from "@himalaya/iap";
6
+ import type { IapPurchaseCopy } from "@himalaya/iap-checkout";
7
+ import { copyParam } from "./himi-copy.js";
8
+
9
+ /** Price labels from the screen's copy pack (`<prefix>.priceLoadingText`, …). */
10
+ export function iapPriceCopy(prefix: string): IapPriceCopy {
11
+ return {
12
+ loading: copyParam(`${prefix}.priceLoadingText`, "Loading price…"),
13
+ unavailable: copyParam(`${prefix}.priceUnavailableText`, "Price unavailable"),
14
+ oneTimePurchase: copyParam(`${prefix}.oneTimePurchaseLabel`, "One-time purchase"),
15
+ purchaseUnavailable: copyParam(`${prefix}.purchaseUnavailableText`, "Purchasing is not available yet."),
16
+ };
17
+ }
18
+
19
+ /** App Store purchase labels from the screen's copy pack (`<prefix>.buyCta`, …), for
20
+ * `iapPurchaseFields`. Customer copy only: no backend names, ids or diagnostics. */
21
+ export function iapPurchaseCopy(prefix: string): IapPurchaseCopy {
22
+ return {
23
+ buy: copyParam(`${prefix}.buyCta`, "Get access"),
24
+ processing: copyParam(`${prefix}.purchaseProcessingText`, "Processing your purchase…"),
25
+ awaitingApproval: copyParam(`${prefix}.purchaseAwaitingApprovalText`, "Waiting for approval"),
26
+ unavailable: copyParam(`${prefix}.purchaseTemporarilyUnavailableText`, "Purchasing is temporarily unavailable."),
27
+ signInRequired: copyParam(`${prefix}.purchaseSignInText`, "Sign in to buy."),
28
+ retry: copyParam(`${prefix}.purchaseRetryText`, "Something went wrong. Try again."),
29
+ priceChanged: copyParam(`${prefix}.purchasePriceChangedText`, "The price changed. Check it and try again."),
30
+ support: copyParam(`${prefix}.purchaseSupportText`, "We couldn't finish this purchase. Contact support."),
31
+ };
32
+ }
33
+
34
+ /** What `notHydrated` does, for a runtime that ignores the option (a binary older than it, still
35
+ * served new bundles): put the listed keys back to their declared values before any action runs,
36
+ * so a persisted price or an enabled purchase action never paints as this run's. */
37
+ export function resetUnhydratedKeysOnOlderRuntime<S extends object>(svc: { getState(): S }, declared: S, keys: readonly (keyof S & string)[]): void {
38
+ if ((globalThis as { himalaya?: { hydrationOptOut?: unknown } }).himalaya?.hydrationOptOut === true) return;
39
+ const booted = svc.getState();
40
+ if (!booted || typeof booted !== "object") return;
41
+ for (const key of keys) booted[key] = declared[key];
42
+ }