@wix/himalaya-cli 0.1323.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/comments-v1/src/extensions.d.ts +79 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/groups/src/extensions.d.ts +221 -14
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/groups/src/index.d.ts +0 -2
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/social-feed/src/index.d.ts +38 -21
- 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/authoring-runtime/node_modules/@wix/himalaya/dist/wix-comments.mjs +80 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-groups.mjs +45 -17
- package/dist/cli.mjs +5 -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/release-notes.md +25 -0
- package/guides/sdk-reference.json +539 -32
- package/guides/wix-api-usage.md +5 -0
- 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 -8
- package/templates/branded-lite/dev/index.ts +46 -134
- package/templates/branded-lite/himalaya.content.json +0 -3
- 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 -8
- package/templates/community-club/dev/index.ts +3 -117
- package/templates/community-club/himalaya.content.json +0 -3
- 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 -8
- package/templates/events-meetups/dev/index.ts +3 -117
- package/templates/events-meetups/himalaya.content.json +0 -3
- 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 -8
- package/templates/nonprofit-cause/dev/index.ts +29 -129
- package/templates/nonprofit-cause/himalaya.content.json +0 -3
- 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
- package/templates/branded-lite/tier5-src/wix-spaces-create-group/index.ts +0 -29
- package/templates/community-club/tier5-src/wix-spaces-create-group/index.ts +0 -29
- package/templates/events-meetups/tier5-src/wix-spaces-create-group/index.ts +0 -29
- package/templates/nonprofit-cause/tier5-src/wix-spaces-create-group/index.ts +0 -29
|
@@ -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
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { fetchMemberBookings, type MemberBookingRow } from "../_shared/member-bookings.js";
|
|
2
2
|
import { memberAccess, assertMemberAccess } from "../_shared/member-access.js";
|
|
3
|
-
import { defineService } from "../_shared/home-service.js";
|
|
3
|
+
import { defineService as registerHome } from "../_shared/home-service.js";
|
|
4
|
+
import { withIapPresentation } from "../_shared/iap-presentation.js";
|
|
4
5
|
import { queryServices, type WixService } from "@himalaya/wix-bookings";
|
|
5
6
|
import { getPost, queryPosts, type WixPost } from "@himalaya/wix-blog";
|
|
6
7
|
import { queryGroups, type WixGroup } from "@himalaya/wix-groups";
|
|
@@ -68,6 +69,7 @@ interface ProductFeature {
|
|
|
68
69
|
/** Device-local catalog saves are useful to visitors too; they are never
|
|
69
70
|
* presented as account-synced data or mixed with member-only bookings. */
|
|
70
71
|
interface SavedProductCard {
|
|
72
|
+
commercePrice: string;
|
|
71
73
|
id: string;
|
|
72
74
|
name: string;
|
|
73
75
|
price: string;
|
|
@@ -573,7 +575,7 @@ function postCard(post: WixPost): PostCard {
|
|
|
573
575
|
function savedProductCard(product: SavedProduct): SavedProductCard {
|
|
574
576
|
const name = cleanTitle(product.name) || copyParam("home.savedProductFallbackName", "Saved item");
|
|
575
577
|
const image = product.image ? wixMediaUrl(product.image) : "";
|
|
576
|
-
return { id: product.id, name, price: product.price, image, initial: initials(name), hasImage: !!image, noImage: !image, detailScreenId: "himi_product" };
|
|
578
|
+
return { id: product.id, name, price: product.price, commercePrice: product.price, image, initial: initials(name), hasImage: !!image, noImage: !image, detailScreenId: "himi_product" };
|
|
577
579
|
}
|
|
578
580
|
|
|
579
581
|
function savedPostCard(post: WixPost): SavedPostCard {
|
|
@@ -1111,6 +1113,18 @@ async function refresh(state: State, arg?: unknown): Promise<State | undefined>
|
|
|
1111
1113
|
weeklyClassCount: current.weeklyClassCount, weeklyClassGoalFraction: current.weeklyClassGoalFraction, weeklyGoalLabel: current.weeklyGoalLabel });
|
|
1112
1114
|
}
|
|
1113
1115
|
|
|
1116
|
+
const defineService = withIapPresentation<State>({
|
|
1117
|
+
register: registerHome, prefix: "home", transientKeys: ["featuredProducts", "savedProducts", "heroPrice"], contentActions: ["refresh"],
|
|
1118
|
+
items: state => [
|
|
1119
|
+
...state.featuredProducts.map(row => ({ reference: { kind: "storesProduct", id: row.id }, commerceDisplayPrice: row.price })),
|
|
1120
|
+
...state.savedProducts.map(row => ({ reference: { kind: "storesProduct", id: row.id }, commerceDisplayPrice: row.commercePrice })),
|
|
1121
|
+
],
|
|
1122
|
+
present: (state, price) => derive({ ...state,
|
|
1123
|
+
featuredProducts: state.featuredProducts.map(row => ({ ...row, price: price(row.id) })),
|
|
1124
|
+
savedProducts: state.savedProducts.map(row => ({ ...row, price: price(row.id, row.commercePrice) })),
|
|
1125
|
+
}),
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1114
1128
|
const svc = defineService<State, {
|
|
1115
1129
|
// `State | undefined`, matching the implementation: `refresh` returns undefined on a
|
|
1116
1130
|
// STALE generation (`if (gen !== refreshGen) return undefined`) so the runtime keeps
|
|
@@ -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","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",
|
|
@@ -1468,7 +1468,9 @@ const SCREENS = runtimeSetting({
|
|
|
1468
1468
|
"integrity": "",
|
|
1469
1469
|
"contentType": "application/javascript+ts-bundle",
|
|
1470
1470
|
"declaredCapabilities": [
|
|
1471
|
-
"wix_api"
|
|
1471
|
+
"wix_api",
|
|
1472
|
+
"iap",
|
|
1473
|
+
"app_info"
|
|
1472
1474
|
],
|
|
1473
1475
|
"minBinaryVersion": "0.2.0",
|
|
1474
1476
|
"bundleVersion": "0.0.1"
|
|
@@ -6691,7 +6693,9 @@ const SCREENS = runtimeSetting({
|
|
|
6691
6693
|
"contentType": "application/javascript+ts-bundle",
|
|
6692
6694
|
"declaredCapabilities": [
|
|
6693
6695
|
"wix_api",
|
|
6694
|
-
"local_database"
|
|
6696
|
+
"local_database",
|
|
6697
|
+
"iap",
|
|
6698
|
+
"app_info"
|
|
6695
6699
|
],
|
|
6696
6700
|
"minBinaryVersion": "0.1.0",
|
|
6697
6701
|
"bundleVersion": "0.0.1"
|
|
@@ -7159,7 +7163,11 @@ const SCREENS = runtimeSetting({
|
|
|
7159
7163
|
"url": "http://localhost:8190/v1/bundles/himi-store-saved",
|
|
7160
7164
|
"integrity": "",
|
|
7161
7165
|
"contentType": "application/javascript+ts-bundle",
|
|
7162
|
-
"declaredCapabilities": [
|
|
7166
|
+
"declaredCapabilities": [
|
|
7167
|
+
"wix_api",
|
|
7168
|
+
"iap",
|
|
7169
|
+
"app_info"
|
|
7170
|
+
],
|
|
7163
7171
|
"minBinaryVersion": "0.1.0",
|
|
7164
7172
|
"bundleVersion": "0.0.1"
|
|
7165
7173
|
}
|
|
@@ -8685,7 +8693,9 @@ const SCREENS = runtimeSetting({
|
|
|
8685
8693
|
"wix_cashier",
|
|
8686
8694
|
"open_url",
|
|
8687
8695
|
"local_database",
|
|
8688
|
-
"wix_platform"
|
|
8696
|
+
"wix_platform",
|
|
8697
|
+
"iap",
|
|
8698
|
+
"app_info"
|
|
8689
8699
|
],
|
|
8690
8700
|
"minBinaryVersion": "0.1.0",
|
|
8691
8701
|
"bundleVersion": "0.0.1"
|
|
@@ -15481,7 +15491,9 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
15481
15491
|
"declaredCapabilities": [
|
|
15482
15492
|
"wix_api",
|
|
15483
15493
|
"local_database",
|
|
15484
|
-
"live_activity"
|
|
15494
|
+
"live_activity",
|
|
15495
|
+
"iap",
|
|
15496
|
+
"app_info"
|
|
15485
15497
|
],
|
|
15486
15498
|
"minBinaryVersion": "0.1.0",
|
|
15487
15499
|
"bundleVersion": "0.0.1"
|
|
@@ -16363,7 +16375,9 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
16363
16375
|
"wix_api",
|
|
16364
16376
|
"share",
|
|
16365
16377
|
"local_database",
|
|
16366
|
-
"open_url"
|
|
16378
|
+
"open_url",
|
|
16379
|
+
"iap",
|
|
16380
|
+
"app_info"
|
|
16367
16381
|
],
|
|
16368
16382
|
"minBinaryVersion": "0.1.0",
|
|
16369
16383
|
"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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getStoreProduct } from "../_shared/himi-store.js";
|
|
2
|
+
import { iapPriceCopy } from "../_shared/iap-worker.js";
|
|
3
|
+
import { withIapPresentation } from "../_shared/iap-presentation.js";
|
|
4
|
+
import { type IapCommerce, classifyCart } from "@himalaya/iap-checkout";
|
|
2
5
|
import { createCheckoutFromCurrentCart, createOrder, getCurrentCart, removeLineItemsFromCurrentCart, updateLineItemsQuantity, type WixCartLineItemDetail } from "@himalaya/wix-ecom";
|
|
3
6
|
import { createRedirectSession } from "@himalaya/wix-redirects";
|
|
4
7
|
import { session as wixPlatformSession } from "@himalaya/wix-platform";
|
|
@@ -6,6 +9,7 @@ import { payCashierOrder } from "@himalaya/checkout";
|
|
|
6
9
|
|
|
7
10
|
/** Wix Stores appDefId — the vertical that owns a cart/ecom order, and therefore the
|
|
8
11
|
* `appId` its cashier order is paid under. (Same constant Wix's own tooling uses.) */
|
|
12
|
+
const WIX_STORES_CATALOG_ID = "215238eb-22a5-4c36-9e7b-e7c08025e04e";
|
|
9
13
|
const WIX_STORES_APP_ID = "1380b703-ce81-ff05-f115-39571d94dfcd";
|
|
10
14
|
|
|
11
15
|
/** The site's metaSiteId for the cashier payment config. Best-effort — the native
|
|
@@ -40,6 +44,7 @@ async function cacheDrop(id: string): Promise<void> { try { await dbRemove(ENTIT
|
|
|
40
44
|
async function queueWrite(actionId: string, params: Record<string, string>): Promise<void> { try { await enqueue(actionId, params); } catch { /* capability not granted */ } }
|
|
41
45
|
|
|
42
46
|
interface Row {
|
|
47
|
+
catalogReference?: { appId: string; catalogItemId: string; options?: Record<string, unknown> };
|
|
43
48
|
id: string;
|
|
44
49
|
name: string;
|
|
45
50
|
title: string;
|
|
@@ -50,6 +55,7 @@ interface Row {
|
|
|
50
55
|
/** The "−" control at qty 1 removes the line, so it never dead-ends. */
|
|
51
56
|
atMinQuantity: boolean;
|
|
52
57
|
total: string;
|
|
58
|
+
commerceTotal: string;
|
|
53
59
|
image: string;
|
|
54
60
|
initial: string;
|
|
55
61
|
hasImage: boolean;
|
|
@@ -59,6 +65,7 @@ interface State {
|
|
|
59
65
|
status: "loading" | "ready" | "error";
|
|
60
66
|
items: Row[];
|
|
61
67
|
subtotal: string;
|
|
68
|
+
commerceSubtotal?: string;
|
|
62
69
|
emptyText: string;
|
|
63
70
|
checkoutHint: string;
|
|
64
71
|
isLoading: boolean;
|
|
@@ -118,6 +125,7 @@ function row(item: WixCartLineItemDetail, index: number): Row {
|
|
|
118
125
|
const name = item.productName?.original || copyParam("cart.itemFallbackName", "Cart item");
|
|
119
126
|
const image = mediaUrl(item);
|
|
120
127
|
return {
|
|
128
|
+
catalogReference: (item as WixCartLineItemDetail & { catalogReference?: Row["catalogReference"] }).catalogReference,
|
|
121
129
|
id: item.id || `line-${index}`,
|
|
122
130
|
name,
|
|
123
131
|
title: name,
|
|
@@ -126,6 +134,7 @@ function row(item: WixCartLineItemDetail, index: number): Row {
|
|
|
126
134
|
quantityLabel: `${quantity}`,
|
|
127
135
|
atMinQuantity: quantity <= 1,
|
|
128
136
|
total: item.price?.formattedAmount ?? "",
|
|
137
|
+
commerceTotal: item.price?.formattedAmount ?? "",
|
|
129
138
|
image,
|
|
130
139
|
initial: (name.trim()[0] || "C").toUpperCase(),
|
|
131
140
|
hasImage: Boolean(image),
|
|
@@ -163,6 +172,7 @@ async function refresh(state: State, checkoutHint = ""): Promise<State> {
|
|
|
163
172
|
status: "ready",
|
|
164
173
|
items,
|
|
165
174
|
subtotal: cart.subtotal?.formattedAmount ?? "",
|
|
175
|
+
commerceSubtotal: cart.subtotal?.formattedAmount ?? "",
|
|
166
176
|
emptyText: items.length ? "" : copyParam("cart.emptyText", "Your cart is empty."),
|
|
167
177
|
checkoutHint,
|
|
168
178
|
});
|
|
@@ -190,6 +200,16 @@ function cartConsumedByOrder(): Pick<State, "items" | "subtotal" | "emptyText" |
|
|
|
190
200
|
};
|
|
191
201
|
}
|
|
192
202
|
|
|
203
|
+
let cartPricing: IapCommerce;
|
|
204
|
+
const defineService = withIapPresentation<State>({
|
|
205
|
+
onCreate: commerce => { cartPricing = commerce; },
|
|
206
|
+
prefix: "cart", transientKeys: ["items", "subtotal"], contentActions: ["load", "remove", "increment", "decrement"],
|
|
207
|
+
items: state => state.items.flatMap(row => row.catalogReference?.catalogItemId ? [{ reference: { kind: "storesProduct", id: row.catalogReference.catalogItemId }, commerceDisplayPrice: row.commerceTotal ?? row.total }] : []),
|
|
208
|
+
present: (state, price, allowed) => ({ ...state,
|
|
209
|
+
items: state.items.map(row => ({ ...row, total: row.catalogReference?.catalogItemId ? price(row.catalogReference.catalogItemId, row.commerceTotal ?? row.total) : row.total })),
|
|
210
|
+
subtotal: state.items.length > 0 && state.items.every(row => row.catalogReference?.catalogItemId && allowed(row.catalogReference.catalogItemId)) ? state.commerceSubtotal ?? state.subtotal : "" }),
|
|
211
|
+
});
|
|
212
|
+
|
|
193
213
|
const svc = defineService<State, {
|
|
194
214
|
load: (state: State, arg?: unknown) => Promise<State>;
|
|
195
215
|
remove: (state: State, arg?: unknown) => Promise<State>;
|
|
@@ -272,6 +292,25 @@ const svc = defineService<State, {
|
|
|
272
292
|
// `createOrder` returning no paymentGatewayOrderId means there is nothing to
|
|
273
293
|
// charge (fully covered / zero total) — the order already stands.
|
|
274
294
|
checkout: async (state) => {
|
|
295
|
+
let validatedLines: WixCartLineItemDetail[] = [];
|
|
296
|
+
const signature = (lines: WixCartLineItemDetail[]) => JSON.stringify(lines.map(line => [line.id, line.quantity, (line as WixCartLineItemDetail & { catalogReference?: Row["catalogReference"] }).catalogReference]));
|
|
297
|
+
try {
|
|
298
|
+
const cart = await getCurrentCart();
|
|
299
|
+
const lines = cart.lineItems ?? [];
|
|
300
|
+
if (!lines.length) return derive({ ...state, checkoutHint: copyParam("cart.checkoutEmptyMessage", "Add an item before checkout.") });
|
|
301
|
+
const commerce = cartPricing;
|
|
302
|
+
const items = lines.map(line => (line as WixCartLineItemDetail & { catalogReference?: Row["catalogReference"] }).catalogReference);
|
|
303
|
+
if (items.some(item => !item?.catalogItemId || !item.appId)) return derive({ ...state, checkoutHint: iapPriceCopy("cart").purchaseUnavailable });
|
|
304
|
+
await commerce.loadPrices(items.map((item, index) => ({ reference: { kind: "storesProduct", id: item!.catalogItemId }, commerceDisplayPrice: lines[index].price?.formattedAmount })));
|
|
305
|
+
const classification = classifyCart(items.map((item, index) => ({ route: commerce.pricing.view({ kind: "storesProduct", id: item!.catalogItemId }).route, quantity: lines[index].quantity ?? 1 })));
|
|
306
|
+
if (classification !== "regular") return derive({ ...state, checkoutHint: classification === "mixedRefused" ? copyParam("cart.iapSingleItemOnly", "Buy digital items individually, with quantity 1 and no options.") : iapPriceCopy("cart").purchaseUnavailable });
|
|
307
|
+
const products = await Promise.all(items.map(async item => item!.appId === WIX_STORES_CATALOG_ID ? (await getStoreProduct(item!.catalogItemId)).product : undefined));
|
|
308
|
+
if (products.some(product => product?.productType?.toLowerCase() !== "physical")) return derive({ ...state, checkoutHint: iapPriceCopy("cart").purchaseUnavailable });
|
|
309
|
+
const latest = await getCurrentCart();
|
|
310
|
+
if (signature(latest.lineItems ?? []) !== signature(lines)) return derive({ ...state, checkoutHint: copyParam("cart.changedBeforeCheckout", "Your cart changed. Review it before checking out.") });
|
|
311
|
+
validatedLines = lines;
|
|
312
|
+
} catch { return derive({ ...state, checkoutHint: iapPriceCopy("cart").purchaseUnavailable }); }
|
|
313
|
+
|
|
275
314
|
if (!state.items.length) return derive({ ...state, checkoutHint: copyParam("cart.checkoutEmptyMessage", "Add an item before checkout.") });
|
|
276
315
|
try {
|
|
277
316
|
const checkout = await createCheckoutFromCurrentCart();
|
|
@@ -288,6 +327,14 @@ const svc = defineService<State, {
|
|
|
288
327
|
appId: WIX_STORES_APP_ID,
|
|
289
328
|
snapshotId,
|
|
290
329
|
msid: await resolveMsid(),
|
|
330
|
+
mayCharge: async () => {
|
|
331
|
+
const latest = await getCurrentCart();
|
|
332
|
+
if (signature(latest.lineItems ?? []) !== signature(validatedLines)) return false;
|
|
333
|
+
return validatedLines.every(line => {
|
|
334
|
+
const reference = (line as WixCartLineItemDetail & { catalogReference?: Row["catalogReference"] }).catalogReference;
|
|
335
|
+
return !!reference && cartPricing.pricing.continueCheckoutGuard({ kind: "storesProduct", id: reference.catalogItemId }).allowed;
|
|
336
|
+
});
|
|
337
|
+
},
|
|
291
338
|
cashierUrl: async () => {
|
|
292
339
|
const redirect = await createRedirectSession({
|
|
293
340
|
ecomCheckout: { checkoutId },
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { fetchMemberBookings, type MemberBookingRow } from "../_shared/member-bookings.js";
|
|
2
2
|
import { memberAccess, assertMemberAccess } from "../_shared/member-access.js";
|
|
3
|
-
import { defineService } from "../_shared/home-service.js";
|
|
3
|
+
import { defineService as registerHome } from "../_shared/home-service.js";
|
|
4
|
+
import { withIapPresentation } from "../_shared/iap-presentation.js";
|
|
4
5
|
import { queryServices, type WixService } from "@himalaya/wix-bookings";
|
|
5
6
|
import { getPost, queryPosts, type WixPost } from "@himalaya/wix-blog";
|
|
6
7
|
import { queryGroups, type WixGroup } from "@himalaya/wix-groups";
|
|
@@ -68,6 +69,7 @@ interface ProductFeature {
|
|
|
68
69
|
/** Device-local catalog saves are useful to visitors too; they are never
|
|
69
70
|
* presented as account-synced data or mixed with member-only bookings. */
|
|
70
71
|
interface SavedProductCard {
|
|
72
|
+
commercePrice: string;
|
|
71
73
|
id: string;
|
|
72
74
|
name: string;
|
|
73
75
|
price: string;
|
|
@@ -573,7 +575,7 @@ function postCard(post: WixPost): PostCard {
|
|
|
573
575
|
function savedProductCard(product: SavedProduct): SavedProductCard {
|
|
574
576
|
const name = cleanTitle(product.name) || copyParam("home.savedProductFallbackName", "Saved item");
|
|
575
577
|
const image = product.image ? wixMediaUrl(product.image) : "";
|
|
576
|
-
return { id: product.id, name, price: product.price, image, initial: initials(name), hasImage: !!image, noImage: !image, detailScreenId: "himi_product" };
|
|
578
|
+
return { id: product.id, name, price: product.price, commercePrice: product.price, image, initial: initials(name), hasImage: !!image, noImage: !image, detailScreenId: "himi_product" };
|
|
577
579
|
}
|
|
578
580
|
|
|
579
581
|
function savedPostCard(post: WixPost): SavedPostCard {
|
|
@@ -1111,6 +1113,18 @@ async function refresh(state: State, arg?: unknown): Promise<State | undefined>
|
|
|
1111
1113
|
weeklyClassCount: current.weeklyClassCount, weeklyClassGoalFraction: current.weeklyClassGoalFraction, weeklyGoalLabel: current.weeklyGoalLabel });
|
|
1112
1114
|
}
|
|
1113
1115
|
|
|
1116
|
+
const defineService = withIapPresentation<State>({
|
|
1117
|
+
register: registerHome, prefix: "home", transientKeys: ["featuredProducts", "savedProducts", "heroPrice"], contentActions: ["refresh"],
|
|
1118
|
+
items: state => [
|
|
1119
|
+
...state.featuredProducts.map(row => ({ reference: { kind: "storesProduct", id: row.id }, commerceDisplayPrice: row.price })),
|
|
1120
|
+
...state.savedProducts.map(row => ({ reference: { kind: "storesProduct", id: row.id }, commerceDisplayPrice: row.commercePrice })),
|
|
1121
|
+
],
|
|
1122
|
+
present: (state, price) => derive({ ...state,
|
|
1123
|
+
featuredProducts: state.featuredProducts.map(row => ({ ...row, price: price(row.id) })),
|
|
1124
|
+
savedProducts: state.savedProducts.map(row => ({ ...row, price: price(row.id, row.commercePrice) })),
|
|
1125
|
+
}),
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1114
1128
|
const svc = defineService<State, {
|
|
1115
1129
|
// `State | undefined`, matching the implementation: `refresh` returns undefined on a
|
|
1116
1130
|
// STALE generation (`if (gen !== refreshGen) return undefined`) so the runtime keeps
|