@wix/himalaya-cli 0.1324.0 → 0.1325.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.
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/checkout/src/index.d.ts +6 -10
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/order-refs.d.ts +1 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/purchase-fields.d.ts +1 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/screens/src/index.d.ts +9 -35
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/stores-v1/src/display.d.ts +1 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/checkout.mjs +30 -13
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap-checkout.mjs +20 -2
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/screens.mjs +2890 -54
- package/dist/cli.mjs +4 -4
- package/fixtures/wix-canned.json +61 -6
- package/guides/iap-pricing.md +9 -0
- package/guides/iap-screen-adoption.md +77 -0
- package/guides/importable-sdks.md +2 -0
- package/guides/index.json +6 -0
- package/guides/sdk-reference.json +339 -1
- package/package.json +2 -2
- package/templates/beauty-salon/config.ts +1 -1
- package/templates/beauty-salon/dev/index.ts +46 -18
- package/templates/beauty-salon/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/beauty-salon/tier5-src/_shared/collection-service.ts +3 -2
- package/templates/beauty-salon/tier5-src/_shared/himi-store-collection.ts +1 -1
- package/templates/beauty-salon/tier5-src/_shared/home-service.ts +2 -1
- package/templates/beauty-salon/tier5-src/_shared/iap-presentation.ts +53 -0
- package/templates/beauty-salon/tier5-src/_shared/iap-worker.ts +42 -0
- package/templates/beauty-salon/tier5-src/himi-branded-lite-cart/index.ts +48 -1
- package/templates/beauty-salon/tier5-src/himi-branded-lite-home/index.ts +16 -2
- package/templates/beauty-salon/tier5-src/himi-branded-lite-order/index.ts +24 -3
- package/templates/beauty-salon/tier5-src/himi-branded-lite-plans/index.ts +153 -64
- package/templates/beauty-salon/tier5-src/himi-store-product/index.ts +24 -3
- package/templates/beauty-salon/tier5-src/himi-store-saved/index.ts +8 -3
- package/templates/beauty-salon/tier5-src/himi-store-shop/index.ts +10 -0
- package/templates/beauty-salon/tier5-src/wix-spaces-plan-detail/index.ts +8 -3
- package/templates/beauty-salon-owner/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/beauty-salon-owner/tier5-src/_shared/collection-service.ts +3 -2
- package/templates/beauty-salon-owner/tier5-src/_shared/home-service.ts +2 -1
- package/templates/branded-lite/config.ts +1 -1
- package/templates/branded-lite/dev/index.ts +46 -18
- package/templates/branded-lite/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/branded-lite/tier5-src/_shared/collection-service.ts +3 -2
- package/templates/branded-lite/tier5-src/_shared/himi-store-collection.ts +1 -1
- package/templates/branded-lite/tier5-src/_shared/home-service.ts +2 -1
- package/templates/branded-lite/tier5-src/_shared/iap-presentation.ts +53 -0
- package/templates/branded-lite/tier5-src/himi-branded-lite-cart/index.ts +48 -1
- package/templates/branded-lite/tier5-src/himi-branded-lite-home/index.ts +16 -2
- package/templates/branded-lite/tier5-src/himi-branded-lite-order/index.ts +24 -3
- package/templates/branded-lite/tier5-src/himi-branded-lite-plans/index.ts +153 -64
- package/templates/branded-lite/tier5-src/himi-store-product/index.ts +24 -3
- package/templates/branded-lite/tier5-src/himi-store-saved/index.ts +8 -3
- package/templates/branded-lite/tier5-src/himi-store-shop/index.ts +10 -0
- package/templates/branded-lite/tier5-src/wix-spaces-plan-detail/index.ts +8 -3
- package/templates/community-club/config.ts +1 -1
- package/templates/community-club/dev/index.ts +3 -1
- package/templates/community-club/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/community-club/tier5-src/_shared/collection-service.ts +3 -2
- package/templates/community-club/tier5-src/_shared/home-service.ts +2 -1
- package/templates/community-club/tier5-src/_shared/iap-presentation.ts +53 -0
- package/templates/community-club/tier5-src/_shared/iap-worker.ts +42 -0
- package/templates/community-club/tier5-src/himi-branded-lite-home/index.ts +16 -2
- package/templates/community-club-owner/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/community-club-owner/tier5-src/_shared/collection-service.ts +3 -2
- package/templates/community-club-owner/tier5-src/_shared/home-service.ts +2 -1
- package/templates/events-meetups/config.ts +1 -1
- package/templates/events-meetups/dev/index.ts +3 -1
- package/templates/events-meetups/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/events-meetups/tier5-src/_shared/collection-service.ts +3 -2
- package/templates/events-meetups/tier5-src/_shared/home-service.ts +2 -1
- package/templates/events-meetups/tier5-src/_shared/iap-presentation.ts +53 -0
- package/templates/events-meetups/tier5-src/_shared/iap-worker.ts +42 -0
- package/templates/events-meetups/tier5-src/himi-branded-lite-home/index.ts +16 -2
- package/templates/events-meetups-owner/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/events-meetups-owner/tier5-src/_shared/collection-service.ts +3 -2
- package/templates/events-meetups-owner/tier5-src/_shared/home-service.ts +2 -1
- package/templates/gym-fitness/config.ts +1 -1
- package/templates/gym-fitness/dev/index.ts +46 -18
- package/templates/gym-fitness/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/gym-fitness/tier5-src/_shared/collection-service.ts +3 -2
- package/templates/gym-fitness/tier5-src/_shared/himi-store-collection.ts +1 -1
- package/templates/gym-fitness/tier5-src/_shared/home-service.ts +2 -1
- package/templates/gym-fitness/tier5-src/_shared/iap-presentation.ts +53 -0
- package/templates/gym-fitness/tier5-src/_shared/iap-worker.ts +42 -0
- package/templates/gym-fitness/tier5-src/himi-branded-lite-cart/index.ts +48 -1
- package/templates/gym-fitness/tier5-src/himi-branded-lite-home/index.ts +16 -2
- package/templates/gym-fitness/tier5-src/himi-branded-lite-order/index.ts +24 -3
- package/templates/gym-fitness/tier5-src/himi-branded-lite-plans/index.ts +153 -64
- package/templates/gym-fitness/tier5-src/himi-store-product/index.ts +24 -3
- package/templates/gym-fitness/tier5-src/himi-store-saved/index.ts +8 -3
- package/templates/gym-fitness/tier5-src/himi-store-shop/index.ts +10 -0
- package/templates/gym-fitness/tier5-src/wix-spaces-plan-detail/index.ts +8 -3
- package/templates/gym-fitness-owner/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/gym-fitness-owner/tier5-src/_shared/collection-service.ts +3 -2
- package/templates/gym-fitness-owner/tier5-src/_shared/home-service.ts +2 -1
- package/templates/nonprofit-cause/config.ts +1 -1
- package/templates/nonprofit-cause/dev/index.ts +29 -13
- package/templates/nonprofit-cause/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/nonprofit-cause/tier5-src/_shared/collection-service.ts +3 -2
- package/templates/nonprofit-cause/tier5-src/_shared/home-service.ts +2 -1
- package/templates/nonprofit-cause/tier5-src/_shared/iap-presentation.ts +53 -0
- package/templates/nonprofit-cause/tier5-src/_shared/iap-worker.ts +42 -0
- package/templates/nonprofit-cause/tier5-src/himi-branded-lite-home/index.ts +16 -2
- package/templates/nonprofit-cause/tier5-src/himi-branded-lite-plans/index.ts +153 -64
- package/templates/nonprofit-cause/tier5-src/wix-spaces-plan-detail/index.ts +8 -3
- package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-service.ts +3 -2
- package/templates/nonprofit-cause-owner/tier5-src/_shared/home-service.ts +2 -1
- package/templates/owner-lite/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/owner-lite/tier5-src/_shared/collection-service.ts +3 -2
- package/templates/owner-lite/tier5-src/_shared/home-service.ts +2 -1
- package/templates/podcast-creator/config.ts +1 -1
- package/templates/podcast-creator/dev/index.ts +29 -13
- package/templates/podcast-creator/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/podcast-creator/tier5-src/_shared/collection-service.ts +3 -2
- package/templates/podcast-creator/tier5-src/_shared/home-service.ts +2 -1
- package/templates/podcast-creator/tier5-src/_shared/iap-presentation.ts +53 -0
- package/templates/podcast-creator/tier5-src/_shared/iap-worker.ts +42 -0
- package/templates/podcast-creator/tier5-src/himi-branded-lite-home/index.ts +16 -2
- package/templates/podcast-creator/tier5-src/himi-branded-lite-plans/index.ts +153 -64
- package/templates/podcast-creator/tier5-src/wix-spaces-plan-detail/index.ts +8 -3
- package/templates/podcast-creator-owner/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/podcast-creator-owner/tier5-src/_shared/collection-service.ts +3 -2
- package/templates/podcast-creator-owner/tier5-src/_shared/home-service.ts +2 -1
- package/templates/portfolio-creative/config.ts +1 -1
- package/templates/portfolio-creative/dev/index.ts +20 -6
- package/templates/portfolio-creative/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/portfolio-creative/tier5-src/_shared/collection-service.ts +3 -2
- package/templates/portfolio-creative/tier5-src/_shared/himi-store-collection.ts +1 -1
- package/templates/portfolio-creative/tier5-src/_shared/home-service.ts +2 -1
- package/templates/portfolio-creative/tier5-src/_shared/iap-presentation.ts +53 -0
- package/templates/portfolio-creative/tier5-src/_shared/iap-worker.ts +42 -0
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-cart/index.ts +48 -1
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-home/index.ts +16 -2
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-order/index.ts +24 -3
- package/templates/portfolio-creative/tier5-src/himi-store-product/index.ts +24 -3
- package/templates/portfolio-creative/tier5-src/himi-store-saved/index.ts +8 -3
- package/templates/portfolio-creative/tier5-src/himi-store-shop/index.ts +10 -0
- package/templates/portfolio-creative-owner/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/portfolio-creative-owner/tier5-src/_shared/collection-service.ts +3 -2
- package/templates/portfolio-creative-owner/tier5-src/_shared/home-service.ts +2 -1
- package/templates/restaurant/config.ts +1 -1
- package/templates/restaurant/dev/index.ts +3 -1
- package/templates/restaurant/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/restaurant/tier5-src/_shared/collection-service.ts +3 -2
- package/templates/restaurant/tier5-src/_shared/home-service.ts +2 -1
- package/templates/restaurant/tier5-src/_shared/iap-presentation.ts +53 -0
- package/templates/restaurant/tier5-src/_shared/iap-worker.ts +42 -0
- package/templates/restaurant/tier5-src/himi-branded-lite-home/index.ts +16 -2
- package/templates/restaurant-owner/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/restaurant-owner/tier5-src/_shared/collection-service.ts +3 -2
- package/templates/restaurant-owner/tier5-src/_shared/home-service.ts +2 -1
- package/templates/retail-store/config.ts +1 -1
- package/templates/retail-store/dev/index.ts +20 -6
- package/templates/retail-store/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/retail-store/tier5-src/_shared/collection-service.ts +3 -2
- package/templates/retail-store/tier5-src/_shared/himi-store-collection.ts +1 -1
- package/templates/retail-store/tier5-src/_shared/home-service.ts +2 -1
- package/templates/retail-store/tier5-src/_shared/iap-presentation.ts +53 -0
- package/templates/retail-store/tier5-src/_shared/iap-worker.ts +42 -0
- package/templates/retail-store/tier5-src/himi-branded-lite-cart/index.ts +48 -1
- package/templates/retail-store/tier5-src/himi-branded-lite-home/index.ts +16 -2
- package/templates/retail-store/tier5-src/himi-branded-lite-order/index.ts +24 -3
- package/templates/retail-store/tier5-src/himi-store-product/index.ts +24 -3
- package/templates/retail-store/tier5-src/himi-store-saved/index.ts +8 -3
- package/templates/retail-store/tier5-src/himi-store-shop/index.ts +10 -0
- package/templates/retail-store-owner/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/retail-store-owner/tier5-src/_shared/collection-service.ts +3 -2
- package/templates/retail-store-owner/tier5-src/_shared/home-service.ts +2 -1
- package/templates/yoga-studio/config.ts +1 -1
- package/templates/yoga-studio/dev/index.ts +29 -13
- package/templates/yoga-studio/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/yoga-studio/tier5-src/_shared/collection-service.ts +3 -2
- package/templates/yoga-studio/tier5-src/_shared/home-service.ts +2 -1
- package/templates/yoga-studio/tier5-src/_shared/iap-presentation.ts +53 -0
- package/templates/yoga-studio/tier5-src/himi-branded-lite-home/index.ts +16 -2
- package/templates/yoga-studio/tier5-src/himi-branded-lite-plans/index.ts +153 -64
- package/templates/yoga-studio/tier5-src/wix-spaces-plan-detail/index.ts +8 -3
- package/templates/yoga-studio-owner/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/yoga-studio-owner/tier5-src/_shared/collection-service.ts +3 -2
- 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 {
|
|
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.
|
|
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 {
|
|
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 }),
|
|
@@ -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 {
|
|
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>) =>
|
|
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","media_playback","wix_api","wix_platform","app_info","release_control","clipboard","motion"],
|
|
40
|
+
capabilities: ["live_activity","push","share","secure_storage","open_url","calendar","local_database","media_playback","wix_api","wix_platform","app_info","iap","release_control","clipboard","motion"],
|
|
41
41
|
designTokensPath: "./tokens.json",
|
|
42
42
|
navigation: {
|
|
43
43
|
"navKind": "responsive",
|
|
@@ -2971,7 +2971,9 @@ const SCREENS = runtimeSetting({
|
|
|
2971
2971
|
"integrity": "",
|
|
2972
2972
|
"contentType": "application/javascript+ts-bundle",
|
|
2973
2973
|
"declaredCapabilities": [
|
|
2974
|
-
"wix_api"
|
|
2974
|
+
"wix_api",
|
|
2975
|
+
"iap",
|
|
2976
|
+
"app_info"
|
|
2975
2977
|
],
|
|
2976
2978
|
"minBinaryVersion": "0.2.0",
|
|
2977
2979
|
"bundleVersion": "0.0.1"
|
|
@@ -8896,7 +8898,9 @@ const SCREENS = runtimeSetting({
|
|
|
8896
8898
|
"contentType": "application/javascript+ts-bundle",
|
|
8897
8899
|
"declaredCapabilities": [
|
|
8898
8900
|
"wix_api",
|
|
8899
|
-
"local_database"
|
|
8901
|
+
"local_database",
|
|
8902
|
+
"iap",
|
|
8903
|
+
"app_info"
|
|
8900
8904
|
],
|
|
8901
8905
|
"minBinaryVersion": "0.1.0",
|
|
8902
8906
|
"bundleVersion": "0.0.1"
|
|
@@ -9364,7 +9368,11 @@ const SCREENS = runtimeSetting({
|
|
|
9364
9368
|
"url": "http://localhost:8190/v1/bundles/himi-store-saved",
|
|
9365
9369
|
"integrity": "",
|
|
9366
9370
|
"contentType": "application/javascript+ts-bundle",
|
|
9367
|
-
"declaredCapabilities": [
|
|
9371
|
+
"declaredCapabilities": [
|
|
9372
|
+
"wix_api",
|
|
9373
|
+
"iap",
|
|
9374
|
+
"app_info"
|
|
9375
|
+
],
|
|
9368
9376
|
"minBinaryVersion": "0.1.0",
|
|
9369
9377
|
"bundleVersion": "0.0.1"
|
|
9370
9378
|
}
|
|
@@ -10890,7 +10898,9 @@ const SCREENS = runtimeSetting({
|
|
|
10890
10898
|
"wix_cashier",
|
|
10891
10899
|
"open_url",
|
|
10892
10900
|
"local_database",
|
|
10893
|
-
"wix_platform"
|
|
10901
|
+
"wix_platform",
|
|
10902
|
+
"iap",
|
|
10903
|
+
"app_info"
|
|
10894
10904
|
],
|
|
10895
10905
|
"minBinaryVersion": "0.1.0",
|
|
10896
10906
|
"bundleVersion": "0.0.1"
|
|
@@ -25546,7 +25556,8 @@ const SCREENS = runtimeSetting({
|
|
|
25546
25556
|
"package": "himalaya",
|
|
25547
25557
|
"type": "Button",
|
|
25548
25558
|
"props": {
|
|
25549
|
-
"title": "
|
|
25559
|
+
"title": "$item.joinLabel",
|
|
25560
|
+
"disabledPath": "$item.joinDisabled",
|
|
25550
25561
|
"style": "primary",
|
|
25551
25562
|
"fullWidth": true,
|
|
25552
25563
|
"action": {
|
|
@@ -25567,7 +25578,8 @@ const SCREENS = runtimeSetting({
|
|
|
25567
25578
|
"package": "himalaya",
|
|
25568
25579
|
"type": "Button",
|
|
25569
25580
|
"props": {
|
|
25570
|
-
"title": "
|
|
25581
|
+
"title": "$item.joinLabel",
|
|
25582
|
+
"disabledPath": "$item.joinDisabled",
|
|
25571
25583
|
"style": "secondary",
|
|
25572
25584
|
"fullWidth": true,
|
|
25573
25585
|
"action": {
|
|
@@ -25748,7 +25760,8 @@ const SCREENS = runtimeSetting({
|
|
|
25748
25760
|
"package": "himalaya",
|
|
25749
25761
|
"type": "Button",
|
|
25750
25762
|
"props": {
|
|
25751
|
-
"title": "
|
|
25763
|
+
"title": "$item.joinLabel",
|
|
25764
|
+
"disabledPath": "$item.joinDisabled",
|
|
25752
25765
|
"style": "primary",
|
|
25753
25766
|
"fullWidth": true,
|
|
25754
25767
|
"action": {
|
|
@@ -25769,7 +25782,8 @@ const SCREENS = runtimeSetting({
|
|
|
25769
25782
|
"package": "himalaya",
|
|
25770
25783
|
"type": "Button",
|
|
25771
25784
|
"props": {
|
|
25772
|
-
"title": "
|
|
25785
|
+
"title": "$item.joinLabel",
|
|
25786
|
+
"disabledPath": "$item.joinDisabled",
|
|
25773
25787
|
"style": "secondary",
|
|
25774
25788
|
"fullWidth": true,
|
|
25775
25789
|
"action": {
|
|
@@ -26174,7 +26188,8 @@ const SCREENS = runtimeSetting({
|
|
|
26174
26188
|
"package": "himalaya",
|
|
26175
26189
|
"type": "Button",
|
|
26176
26190
|
"props": {
|
|
26177
|
-
"title": "
|
|
26191
|
+
"title": "$item.joinLabel",
|
|
26192
|
+
"disabledPath": "$item.joinDisabled",
|
|
26178
26193
|
"style": "primary",
|
|
26179
26194
|
"fullWidth": true,
|
|
26180
26195
|
"action": {
|
|
@@ -26195,7 +26210,8 @@ const SCREENS = runtimeSetting({
|
|
|
26195
26210
|
"package": "himalaya",
|
|
26196
26211
|
"type": "Button",
|
|
26197
26212
|
"props": {
|
|
26198
|
-
"title": "
|
|
26213
|
+
"title": "$item.joinLabel",
|
|
26214
|
+
"disabledPath": "$item.joinDisabled",
|
|
26199
26215
|
"style": "secondary",
|
|
26200
26216
|
"fullWidth": true,
|
|
26201
26217
|
"action": {
|
|
@@ -26376,7 +26392,8 @@ const SCREENS = runtimeSetting({
|
|
|
26376
26392
|
"package": "himalaya",
|
|
26377
26393
|
"type": "Button",
|
|
26378
26394
|
"props": {
|
|
26379
|
-
"title": "
|
|
26395
|
+
"title": "$item.joinLabel",
|
|
26396
|
+
"disabledPath": "$item.joinDisabled",
|
|
26380
26397
|
"style": "primary",
|
|
26381
26398
|
"fullWidth": true,
|
|
26382
26399
|
"action": {
|
|
@@ -26397,7 +26414,8 @@ const SCREENS = runtimeSetting({
|
|
|
26397
26414
|
"package": "himalaya",
|
|
26398
26415
|
"type": "Button",
|
|
26399
26416
|
"props": {
|
|
26400
|
-
"title": "
|
|
26417
|
+
"title": "$item.joinLabel",
|
|
26418
|
+
"disabledPath": "$item.joinDisabled",
|
|
26401
26419
|
"style": "secondary",
|
|
26402
26420
|
"fullWidth": true,
|
|
26403
26421
|
"action": {
|
|
@@ -26975,7 +26993,8 @@ const SCREENS = runtimeSetting({
|
|
|
26975
26993
|
"package": "himalaya",
|
|
26976
26994
|
"type": "Button",
|
|
26977
26995
|
"props": {
|
|
26978
|
-
"title": "
|
|
26996
|
+
"title": "$item.joinLabel",
|
|
26997
|
+
"disabledPath": "$item.joinDisabled",
|
|
26979
26998
|
"style": "primary",
|
|
26980
26999
|
"fullWidth": true,
|
|
26981
27000
|
"action": {
|
|
@@ -26996,7 +27015,8 @@ const SCREENS = runtimeSetting({
|
|
|
26996
27015
|
"package": "himalaya",
|
|
26997
27016
|
"type": "Button",
|
|
26998
27017
|
"props": {
|
|
26999
|
-
"title": "
|
|
27018
|
+
"title": "$item.joinLabel",
|
|
27019
|
+
"disabledPath": "$item.joinDisabled",
|
|
27000
27020
|
"style": "secondary",
|
|
27001
27021
|
"fullWidth": true,
|
|
27002
27022
|
"action": {
|
|
@@ -27271,7 +27291,9 @@ const SCREENS = runtimeSetting({
|
|
|
27271
27291
|
"wix_api",
|
|
27272
27292
|
"wix_cashier",
|
|
27273
27293
|
"open_url",
|
|
27274
|
-
"wix_platform"
|
|
27294
|
+
"wix_platform",
|
|
27295
|
+
"iap",
|
|
27296
|
+
"app_info"
|
|
27275
27297
|
],
|
|
27276
27298
|
"minBinaryVersion": "0.1.0",
|
|
27277
27299
|
"bundleVersion": "0.0.1"
|
|
@@ -27527,7 +27549,9 @@ const SCREENS = runtimeSetting({
|
|
|
27527
27549
|
"integrity": "",
|
|
27528
27550
|
"contentType": "application/javascript+ts-bundle",
|
|
27529
27551
|
"declaredCapabilities": [
|
|
27530
|
-
"wix_api"
|
|
27552
|
+
"wix_api",
|
|
27553
|
+
"iap",
|
|
27554
|
+
"app_info"
|
|
27531
27555
|
],
|
|
27532
27556
|
"minBinaryVersion": "0.1.0",
|
|
27533
27557
|
"bundleVersion": "0.0.1"
|
|
@@ -81851,7 +81875,9 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
81851
81875
|
"declaredCapabilities": [
|
|
81852
81876
|
"wix_api",
|
|
81853
81877
|
"local_database",
|
|
81854
|
-
"live_activity"
|
|
81878
|
+
"live_activity",
|
|
81879
|
+
"iap",
|
|
81880
|
+
"app_info"
|
|
81855
81881
|
],
|
|
81856
81882
|
"minBinaryVersion": "0.1.0",
|
|
81857
81883
|
"bundleVersion": "0.0.1"
|
|
@@ -82733,7 +82759,9 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
82733
82759
|
"wix_api",
|
|
82734
82760
|
"share",
|
|
82735
82761
|
"local_database",
|
|
82736
|
-
"open_url"
|
|
82762
|
+
"open_url",
|
|
82763
|
+
"iap",
|
|
82764
|
+
"app_info"
|
|
82737
82765
|
],
|
|
82738
82766
|
"minBinaryVersion": "0.1.0",
|
|
82739
82767
|
"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>) =>
|
|
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
|
+
}
|