@wix/himalaya-cli 0.1368.0 → 0.1369.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 (39) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/mapping-source.d.ts +3 -1
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/wix-payments-contract.d.ts +44 -2
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/wix-payments-wire.d.ts +14 -1
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/wix-payments.d.ts +9 -3
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/apple-iap-contract.d.ts +18 -5
  6. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/apple-iap-core.d.ts +2 -0
  7. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/checkout-source.d.ts +8 -0
  8. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/commerce-core.d.ts +2 -0
  9. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/iap-order-backend.d.ts +3 -0
  10. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/index.d.ts +1 -0
  11. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/order-refs.d.ts +5 -1
  12. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap-checkout.mjs +167 -41
  13. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap.mjs +75 -21
  14. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/screens.mjs +147 -30
  15. package/dist/cli.mjs +7 -6
  16. package/guides/iap-screen-adoption.md +3 -2
  17. package/guides/sdk-reference.json +57 -9
  18. package/package.json +2 -2
  19. package/templates/beauty-salon/dev/index.ts +49 -1
  20. package/templates/beauty-salon/tier5-src/_shared/iap-presentation.ts +4 -2
  21. package/templates/beauty-salon/tier5-src/himi-store-product/index.ts +68 -5
  22. package/templates/branded-lite/dev/index.ts +49 -1
  23. package/templates/branded-lite/tier5-src/_shared/iap-presentation.ts +4 -2
  24. package/templates/branded-lite/tier5-src/himi-store-product/index.ts +68 -5
  25. package/templates/community-club/tier5-src/_shared/iap-presentation.ts +4 -2
  26. package/templates/events-meetups/tier5-src/_shared/iap-presentation.ts +4 -2
  27. package/templates/gym-fitness/dev/index.ts +49 -1
  28. package/templates/gym-fitness/tier5-src/_shared/iap-presentation.ts +4 -2
  29. package/templates/gym-fitness/tier5-src/himi-store-product/index.ts +68 -5
  30. package/templates/nonprofit-cause/tier5-src/_shared/iap-presentation.ts +4 -2
  31. package/templates/podcast-creator/tier5-src/_shared/iap-presentation.ts +4 -2
  32. package/templates/portfolio-creative/dev/index.ts +49 -1
  33. package/templates/portfolio-creative/tier5-src/_shared/iap-presentation.ts +4 -2
  34. package/templates/portfolio-creative/tier5-src/himi-store-product/index.ts +68 -5
  35. package/templates/restaurant/tier5-src/_shared/iap-presentation.ts +4 -2
  36. package/templates/retail-store/dev/index.ts +49 -1
  37. package/templates/retail-store/tier5-src/_shared/iap-presentation.ts +4 -2
  38. package/templates/retail-store/tier5-src/himi-store-product/index.ts +68 -5
  39. package/templates/yoga-studio/tier5-src/_shared/iap-presentation.ts +4 -2
@@ -26717,7 +26717,55 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
26717
26717
  "modifiers": {
26718
26718
  "cornerRadius": 8,
26719
26719
  "pressFeedback": "scale",
26720
- "haptic": "medium"
26720
+ "haptic": "medium",
26721
+ "hiddenPath": "$service:himiStoreProduct.cartHidden"
26722
+ }
26723
+ },
26724
+ {
26725
+ "id": "himi_product.buy",
26726
+ "body": {
26727
+ "package": "himalaya",
26728
+ "type": "Button",
26729
+ "props": {
26730
+ "title": "$service:himiStoreProduct.buyLabel",
26731
+ "style": "primary",
26732
+ "fullWidth": true,
26733
+ "disabledPath": "$service:himiStoreProduct.buyDisabled",
26734
+ "action": {
26735
+ "actionId": "tier5.himiStoreProduct.buy"
26736
+ }
26737
+ }
26738
+ },
26739
+ "modifiers": {
26740
+ "hiddenPath": "$service:himiStoreProduct.buyHidden"
26741
+ }
26742
+ },
26743
+ {
26744
+ "id": "himi_product.buy_note",
26745
+ "body": {
26746
+ "package": "himalaya",
26747
+ "type": "Text",
26748
+ "props": {
26749
+ "text": "$service:himiStoreProduct.buyNote",
26750
+ "style": "footnote",
26751
+ "color": "textSecondary",
26752
+ "collapseWhenEmpty": true
26753
+ }
26754
+ },
26755
+ "modifiers": {
26756
+ "hiddenPath": "$service:himiStoreProduct.buyHidden"
26757
+ }
26758
+ },
26759
+ {
26760
+ "id": "himi_product.purchased",
26761
+ "body": {
26762
+ "package": "himalaya",
26763
+ "type": "Text",
26764
+ "props": {
26765
+ "text": "$service:himiStoreProduct.purchasedNote",
26766
+ "style": "footnote",
26767
+ "collapseWhenEmpty": true
26768
+ }
26721
26769
  }
26722
26770
  },
26723
26771
  {
@@ -1,11 +1,13 @@
1
1
  import { defineService, type ServiceDefinition } from "@himalaya/state";
2
- import { createIapCommerce, type IapCommerce } from "@himalaya/iap-checkout";
2
+ import { createIapCommerce, type IapCommerce, type IapOrderAdapter } from "@himalaya/iap-checkout";
3
3
  import { iapPriceFields, type IapCatalogItem } from "@himalaya/iap";
4
4
  import { iapPriceCopy, resetUnhydratedKeysOnOlderRuntime } from "./iap-worker.js";
5
5
 
6
6
  // Presentation only. Checkout, mapping, cache, session and refresh policy stay in the shared flow.
7
7
  export function withIapPresentation<S extends object>(options: {
8
8
  register?: typeof defineService;
9
+ /** The verticals this screen sells; none for a screen that only shows prices. */
10
+ adapters?: readonly IapOrderAdapter[];
9
11
  onCreate?(commerce: IapCommerce): void;
10
12
  onContent?(state: S): void;
11
13
  items(state: S): IapCatalogItem[];
@@ -15,7 +17,7 @@ export function withIapPresentation<S extends object>(options: {
15
17
  prefix: string;
16
18
  }): typeof defineService {
17
19
  return ((name: string, definition: ServiceDefinition<S>) => {
18
- const commerce = createIapCommerce({ onChange: () => { void service.iapPublish(); } });
20
+ const commerce = createIapCommerce({ adapters: options.adapters, onChange: () => { void service.iapPublish(); } });
19
21
  options.onCreate?.(commerce);
20
22
  let items: IapCatalogItem[] = [];
21
23
  let contentVersion = 0;
@@ -1,7 +1,7 @@
1
1
  // Tier-5 bundle: himi-store — product detail.
2
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
+ import { itemPaymentEnabled, storesProductOrders, type IapCommerce, type IapPurchaseFields } from "@himalaya/iap-checkout";
4
+ import { iapPriceCopy, iapPurchaseCopy } from "../_shared/iap-worker.js";
5
5
  import { share as shareSheet } from "@himalaya/share";
6
6
  import { addToCurrentCart, getCurrentCart } from "@himalaya/wix-ecom";
7
7
  import { get, upsert, enqueue, type DbEntity } from "@himalaya/local-db";
@@ -202,6 +202,14 @@ interface S {
202
202
  opt1Label: string; opt1Chips: OptionChip[]; opt1Selected: string[]; noOpt1: boolean;
203
203
  opt2Label: string; opt2Chips: OptionChip[]; opt2Selected: string[]; noOpt2: boolean;
204
204
  opt3Label: string; opt3Chips: OptionChip[]; opt3Selected: string[]; noOpt3: boolean;
205
+ /** The App Store action (`commerce.purchaseFields`), in place of Add to cart while it shows. */
206
+ cartHidden: boolean;
207
+ buyHidden: boolean;
208
+ buyLabel: string;
209
+ buyDisabled: boolean;
210
+ buyNote: string;
211
+ /** Shown once the eCom order reads paid, never on the payment's approval alone. */
212
+ purchasedNote: string;
205
213
  }
206
214
 
207
215
  function inputId(): string {
@@ -309,14 +317,53 @@ function derive(s: S): S {
309
317
  // read answering late — must not replace the badge the add set (#3468 review).
310
318
  let cartBadgeEpoch = 0;
311
319
 
320
+ // A digital product with no options sells through the App Store once the platform pays for Wix
321
+ // Stores items with wix-iap StartItemPayment (`itemPaymentEnabled`). Until then, and for any other
322
+ // product, an App Store route only blocks the regular checkout, as before.
323
+ const SELLS_THROUGH_APPLE = itemPaymentEnabled();
324
+ const WIX_STORES_APP = "215238eb-22a5-4c36-9e7b-e7c08025e04e";
325
+ const NOT_APPLE: IapPurchaseFields = { appleRoute: false, accessReady: false, buyLabel: "", buyDisabled: true, buyBusy: false, buyNote: "" };
326
+
312
327
  let productPricing: IapCommerce;
328
+ let checkoutEntered = false;
313
329
  const defineService = withIapPresentation<S>({
314
- onCreate: commerce => { productPricing = commerce; }, prefix: "product", transientKeys: ["price", "optionsIncomplete"], contentActions: ["load", "paintCached"],
330
+ adapters: SELLS_THROUGH_APPLE ? [storesProductOrders] : [],
331
+ onCreate: commerce => { productPricing = commerce; }, prefix: "product",
332
+ transientKeys: ["price", "optionsIncomplete", "cartHint", "cartHidden", "buyHidden", "buyLabel", "buyDisabled", "buyNote", "purchasedNote"], contentActions: ["load", "paintCached"],
315
333
  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 }),
334
+ present: (state, price, allowed) => {
335
+ const purchase = applePurchase(state);
336
+ const unavailable = iapPriceCopy("product").purchaseUnavailable;
337
+ const blocked = lastProduct?.productType?.toLowerCase() === "digital" || (!state.optionsUnknown && !allowed(inputId()));
338
+ return {
339
+ ...state, price: price(inputId()), optionsIncomplete: derive(state).optionsIncomplete || !allowed(inputId()) || lastProduct?.productType?.toLowerCase() === "digital",
340
+ cartHint: !purchase.appleRoute && blocked ? unavailable : state.cartHint === unavailable ? "" : state.cartHint,
341
+ cartHidden: purchase.appleRoute,
342
+ buyHidden: !purchase.appleRoute || purchase.accessReady, buyLabel: purchase.buyLabel, buyDisabled: purchase.buyDisabled, buyNote: purchase.buyNote,
343
+ purchasedNote: purchase.accessReady ? copyParam("product.purchasedText", "Purchased") : "",
344
+ };
345
+ },
318
346
  });
319
347
 
348
+ /** The App Store action, only for the one shape it sells: digital, with no options to choose, read
349
+ * from the product loaded for the id on screen (as Add to cart is). A cached product whose
350
+ * options were never read is not known to have none. */
351
+ function applePurchase(state: S): IapPurchaseFields {
352
+ const id = inputId();
353
+ const loaded = !!id && lastLoadedId === id;
354
+ const sellable = SELLS_THROUGH_APPLE && loaded && lastProduct?.productType?.toLowerCase() === "digital" && !state.optionsUnknown && state.optionGroups.length === 0;
355
+ const copy = { ...iapPurchaseCopy("product"), buy: copyParam("product.buyCta", "Buy") };
356
+ const fields = sellable ? productPricing.purchaseFields({ kind: "storesProduct", id }, copy) : NOT_APPLE;
357
+ return fields.appleRoute ? fields : NOT_APPLE;
358
+ }
359
+
360
+ /** On every entry: the first starts the checkout (recovering held purchases), later ones reconcile. */
361
+ function enterCheckout(): void {
362
+ if (!SELLS_THROUGH_APPLE) return;
363
+ void (checkoutEntered ? productPricing.reconcile() : productPricing.start());
364
+ checkoutEntered = true;
365
+ }
366
+
320
367
  const svc = defineService<S, {
321
368
  load: (s: S, arg?: unknown) => Promise<S>;
322
369
  setQuantity: (s: S, arg?: unknown) => S;
@@ -329,6 +376,8 @@ const svc = defineService<S, {
329
376
  toggleSave: (s: S) => S;
330
377
  /** [rico] #3372 — RichContent's on_link: `{ url }` of the tapped link. */
331
378
  openLink: (s: S, arg?: unknown) => Promise<S>;
379
+ /** The App Store Buy: through the shared checkout, for the product on screen. */
380
+ buy: (s: S) => Promise<S>;
332
381
  }>("himiStoreProduct", {
333
382
  state: derive({
334
383
  status: "loading",
@@ -375,10 +424,17 @@ const svc = defineService<S, {
375
424
  opt1Label: "", opt1Chips: [], opt1Selected: [], noOpt1: true,
376
425
  opt2Label: "", opt2Chips: [], opt2Selected: [], noOpt2: true,
377
426
  opt3Label: "", opt3Chips: [], opt3Selected: [], noOpt3: true,
427
+ cartHidden: false,
428
+ buyHidden: true,
429
+ buyLabel: "",
430
+ buyDisabled: true,
431
+ buyNote: "",
432
+ purchasedNote: "",
378
433
  }),
379
434
  actions: {
380
435
  load: async (s, arg): Promise<S> => {
381
436
  stashCopy(arg);
437
+ enterCheckout();
382
438
  const sequence = ++productLoadSequence;
383
439
  lastProduct = undefined;
384
440
  const id = inputId();
@@ -657,6 +713,13 @@ const svc = defineService<S, {
657
713
  openLink: async (s, arg) => ((await openRichLink(arg))
658
714
  ? s
659
715
  : derive({ ...s, added: false, cartHint: copyParam("product.linkOpenFailed", "This link could not be opened.") })),
716
+ buy: async (s): Promise<S> => {
717
+ const id = inputId();
718
+ const purchase = applePurchase(s);
719
+ if (!purchase.appleRoute || purchase.buyDisabled) return s;
720
+ await productPricing.buy({ reference: { kind: "storesProduct", id }, catalogReference: { appId: WIX_STORES_APP, catalogItemId: id }, quantity: 1 });
721
+ return svc.getState() as S;
722
+ },
660
723
  },
661
724
  });
662
725
 
@@ -82885,7 +82885,55 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
82885
82885
  "modifiers": {
82886
82886
  "cornerRadius": 8,
82887
82887
  "pressFeedback": "scale",
82888
- "haptic": "medium"
82888
+ "haptic": "medium",
82889
+ "hiddenPath": "$service:himiStoreProduct.cartHidden"
82890
+ }
82891
+ },
82892
+ {
82893
+ "id": "himi_product.buy",
82894
+ "body": {
82895
+ "package": "himalaya",
82896
+ "type": "Button",
82897
+ "props": {
82898
+ "title": "$service:himiStoreProduct.buyLabel",
82899
+ "style": "primary",
82900
+ "fullWidth": true,
82901
+ "disabledPath": "$service:himiStoreProduct.buyDisabled",
82902
+ "action": {
82903
+ "actionId": "tier5.himiStoreProduct.buy"
82904
+ }
82905
+ }
82906
+ },
82907
+ "modifiers": {
82908
+ "hiddenPath": "$service:himiStoreProduct.buyHidden"
82909
+ }
82910
+ },
82911
+ {
82912
+ "id": "himi_product.buy_note",
82913
+ "body": {
82914
+ "package": "himalaya",
82915
+ "type": "Text",
82916
+ "props": {
82917
+ "text": "$service:himiStoreProduct.buyNote",
82918
+ "style": "footnote",
82919
+ "color": "textSecondary",
82920
+ "collapseWhenEmpty": true
82921
+ }
82922
+ },
82923
+ "modifiers": {
82924
+ "hiddenPath": "$service:himiStoreProduct.buyHidden"
82925
+ }
82926
+ },
82927
+ {
82928
+ "id": "himi_product.purchased",
82929
+ "body": {
82930
+ "package": "himalaya",
82931
+ "type": "Text",
82932
+ "props": {
82933
+ "text": "$service:himiStoreProduct.purchasedNote",
82934
+ "style": "footnote",
82935
+ "collapseWhenEmpty": true
82936
+ }
82889
82937
  }
82890
82938
  },
82891
82939
  {
@@ -1,11 +1,13 @@
1
1
  import { defineService, type ServiceDefinition } from "@himalaya/state";
2
- import { createIapCommerce, type IapCommerce } from "@himalaya/iap-checkout";
2
+ import { createIapCommerce, type IapCommerce, type IapOrderAdapter } from "@himalaya/iap-checkout";
3
3
  import { iapPriceFields, type IapCatalogItem } from "@himalaya/iap";
4
4
  import { iapPriceCopy, resetUnhydratedKeysOnOlderRuntime } from "./iap-worker.js";
5
5
 
6
6
  // Presentation only. Checkout, mapping, cache, session and refresh policy stay in the shared flow.
7
7
  export function withIapPresentation<S extends object>(options: {
8
8
  register?: typeof defineService;
9
+ /** The verticals this screen sells; none for a screen that only shows prices. */
10
+ adapters?: readonly IapOrderAdapter[];
9
11
  onCreate?(commerce: IapCommerce): void;
10
12
  onContent?(state: S): void;
11
13
  items(state: S): IapCatalogItem[];
@@ -15,7 +17,7 @@ export function withIapPresentation<S extends object>(options: {
15
17
  prefix: string;
16
18
  }): typeof defineService {
17
19
  return ((name: string, definition: ServiceDefinition<S>) => {
18
- const commerce = createIapCommerce({ onChange: () => { void service.iapPublish(); } });
20
+ const commerce = createIapCommerce({ adapters: options.adapters, onChange: () => { void service.iapPublish(); } });
19
21
  options.onCreate?.(commerce);
20
22
  let items: IapCatalogItem[] = [];
21
23
  let contentVersion = 0;
@@ -1,7 +1,7 @@
1
1
  // Tier-5 bundle: himi-store — product detail.
2
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
+ import { itemPaymentEnabled, storesProductOrders, type IapCommerce, type IapPurchaseFields } from "@himalaya/iap-checkout";
4
+ import { iapPriceCopy, iapPurchaseCopy } from "../_shared/iap-worker.js";
5
5
  import { share as shareSheet } from "@himalaya/share";
6
6
  import { addToCurrentCart, getCurrentCart } from "@himalaya/wix-ecom";
7
7
  import { get, upsert, enqueue, type DbEntity } from "@himalaya/local-db";
@@ -202,6 +202,14 @@ interface S {
202
202
  opt1Label: string; opt1Chips: OptionChip[]; opt1Selected: string[]; noOpt1: boolean;
203
203
  opt2Label: string; opt2Chips: OptionChip[]; opt2Selected: string[]; noOpt2: boolean;
204
204
  opt3Label: string; opt3Chips: OptionChip[]; opt3Selected: string[]; noOpt3: boolean;
205
+ /** The App Store action (`commerce.purchaseFields`), in place of Add to cart while it shows. */
206
+ cartHidden: boolean;
207
+ buyHidden: boolean;
208
+ buyLabel: string;
209
+ buyDisabled: boolean;
210
+ buyNote: string;
211
+ /** Shown once the eCom order reads paid, never on the payment's approval alone. */
212
+ purchasedNote: string;
205
213
  }
206
214
 
207
215
  function inputId(): string {
@@ -309,14 +317,53 @@ function derive(s: S): S {
309
317
  // read answering late — must not replace the badge the add set (#3468 review).
310
318
  let cartBadgeEpoch = 0;
311
319
 
320
+ // A digital product with no options sells through the App Store once the platform pays for Wix
321
+ // Stores items with wix-iap StartItemPayment (`itemPaymentEnabled`). Until then, and for any other
322
+ // product, an App Store route only blocks the regular checkout, as before.
323
+ const SELLS_THROUGH_APPLE = itemPaymentEnabled();
324
+ const WIX_STORES_APP = "215238eb-22a5-4c36-9e7b-e7c08025e04e";
325
+ const NOT_APPLE: IapPurchaseFields = { appleRoute: false, accessReady: false, buyLabel: "", buyDisabled: true, buyBusy: false, buyNote: "" };
326
+
312
327
  let productPricing: IapCommerce;
328
+ let checkoutEntered = false;
313
329
  const defineService = withIapPresentation<S>({
314
- onCreate: commerce => { productPricing = commerce; }, prefix: "product", transientKeys: ["price", "optionsIncomplete"], contentActions: ["load", "paintCached"],
330
+ adapters: SELLS_THROUGH_APPLE ? [storesProductOrders] : [],
331
+ onCreate: commerce => { productPricing = commerce; }, prefix: "product",
332
+ transientKeys: ["price", "optionsIncomplete", "cartHint", "cartHidden", "buyHidden", "buyLabel", "buyDisabled", "buyNote", "purchasedNote"], contentActions: ["load", "paintCached"],
315
333
  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 }),
334
+ present: (state, price, allowed) => {
335
+ const purchase = applePurchase(state);
336
+ const unavailable = iapPriceCopy("product").purchaseUnavailable;
337
+ const blocked = lastProduct?.productType?.toLowerCase() === "digital" || (!state.optionsUnknown && !allowed(inputId()));
338
+ return {
339
+ ...state, price: price(inputId()), optionsIncomplete: derive(state).optionsIncomplete || !allowed(inputId()) || lastProduct?.productType?.toLowerCase() === "digital",
340
+ cartHint: !purchase.appleRoute && blocked ? unavailable : state.cartHint === unavailable ? "" : state.cartHint,
341
+ cartHidden: purchase.appleRoute,
342
+ buyHidden: !purchase.appleRoute || purchase.accessReady, buyLabel: purchase.buyLabel, buyDisabled: purchase.buyDisabled, buyNote: purchase.buyNote,
343
+ purchasedNote: purchase.accessReady ? copyParam("product.purchasedText", "Purchased") : "",
344
+ };
345
+ },
318
346
  });
319
347
 
348
+ /** The App Store action, only for the one shape it sells: digital, with no options to choose, read
349
+ * from the product loaded for the id on screen (as Add to cart is). A cached product whose
350
+ * options were never read is not known to have none. */
351
+ function applePurchase(state: S): IapPurchaseFields {
352
+ const id = inputId();
353
+ const loaded = !!id && lastLoadedId === id;
354
+ const sellable = SELLS_THROUGH_APPLE && loaded && lastProduct?.productType?.toLowerCase() === "digital" && !state.optionsUnknown && state.optionGroups.length === 0;
355
+ const copy = { ...iapPurchaseCopy("product"), buy: copyParam("product.buyCta", "Buy") };
356
+ const fields = sellable ? productPricing.purchaseFields({ kind: "storesProduct", id }, copy) : NOT_APPLE;
357
+ return fields.appleRoute ? fields : NOT_APPLE;
358
+ }
359
+
360
+ /** On every entry: the first starts the checkout (recovering held purchases), later ones reconcile. */
361
+ function enterCheckout(): void {
362
+ if (!SELLS_THROUGH_APPLE) return;
363
+ void (checkoutEntered ? productPricing.reconcile() : productPricing.start());
364
+ checkoutEntered = true;
365
+ }
366
+
320
367
  const svc = defineService<S, {
321
368
  load: (s: S, arg?: unknown) => Promise<S>;
322
369
  setQuantity: (s: S, arg?: unknown) => S;
@@ -329,6 +376,8 @@ const svc = defineService<S, {
329
376
  toggleSave: (s: S) => S;
330
377
  /** [rico] #3372 — RichContent's on_link: `{ url }` of the tapped link. */
331
378
  openLink: (s: S, arg?: unknown) => Promise<S>;
379
+ /** The App Store Buy: through the shared checkout, for the product on screen. */
380
+ buy: (s: S) => Promise<S>;
332
381
  }>("himiStoreProduct", {
333
382
  state: derive({
334
383
  status: "loading",
@@ -375,10 +424,17 @@ const svc = defineService<S, {
375
424
  opt1Label: "", opt1Chips: [], opt1Selected: [], noOpt1: true,
376
425
  opt2Label: "", opt2Chips: [], opt2Selected: [], noOpt2: true,
377
426
  opt3Label: "", opt3Chips: [], opt3Selected: [], noOpt3: true,
427
+ cartHidden: false,
428
+ buyHidden: true,
429
+ buyLabel: "",
430
+ buyDisabled: true,
431
+ buyNote: "",
432
+ purchasedNote: "",
378
433
  }),
379
434
  actions: {
380
435
  load: async (s, arg): Promise<S> => {
381
436
  stashCopy(arg);
437
+ enterCheckout();
382
438
  const sequence = ++productLoadSequence;
383
439
  lastProduct = undefined;
384
440
  const id = inputId();
@@ -657,6 +713,13 @@ const svc = defineService<S, {
657
713
  openLink: async (s, arg) => ((await openRichLink(arg))
658
714
  ? s
659
715
  : derive({ ...s, added: false, cartHint: copyParam("product.linkOpenFailed", "This link could not be opened.") })),
716
+ buy: async (s): Promise<S> => {
717
+ const id = inputId();
718
+ const purchase = applePurchase(s);
719
+ if (!purchase.appleRoute || purchase.buyDisabled) return s;
720
+ await productPricing.buy({ reference: { kind: "storesProduct", id }, catalogReference: { appId: WIX_STORES_APP, catalogItemId: id }, quantity: 1 });
721
+ return svc.getState() as S;
722
+ },
660
723
  },
661
724
  });
662
725
 
@@ -1,11 +1,13 @@
1
1
  import { defineService, type ServiceDefinition } from "@himalaya/state";
2
- import { createIapCommerce, type IapCommerce } from "@himalaya/iap-checkout";
2
+ import { createIapCommerce, type IapCommerce, type IapOrderAdapter } from "@himalaya/iap-checkout";
3
3
  import { iapPriceFields, type IapCatalogItem } from "@himalaya/iap";
4
4
  import { iapPriceCopy, resetUnhydratedKeysOnOlderRuntime } from "./iap-worker.js";
5
5
 
6
6
  // Presentation only. Checkout, mapping, cache, session and refresh policy stay in the shared flow.
7
7
  export function withIapPresentation<S extends object>(options: {
8
8
  register?: typeof defineService;
9
+ /** The verticals this screen sells; none for a screen that only shows prices. */
10
+ adapters?: readonly IapOrderAdapter[];
9
11
  onCreate?(commerce: IapCommerce): void;
10
12
  onContent?(state: S): void;
11
13
  items(state: S): IapCatalogItem[];
@@ -15,7 +17,7 @@ export function withIapPresentation<S extends object>(options: {
15
17
  prefix: string;
16
18
  }): typeof defineService {
17
19
  return ((name: string, definition: ServiceDefinition<S>) => {
18
- const commerce = createIapCommerce({ onChange: () => { void service.iapPublish(); } });
20
+ const commerce = createIapCommerce({ adapters: options.adapters, onChange: () => { void service.iapPublish(); } });
19
21
  options.onCreate?.(commerce);
20
22
  let items: IapCatalogItem[] = [];
21
23
  let contentVersion = 0;
@@ -1,11 +1,13 @@
1
1
  import { defineService, type ServiceDefinition } from "@himalaya/state";
2
- import { createIapCommerce, type IapCommerce } from "@himalaya/iap-checkout";
2
+ import { createIapCommerce, type IapCommerce, type IapOrderAdapter } from "@himalaya/iap-checkout";
3
3
  import { iapPriceFields, type IapCatalogItem } from "@himalaya/iap";
4
4
  import { iapPriceCopy, resetUnhydratedKeysOnOlderRuntime } from "./iap-worker.js";
5
5
 
6
6
  // Presentation only. Checkout, mapping, cache, session and refresh policy stay in the shared flow.
7
7
  export function withIapPresentation<S extends object>(options: {
8
8
  register?: typeof defineService;
9
+ /** The verticals this screen sells; none for a screen that only shows prices. */
10
+ adapters?: readonly IapOrderAdapter[];
9
11
  onCreate?(commerce: IapCommerce): void;
10
12
  onContent?(state: S): void;
11
13
  items(state: S): IapCatalogItem[];
@@ -15,7 +17,7 @@ export function withIapPresentation<S extends object>(options: {
15
17
  prefix: string;
16
18
  }): typeof defineService {
17
19
  return ((name: string, definition: ServiceDefinition<S>) => {
18
- const commerce = createIapCommerce({ onChange: () => { void service.iapPublish(); } });
20
+ const commerce = createIapCommerce({ adapters: options.adapters, onChange: () => { void service.iapPublish(); } });
19
21
  options.onCreate?.(commerce);
20
22
  let items: IapCatalogItem[] = [];
21
23
  let contentVersion = 0;
@@ -31318,7 +31318,55 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
31318
31318
  "modifiers": {
31319
31319
  "cornerRadius": 8,
31320
31320
  "pressFeedback": "scale",
31321
- "haptic": "medium"
31321
+ "haptic": "medium",
31322
+ "hiddenPath": "$service:himiStoreProduct.cartHidden"
31323
+ }
31324
+ },
31325
+ {
31326
+ "id": "himi_product.buy",
31327
+ "body": {
31328
+ "package": "himalaya",
31329
+ "type": "Button",
31330
+ "props": {
31331
+ "title": "$service:himiStoreProduct.buyLabel",
31332
+ "style": "primary",
31333
+ "fullWidth": true,
31334
+ "disabledPath": "$service:himiStoreProduct.buyDisabled",
31335
+ "action": {
31336
+ "actionId": "tier5.himiStoreProduct.buy"
31337
+ }
31338
+ }
31339
+ },
31340
+ "modifiers": {
31341
+ "hiddenPath": "$service:himiStoreProduct.buyHidden"
31342
+ }
31343
+ },
31344
+ {
31345
+ "id": "himi_product.buy_note",
31346
+ "body": {
31347
+ "package": "himalaya",
31348
+ "type": "Text",
31349
+ "props": {
31350
+ "text": "$service:himiStoreProduct.buyNote",
31351
+ "style": "footnote",
31352
+ "color": "textSecondary",
31353
+ "collapseWhenEmpty": true
31354
+ }
31355
+ },
31356
+ "modifiers": {
31357
+ "hiddenPath": "$service:himiStoreProduct.buyHidden"
31358
+ }
31359
+ },
31360
+ {
31361
+ "id": "himi_product.purchased",
31362
+ "body": {
31363
+ "package": "himalaya",
31364
+ "type": "Text",
31365
+ "props": {
31366
+ "text": "$service:himiStoreProduct.purchasedNote",
31367
+ "style": "footnote",
31368
+ "collapseWhenEmpty": true
31369
+ }
31322
31370
  }
31323
31371
  },
31324
31372
  {
@@ -1,11 +1,13 @@
1
1
  import { defineService, type ServiceDefinition } from "@himalaya/state";
2
- import { createIapCommerce, type IapCommerce } from "@himalaya/iap-checkout";
2
+ import { createIapCommerce, type IapCommerce, type IapOrderAdapter } from "@himalaya/iap-checkout";
3
3
  import { iapPriceFields, type IapCatalogItem } from "@himalaya/iap";
4
4
  import { iapPriceCopy, resetUnhydratedKeysOnOlderRuntime } from "./iap-worker.js";
5
5
 
6
6
  // Presentation only. Checkout, mapping, cache, session and refresh policy stay in the shared flow.
7
7
  export function withIapPresentation<S extends object>(options: {
8
8
  register?: typeof defineService;
9
+ /** The verticals this screen sells; none for a screen that only shows prices. */
10
+ adapters?: readonly IapOrderAdapter[];
9
11
  onCreate?(commerce: IapCommerce): void;
10
12
  onContent?(state: S): void;
11
13
  items(state: S): IapCatalogItem[];
@@ -15,7 +17,7 @@ export function withIapPresentation<S extends object>(options: {
15
17
  prefix: string;
16
18
  }): typeof defineService {
17
19
  return ((name: string, definition: ServiceDefinition<S>) => {
18
- const commerce = createIapCommerce({ onChange: () => { void service.iapPublish(); } });
20
+ const commerce = createIapCommerce({ adapters: options.adapters, onChange: () => { void service.iapPublish(); } });
19
21
  options.onCreate?.(commerce);
20
22
  let items: IapCatalogItem[] = [];
21
23
  let contentVersion = 0;