@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
|
@@ -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
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { createIapCommerce, classifyCart } from "@himalaya/iap-checkout";
|
|
2
|
+
import { getStoreProduct } from "../_shared/himi-store.js";
|
|
3
|
+
import { iapPriceCopy } from "../_shared/iap-worker.js";
|
|
1
4
|
import { defineService } from "@himalaya/state";
|
|
2
5
|
import { orderMember, assertOrderMember, ownedOrderCache, orderAccessDenied, orderAccessChanged, type OrderMember } from "../_shared/member-orders.js";
|
|
3
6
|
import { addToCurrentCart, getOrder, searchOrders, type WixOrder } from "@himalaya/wix-ecom";
|
|
@@ -38,6 +41,8 @@ interface LineRow {
|
|
|
38
41
|
|
|
39
42
|
/** What reorder re-adds: catalog item id + quantity per line (internal). */
|
|
40
43
|
interface ReorderRef {
|
|
44
|
+
appId?: string;
|
|
45
|
+
options?: Record<string, unknown>;
|
|
41
46
|
catalogItemId: string;
|
|
42
47
|
quantity: number;
|
|
43
48
|
}
|
|
@@ -164,10 +169,10 @@ function lineRows(order: WixOrder): LineRow[] {
|
|
|
164
169
|
/** Catalog references for the reorder CTA — only store-catalog lines re-add. */
|
|
165
170
|
function reorderRefs(order: WixOrder): ReorderRef[] {
|
|
166
171
|
return (order.lineItems ?? []).flatMap((item) => {
|
|
167
|
-
const ref = (item as { catalogReference?: { catalogItemId?: string } }).catalogReference;
|
|
172
|
+
const ref = (item as { catalogReference?: { catalogItemId?: string; appId?: string; options?: Record<string, unknown> } }).catalogReference;
|
|
168
173
|
const catalogItemId = ref?.catalogItemId;
|
|
169
174
|
return typeof catalogItemId === "string" && catalogItemId
|
|
170
|
-
? [{ catalogItemId, quantity: item.quantity || 1 }]
|
|
175
|
+
? [{ catalogItemId, appId: ref.appId, ...(ref.options ? { options: ref.options } : {}), quantity: item.quantity || 1 }]
|
|
171
176
|
: [];
|
|
172
177
|
});
|
|
173
178
|
}
|
|
@@ -307,6 +312,7 @@ async function load(state: State, arg?: unknown): Promise<State | undefined> {
|
|
|
307
312
|
*/
|
|
308
313
|
const FOLLOW_TTL_SECONDS = 24 * 60 * 60;
|
|
309
314
|
|
|
315
|
+
const reorderPricing = createIapCommerce();
|
|
310
316
|
const svc = defineService<State, {
|
|
311
317
|
load: (state: State, arg?: unknown) => Promise<State | undefined>;
|
|
312
318
|
retry: (state: State) => Promise<State | undefined>;
|
|
@@ -401,8 +407,23 @@ const svc = defineService<State, {
|
|
|
401
407
|
catch { return version === loadVersion ? derive({ ...clearOrder(state), status: "error", emptyText: copyParam("order.signInEmptyText", "Sign in to view this order.") }) : undefined; }
|
|
402
408
|
if (version !== loadVersion) return undefined;
|
|
403
409
|
if (!state.reorderRefs.length) return derive({ ...state, reorderMessage: copyParam("order.reorderNoneMessage", "Nothing on this order can be re-added.") });
|
|
410
|
+
try {
|
|
411
|
+
const refs = state.reorderRefs;
|
|
412
|
+
if (refs.some(ref => ref.appId !== "215238eb-22a5-4c36-9e7b-e7c08025e04e")) return derive({ ...state, reorderMessage: iapPriceCopy("order").purchaseUnavailable });
|
|
413
|
+
await reorderPricing.loadPrices(refs.map(ref => ({ reference: { kind: "storesProduct", id: ref.catalogItemId } })));
|
|
414
|
+
const route = classifyCart(refs.map(ref => ({ route: reorderPricing.pricing.view({ kind: "storesProduct", id: ref.catalogItemId }).route, quantity: ref.quantity })));
|
|
415
|
+
const products = await Promise.all(refs.map(async ref => (await getStoreProduct(ref.catalogItemId)).product));
|
|
416
|
+
if (route !== "regular" || products.some(product => product?.productType?.toLowerCase() !== "physical")) return derive({ ...state, reorderMessage: iapPriceCopy("order").purchaseUnavailable });
|
|
417
|
+
await assertOrderMember(member);
|
|
418
|
+
if (version !== loadVersion) return undefined;
|
|
419
|
+
} catch {
|
|
420
|
+
if (version !== loadVersion) return undefined;
|
|
421
|
+
try { await assertOrderMember(member); }
|
|
422
|
+
catch { return derive({ ...clearOrder(state), status: "error", emptyText: copyParam("order.signInEmptyText", "Sign in to view this order.") }); }
|
|
423
|
+
return derive({ ...state, reorderMessage: iapPriceCopy("order").purchaseUnavailable });
|
|
424
|
+
}
|
|
404
425
|
const results = await Promise.allSettled(
|
|
405
|
-
state.reorderRefs.map((ref) => addToCurrentCart(ref.catalogItemId, ref.quantity)),
|
|
426
|
+
state.reorderRefs.map((ref) => addToCurrentCart(ref.catalogItemId, ref.quantity, ref.options)),
|
|
406
427
|
);
|
|
407
428
|
// A pending mutation must never restore an older order after navigation or sign-out.
|
|
408
429
|
if (version !== loadVersion) return undefined;
|
|
@@ -1,21 +1,13 @@
|
|
|
1
|
-
// Tier-5 bundle: himi-branded-lite — public pricing plans (memberships).
|
|
2
|
-
//
|
|
3
|
-
// Visitors can browse the site's public pricing plans (design §3.5 `plans`
|
|
4
|
-
// vertical). The public GA route works on the anonymous visitor rung, so the
|
|
5
|
-
// list paints with no login. Purchasing is NATIVE (Phase 2b): "Join" creates the
|
|
6
|
-
// member's plan order via createOnlineOrder, then settles any balance on the shared
|
|
7
|
-
// in-app pay rail (@wix/himalaya/checkout) — wallet where the app has one, in-app Wix
|
|
8
|
-
// cashier otherwise. A FREE / fully-discounted plan activates with no payment step at
|
|
9
|
-
// all. A plan-carried purchasable URL still wins when present, and any failure degrades
|
|
10
|
-
// gracefully to the site-directing message rather than a dead button.
|
|
11
|
-
// Four-state (loading / error / rows / empty) like the sibling branded workers.
|
|
12
1
|
import { defineService } from "../_shared/collection-service.js";
|
|
13
2
|
import { createOnlineOrder, listPublicPlans, type WixPlan } from "@himalaya/wix-pricing-plans";
|
|
14
3
|
import { createRedirectSession } from "@himalaya/wix-redirects";
|
|
15
4
|
import { open as openUrl } from "@himalaya/open-url";
|
|
16
5
|
import { session as wixPlatformSession } from "@himalaya/wix-platform";
|
|
17
6
|
import { copyParam, copyTemplate, stashCopy } from "../_shared/himi-copy.js";
|
|
18
|
-
import { payCashierOrder } from "@himalaya/checkout";
|
|
7
|
+
import { activePlanIds, entitlement, payCashierOrder } from "@himalaya/checkout";
|
|
8
|
+
import { hostSessionFromGlobal, iapPriceFields, type IapCatalogItem } from "@himalaya/iap";
|
|
9
|
+
import { createIapCommerce, pricingPlanOrders } from "@himalaya/iap-checkout";
|
|
10
|
+
import { iapPriceCopy, iapPurchaseCopy, resetUnhydratedKeysOnOlderRuntime } from "../_shared/iap-worker.js";
|
|
19
11
|
import { formatCurrency } from "../_shared/engine-intl.js";
|
|
20
12
|
|
|
21
13
|
/** Wix Pricing Plans appDefId — the vertical that owns a plan order, so the `appId` its
|
|
@@ -57,6 +49,11 @@ interface PlanRow {
|
|
|
57
49
|
hasBadge: boolean;
|
|
58
50
|
/** Purchasable URL when the plan data carries one; "" otherwise. */
|
|
59
51
|
joinUrl: string;
|
|
52
|
+
joined: boolean;
|
|
53
|
+
appleSupported: boolean;
|
|
54
|
+
joinLabel: string;
|
|
55
|
+
joinDisabled: boolean;
|
|
56
|
+
joinNote: string;
|
|
60
57
|
}
|
|
61
58
|
|
|
62
59
|
interface State {
|
|
@@ -130,7 +127,7 @@ function planRow(plan: WixPlan): PlanRow {
|
|
|
130
127
|
const trialDays = plan.pricing?.freeTrialDays ?? 0;
|
|
131
128
|
const trialBadge = trialDays > 0 ? copyTemplate("plans.trialBadge", "{days}-day free trial", { days: trialDays }) : "";
|
|
132
129
|
return {
|
|
133
|
-
id: plan.id ||
|
|
130
|
+
id: plan.id || "",
|
|
134
131
|
name,
|
|
135
132
|
description: (plan.description ?? "").trim(),
|
|
136
133
|
trialBadge,
|
|
@@ -145,6 +142,8 @@ function planRow(plan: WixPlan): PlanRow {
|
|
|
145
142
|
noBadge: !badge,
|
|
146
143
|
hasBadge: !!badge,
|
|
147
144
|
joinUrl: joinUrl(plan),
|
|
145
|
+
appleSupported: plan.pricing?.singlePaymentUnlimited === true,
|
|
146
|
+
joined: false, joinLabel: copyParam("plans.joinCta", "Join"), joinDisabled: true, joinNote: "",
|
|
148
147
|
};
|
|
149
148
|
}
|
|
150
149
|
|
|
@@ -170,9 +169,97 @@ function idOf(arg: unknown): string {
|
|
|
170
169
|
return typeof value === "string" ? value : "";
|
|
171
170
|
}
|
|
172
171
|
|
|
172
|
+
const commerce = createIapCommerce({ adapters: [pricingPlanOrders], onChange: () => { void svc.pricingPublish(); } });
|
|
173
|
+
let rows: PlanRow[] = [];
|
|
174
|
+
let items: IapCatalogItem[] = [];
|
|
175
|
+
let message = "";
|
|
176
|
+
let started = false;
|
|
177
|
+
let loading = 0;
|
|
178
|
+
let contentReady = false;
|
|
179
|
+
const joining = new Set<string>();
|
|
180
|
+
const memberSession = hostSessionFromGlobal();
|
|
181
|
+
let regularGeneration = -1;
|
|
182
|
+
let regularAccess = new Set<string>();
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
function pricedState(state: State): State {
|
|
186
|
+
if (!contentReady) return state;
|
|
187
|
+
const priced = rows.map(row => {
|
|
188
|
+
const reference = { kind: "pricingPlan", id: row.id };
|
|
189
|
+
const view = commerce.pricing.view(reference);
|
|
190
|
+
const price = iapPriceFields(view, iapPriceCopy("plans"));
|
|
191
|
+
const purchase = commerce.purchaseFields(reference, iapPurchaseCopy("plans"));
|
|
192
|
+
const apple = purchase.appleRoute;
|
|
193
|
+
const regularReady = regularGeneration === memberSession.current().generation && (!memberSession.identityBound || memberSession.current().kind === "member");
|
|
194
|
+
const joined = apple ? purchase.accessReady : regularReady && regularAccess.has(row.id);
|
|
195
|
+
return { ...row, priceLine: view.route === "disabled" ? row.priceLine : price.priceText,
|
|
196
|
+
periodLine: apple ? price.priceNote : view.route === "regular" || view.route === "disabled" ? row.periodLine : "",
|
|
197
|
+
trialBadge: apple || price.actionDisabled ? "" : row.trialBadge,
|
|
198
|
+
noTrialBadge: apple || price.actionDisabled || row.noTrialBadge,
|
|
199
|
+
joined,
|
|
200
|
+
joinLabel: joined ? copyParam("plans.joinedLabel", "Current plan") : apple ? purchase.buyLabel : row.joinLabel,
|
|
201
|
+
joinDisabled: joined || (apple ? purchase.buyDisabled || !row.appleSupported : price.actionDisabled || !regularReady) || joining.has(row.id),
|
|
202
|
+
joinNote: apple ? row.appleSupported ? purchase.buyNote : iapPriceCopy("plans").purchaseUnavailable : price.actionNote };
|
|
203
|
+
});
|
|
204
|
+
const owned = priced.find(row => row.joined);
|
|
205
|
+
const poll = commerce.pollDirective("himiBrandedPlans.iapPoll");
|
|
206
|
+
return { ...state, ...ready(priced, owned ? copyTemplate("plans.joinedMessage", "You've joined {name}.", { name: owned.name }) : message), ...(poll ? { _poll: poll } : {}) };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function regularJoinMessage(row: PlanRow, reference: { kind: string; id: string }): Promise<string> {
|
|
210
|
+
const generation = memberSession.current().generation;
|
|
211
|
+
if (row.joinUrl) {
|
|
212
|
+
try {
|
|
213
|
+
const result = await openUrl(row.joinUrl, "in_app_browser");
|
|
214
|
+
if (result.opened) { return ""; }
|
|
215
|
+
} catch {
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
const created = await createOnlineOrder(row.id);
|
|
220
|
+
const snapshotId = created.purchaseFlowId;
|
|
221
|
+
if (generation !== memberSession.current().generation) return "";
|
|
222
|
+
if (created.order?.status === "ACTIVE") {
|
|
223
|
+
regularAccess.add(row.id);
|
|
224
|
+
return copyTemplate("plans.joinedMessage", "You've joined {name}.", { name: row.name });
|
|
225
|
+
}
|
|
226
|
+
const pay = await payCashierOrder({
|
|
227
|
+
appId: WIX_PRICING_PLANS_APP_ID,
|
|
228
|
+
snapshotId: snapshotId ?? "",
|
|
229
|
+
msid: await resolveMsid(),
|
|
230
|
+
mayCharge: () => commerce.pricing.continueCheckoutGuard(reference).allowed,
|
|
231
|
+
cashierUrl: async () => {
|
|
232
|
+
if (!commerce.pricing.continueCheckoutGuard(reference).allowed) return undefined;
|
|
233
|
+
const redirect = await createRedirectSession({
|
|
234
|
+
paidPlansCheckout: { planId: row.id },
|
|
235
|
+
callbacks: { postFlowUrl: "himalaya-himi-branded-lite://screen/himi_plans" },
|
|
236
|
+
});
|
|
237
|
+
return redirect.redirectSession?.fullUrl;
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
if (pay.kind === "paid") {
|
|
241
|
+
const access = await entitlement(row.id);
|
|
242
|
+
if (generation !== memberSession.current().generation) return "";
|
|
243
|
+
if (access.entitled) regularAccess.add(row.id);
|
|
244
|
+
return access.entitled
|
|
245
|
+
? copyTemplate("plans.joinedMessage", "You've joined {name}.", { name: row.name })
|
|
246
|
+
: copyParam("plans.purchaseProcessingText", "Processing your purchase…");
|
|
247
|
+
}
|
|
248
|
+
if (pay.kind === "handoff") { return ""; }
|
|
249
|
+
if (pay.kind === "cancelled") {
|
|
250
|
+
return copyParam("plans.joinCancelledMessage", "Payment cancelled — you haven't joined.");
|
|
251
|
+
}
|
|
252
|
+
} catch {
|
|
253
|
+
}
|
|
254
|
+
return copyTemplate("plans.joinOnSiteMessage", "Memberships are purchased on the website — visit the site to join {name}.", { name: row.name });
|
|
255
|
+
}
|
|
256
|
+
|
|
173
257
|
const svc = defineService<State, {
|
|
174
|
-
load: (state: State, arg?: unknown) => Promise<State>;
|
|
175
|
-
|
|
258
|
+
load: (state: State, arg?: unknown) => Promise<State | undefined>;
|
|
259
|
+
pricingPublish: (state: State) => State;
|
|
260
|
+
pricingRefresh: (state: State) => Promise<undefined>;
|
|
261
|
+
iapPoll: (state: State) => Promise<undefined>;
|
|
262
|
+
join: (state: State, arg?: unknown) => Promise<State | undefined>;
|
|
176
263
|
}>("himiBrandedPlans", {
|
|
177
264
|
state: {
|
|
178
265
|
status: "loading",
|
|
@@ -187,13 +274,38 @@ const svc = defineService<State, {
|
|
|
187
274
|
noRows: true,
|
|
188
275
|
noEmpty: true,
|
|
189
276
|
},
|
|
277
|
+
notHydrated: ["rows", "actionMessage"],
|
|
190
278
|
actions: {
|
|
279
|
+
pricingPublish: pricedState,
|
|
280
|
+
pricingRefresh: async () => { await commerce.loadPrices(items); return undefined; },
|
|
281
|
+
iapPoll: async () => { await commerce.poll(); void svc.pricingPublish(); return undefined; },
|
|
191
282
|
load: async (state, arg) => {
|
|
192
283
|
stashCopy(arg);
|
|
284
|
+
const seq = ++loading;
|
|
285
|
+
const generation = memberSession.current().generation;
|
|
286
|
+
regularGeneration = -1;
|
|
287
|
+
regularAccess = new Set();
|
|
193
288
|
try {
|
|
194
289
|
const response = await listPublicPlans();
|
|
195
|
-
|
|
290
|
+
if (seq !== loading) return undefined;
|
|
291
|
+
message = "";
|
|
292
|
+
contentReady = true;
|
|
293
|
+
rows = (response.plans ?? []).filter(plan => !plan.archived).map(planRow);
|
|
294
|
+
items = rows.map(row => ({ reference: { kind: "pricingPlan", id: row.id }, commerceDisplayPrice: row.priceLine }));
|
|
295
|
+
void svc.pricingPublish();
|
|
296
|
+
const access = rows.length > 0 && memberSession.current().kind === "member" ? await activePlanIds() : [];
|
|
297
|
+
if (seq !== loading || generation !== memberSession.current().generation) return undefined;
|
|
298
|
+
regularAccess = new Set(access);
|
|
299
|
+
regularGeneration = generation;
|
|
300
|
+
await commerce.loadPrices(items);
|
|
301
|
+
await (started ? commerce.reconcile() : commerce.start());
|
|
302
|
+
if (seq !== loading || generation !== memberSession.current().generation) return undefined;
|
|
303
|
+
started = true;
|
|
304
|
+
void svc.pricingPublish();
|
|
305
|
+
return undefined;
|
|
196
306
|
} catch {
|
|
307
|
+
if (seq !== loading || generation !== memberSession.current().generation) return undefined;
|
|
308
|
+
contentReady = false;
|
|
197
309
|
// Bridge errors (e.g. a 501 from a serve bridge without the route) land
|
|
198
310
|
// here — a graceful error card with retry, never a dead screen.
|
|
199
311
|
return {
|
|
@@ -209,57 +321,26 @@ const svc = defineService<State, {
|
|
|
209
321
|
},
|
|
210
322
|
join: async (state, arg) => {
|
|
211
323
|
const row = state.rows.find((candidate) => candidate.id === idOf(arg));
|
|
212
|
-
if (!row) return
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if (
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
// fall through to the redirect-session rail below
|
|
222
|
-
}
|
|
324
|
+
if (!row || joining.has(row.id)) return undefined;
|
|
325
|
+
const reference = { kind: "pricingPlan", id: row.id };
|
|
326
|
+
const purchase = commerce.purchaseFields(reference, iapPurchaseCopy("plans"));
|
|
327
|
+
if (purchase.accessReady) return undefined;
|
|
328
|
+
if (purchase.appleRoute) {
|
|
329
|
+
if (!row.appleSupported) return undefined;
|
|
330
|
+
await commerce.buy({ reference });
|
|
331
|
+
void svc.pricingPublish();
|
|
332
|
+
return undefined;
|
|
223
333
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
// the case the hosted redirect used to make needlessly heavy.
|
|
334
|
+
if (regularGeneration !== memberSession.current().generation || (memberSession.identityBound && memberSession.current().kind !== "member") || regularAccess.has(row.id) || !commerce.pricing.checkoutGuard(reference).allowed) return undefined;
|
|
335
|
+
joining.add(row.id);
|
|
336
|
+
void svc.pricingPublish();
|
|
228
337
|
try {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
235
|
-
const pay = await payCashierOrder({
|
|
236
|
-
appId: WIX_PRICING_PLANS_APP_ID,
|
|
237
|
-
snapshotId,
|
|
238
|
-
msid: await resolveMsid(),
|
|
239
|
-
// Wallet-less apps keep the hosted plans checkout, IN-APP — so a site with no
|
|
240
|
-
// provisioned wallet can still sell memberships (see @wix/himalaya/checkout).
|
|
241
|
-
cashierUrl: async () => {
|
|
242
|
-
const redirect = await createRedirectSession({
|
|
243
|
-
paidPlansCheckout: { planId: row.id },
|
|
244
|
-
callbacks: { postFlowUrl: "himalaya-himi-branded-lite://screen/himi_plans" },
|
|
245
|
-
});
|
|
246
|
-
return redirect.redirectSession?.fullUrl;
|
|
247
|
-
},
|
|
248
|
-
});
|
|
249
|
-
if (pay.kind === "paid") {
|
|
250
|
-
return { ...state, actionMessage: copyTemplate("plans.joinedMessage", "You've joined {name}.", { name: row.name }) };
|
|
251
|
-
}
|
|
252
|
-
if (pay.kind === "handoff") return { ...state, actionMessage: "" };
|
|
253
|
-
if (pay.kind === "cancelled") {
|
|
254
|
-
return { ...state, actionMessage: copyParam("plans.joinCancelledMessage", "Payment cancelled — you haven't joined.") };
|
|
255
|
-
}
|
|
256
|
-
} catch {
|
|
257
|
-
// Order/pay failure → graceful site-directing fallback below.
|
|
338
|
+
message = await regularJoinMessage(row, reference);
|
|
339
|
+
return undefined;
|
|
340
|
+
} finally {
|
|
341
|
+
joining.delete(row.id);
|
|
342
|
+
void svc.pricingPublish();
|
|
258
343
|
}
|
|
259
|
-
return {
|
|
260
|
-
...state,
|
|
261
|
-
actionMessage: copyTemplate("plans.joinOnSiteMessage", "Memberships are purchased on the website — visit the site to join {name}.", { name: row.name }),
|
|
262
|
-
};
|
|
263
344
|
},
|
|
264
345
|
},
|
|
265
346
|
});
|
|
@@ -270,5 +351,13 @@ const svc = defineService<State, {
|
|
|
270
351
|
// loading skeleton indefinitely on iOS. onAppear still fires afterwards and
|
|
271
352
|
// re-loads with the screen's copy pack; it passes no required params here
|
|
272
353
|
// (`appear(..., undefined, "<prefix>.")`), so a param-less first load is safe.
|
|
354
|
+
resetUnhydratedKeysOnOlderRuntime(svc, { ...svc.getState(), rows: [], actionMessage: "" }, ["rows", "actionMessage"]);
|
|
355
|
+
memberSession.subscribe(() => {
|
|
356
|
+
message = "";
|
|
357
|
+
regularGeneration = -1;
|
|
358
|
+
regularAccess = new Set();
|
|
359
|
+
void svc.pricingPublish();
|
|
360
|
+
void svc.load();
|
|
361
|
+
});
|
|
273
362
|
svc.load();
|
|
274
363
|
export { svc };
|