@wix/himalaya-cli 0.1320.0 → 0.1322.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/core/dev-server/src/app-config.d.ts +1 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/dev-server/src/app-module.d.ts +3 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/biometrics/src/index.d.ts +11 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/catalog-pricing-contract.d.ts +4 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/host-session.d.ts +4 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/index.d.ts +13 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/mapping-source.d.ts +8 -3
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/wix-payments.d.ts +5 -4
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/sdk-types/src/capabilities.generated.d.ts +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/webview/src/index.d.ts +4 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/webview-page/src/index.d.ts +16 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/tokens.d.ts +9 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/apple-iap-contract.d.ts +5 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/apple-iap-core.d.ts +3 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/checkout-source.d.ts +4 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/commerce-contract.d.ts +83 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/commerce-core.d.ts +36 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/commerce.d.ts +8 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/index.d.ts +3 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/purchase-fields.d.ts +4 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/stores-v1/src/display.d.ts +21 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/audio-record.mjs +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/calendar.mjs +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/calls.mjs +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/contacts.mjs +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap-checkout.mjs +1205 -71
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap.mjs +125 -13
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/location.mjs +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/motion.mjs +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/photos.mjs +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/sdk-types.mjs +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/sdui.mjs +1 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/tasks.mjs +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/webview-page.mjs +78 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/webview.mjs +16 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-stores.mjs +13 -4
- package/dist/cli.mjs +87 -23
- package/dist/preview-assets.generated.bin +0 -0
- package/guides/getting-started.md +4 -2
- package/guides/himi-skill.md +4 -2
- package/guides/iap-pricing.md +33 -26
- package/guides/index.json +2 -1
- package/guides/sdk-reference.json +50 -8
- package/guides/tokens.md +22 -1
- package/guides/web-page-native-sdk.md +154 -33
- package/guides/webview-page-sdk.md +11 -0
- package/package.json +2 -2
- package/templates/beauty-salon/dev/index.ts +492 -0
- package/templates/beauty-salon/tier5-src/wix-spaces-collection-detail/index.ts +146 -30
- package/templates/branded-lite/dev/index.ts +533 -0
- package/templates/branded-lite/tier5-src/_shared/iap-worker.ts +21 -44
- package/templates/branded-lite/tier5-src/himi-branded-lite-program/index.ts +77 -47
- package/templates/branded-lite/tier5-src/himi-branded-lite-programs/index.ts +24 -38
- package/templates/branded-lite/tier5-src/wix-spaces-collection-detail/index.ts +146 -30
- package/templates/gym-fitness/dev/index.ts +492 -0
- package/templates/gym-fitness/tier5-src/wix-spaces-collection-detail/index.ts +146 -30
- package/templates/portfolio-creative/dev/index.ts +492 -0
- package/templates/portfolio-creative/tier5-src/wix-spaces-collection-detail/index.ts +146 -30
- package/templates/retail-store/dev/index.ts +492 -0
- package/templates/retail-store/tier5-src/wix-spaces-collection-detail/index.ts +146 -30
- package/templates/yoga-studio/dev/index.ts +41 -0
- package/templates/yoga-studio/tier5-src/_shared/iap-worker.ts +21 -44
- package/templates/yoga-studio/tier5-src/himi-branded-lite-program/index.ts +77 -47
- package/templates/yoga-studio/tier5-src/himi-branded-lite-programs/index.ts +24 -38
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
// Tier-5 bundle: Wix Spaces clone — Collection detail (ON-DEVICE, site-scoped).
|
|
2
2
|
// Prefer Catalog V3 `getStoreCategory`; V1 `getCollection` 428s on CATALOG_V3 sites.
|
|
3
|
+
//
|
|
4
|
+
// #4155 — the screen showed the category header ("N products") and never the products.
|
|
5
|
+
// Once the category resolves, its products load through `searchStoreProducts({ categoryId })`
|
|
6
|
+
// with the same row shape as `himi-store-shop`, so a category reads as a filtered Shop.
|
|
7
|
+
//
|
|
8
|
+
// Two lifecycles, deliberately kept apart:
|
|
9
|
+
// • the CATEGORY — presence flags (loading / found / missing / failed), as every detail screen;
|
|
10
|
+
// • its PRODUCTS — a cursor-paged list (`createCollectionQuery`) with its own loading, failed
|
|
11
|
+
// and empty states. A product read that fails leaves a FOUND category found: the header
|
|
12
|
+
// stays and the list offers a retry. It never turns into "Category not found".
|
|
3
13
|
import { defineService } from "@himalaya/state";
|
|
4
14
|
import {
|
|
5
15
|
categoryCount,
|
|
@@ -8,46 +18,152 @@ import {
|
|
|
8
18
|
type WixCollection,
|
|
9
19
|
} from "@himalaya/wix-stores-collections";
|
|
10
20
|
import { presence, presenceAfterError, detailInputId, type PresenceFlags } from "../_shared/detail-presence.js";
|
|
21
|
+
import { createCollectionQuery, type CollectionQueryState } from "../_shared/collection-query.js";
|
|
22
|
+
import { productImage, productPrice, searchStoreProducts, type StoreProduct } from "../_shared/himi-store.js";
|
|
23
|
+
import { cleanTitle, initials } from "../_shared/himi-text.js";
|
|
11
24
|
|
|
12
|
-
interface
|
|
25
|
+
interface Row {
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
subtitle: string;
|
|
29
|
+
image: string;
|
|
30
|
+
initial: string;
|
|
31
|
+
hasImage: boolean;
|
|
32
|
+
noImage: boolean;
|
|
33
|
+
detailScreenId: string;
|
|
34
|
+
}
|
|
13
35
|
|
|
36
|
+
interface S extends PresenceFlags {
|
|
37
|
+
status: "loading" | "ready";
|
|
38
|
+
name: string;
|
|
39
|
+
subtitle: string;
|
|
40
|
+
description: string;
|
|
41
|
+
/** Which `load` the committed state belongs to — a products snapshot is only ever
|
|
42
|
+
* projected onto the category it was fetched for. */
|
|
43
|
+
loadId: number;
|
|
44
|
+
products: Row[];
|
|
45
|
+
/** Load-more cursor — non-empty while another page exists and none is in flight. */
|
|
46
|
+
productsCursor: string;
|
|
47
|
+
hideProducts: boolean;
|
|
48
|
+
hideProductsLoading: boolean;
|
|
49
|
+
/** Gates the products retry block (the category's own failure is `hideLoadError`). */
|
|
50
|
+
productsNotErrored: boolean;
|
|
51
|
+
/** Gates the "next page failed" retry under the list. A failed load-more keeps the rows
|
|
52
|
+
* and clears the cursor, so without this the list would just stop and read as complete. */
|
|
53
|
+
productsNoPageError: boolean;
|
|
54
|
+
/** One plain line for a screen that has no dedicated empty / error blocks ("" otherwise). */
|
|
55
|
+
productsMessage: string;
|
|
56
|
+
/** Gates the "no products in this category" block (screen-kit `emptyBlock` reads `noEmpty`). */
|
|
57
|
+
noEmpty: boolean;
|
|
58
|
+
}
|
|
14
59
|
|
|
15
|
-
|
|
16
|
-
|
|
60
|
+
/** The same row `himi-store-shop` emits, so the screen reuses the Shop tile. */
|
|
61
|
+
function row(p: StoreProduct): Row {
|
|
62
|
+
const name = cleanTitle(p.name) || "Product";
|
|
63
|
+
const image = productImage(p);
|
|
64
|
+
return {
|
|
65
|
+
id: p.id ?? "",
|
|
66
|
+
name,
|
|
67
|
+
subtitle: productPrice(p),
|
|
68
|
+
image,
|
|
69
|
+
initial: initials(name),
|
|
70
|
+
hasImage: !!image,
|
|
71
|
+
noImage: !image,
|
|
72
|
+
detailScreenId: "himi_product",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let categoryId = "";
|
|
77
|
+
let loadId = 0;
|
|
78
|
+
const products = createCollectionQuery<Row>({
|
|
79
|
+
id: (item) => item.id,
|
|
80
|
+
async fetch(_criteria, cursor) {
|
|
81
|
+
const response = await searchStoreProducts({ categoryId, limit: 50, cursor: cursor || undefined });
|
|
82
|
+
return {
|
|
83
|
+
items: (response.products ?? []).map(row).filter((item) => !!item.id),
|
|
84
|
+
nextCursor: response.pagingMetadata?.cursors?.next ?? "",
|
|
85
|
+
total: response.pagingMetadata?.total,
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
}, () => { void svc.productsChanged(); });
|
|
89
|
+
|
|
90
|
+
/** Project the products snapshot onto a state whose category is FOUND. Anything else
|
|
91
|
+
* (loading, missing, failed) shows no product UI at all. */
|
|
92
|
+
function withProducts(s: S, page: CollectionQueryState<Row> = products.getState()): S {
|
|
93
|
+
const found = s.found;
|
|
94
|
+
const items = found ? page.items : [];
|
|
95
|
+
const loading = page.status === "idle" || page.status === "loading";
|
|
96
|
+
return {
|
|
97
|
+
...s,
|
|
98
|
+
products: items,
|
|
99
|
+
productsCursor: found && !page.pageError && !page.loadingMore ? page.nextCursor : "",
|
|
100
|
+
hideProducts: items.length === 0,
|
|
101
|
+
hideProductsLoading: !(found && loading && items.length === 0),
|
|
102
|
+
productsNotErrored: !(found && page.status === "error"),
|
|
103
|
+
productsNoPageError: !(found && page.pageError),
|
|
104
|
+
noEmpty: !(found && page.status === "ready" && items.length === 0),
|
|
105
|
+
productsMessage: !found ? ""
|
|
106
|
+
: page.status === "error" || page.pageError ? "Products couldn't load. Pull to refresh to try again."
|
|
107
|
+
: page.status === "ready" && items.length === 0 ? "No products in this category yet." : "",
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const NO_PRODUCTS = { products: [], productsCursor: "", hideProducts: true, hideProductsLoading: true,
|
|
112
|
+
productsNotErrored: true, productsNoPageError: true, noEmpty: true, productsMessage: "" };
|
|
113
|
+
|
|
114
|
+
const initial: S = { status: "loading", name: "", subtitle: "", description: "", loadId: 0, ...NO_PRODUCTS, ...presence("loading") };
|
|
115
|
+
|
|
116
|
+
/** The category header for `id`: V3 category first, V1 collection as the fallback. */
|
|
117
|
+
async function readCategory(id: string): Promise<Pick<S, "name" | "subtitle" | "description">> {
|
|
118
|
+
try {
|
|
119
|
+
const c = (await getStoreCategory(id)).category;
|
|
120
|
+
if (c?.id || c?.name) return { name: c.name || "Category", subtitle: categoryCount(c), description: c.description || "" };
|
|
121
|
+
} catch { /* fall through to V1 */ }
|
|
122
|
+
const res = await getCollection(id);
|
|
123
|
+
const c = (((res as unknown as { collection?: WixCollection }).collection) ?? (res as unknown as WixCollection)) as unknown as {
|
|
124
|
+
name?: string; numberOfProducts?: number; description?: string;
|
|
125
|
+
};
|
|
126
|
+
const n = c.numberOfProducts;
|
|
127
|
+
return {
|
|
128
|
+
name: c.name || "Collection",
|
|
129
|
+
subtitle: n != null ? `${n} ${n === 1 ? "product" : "products"}` : "",
|
|
130
|
+
description: c.description || "",
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const svc = defineService<S, {
|
|
135
|
+
load: (s: S) => Promise<S | undefined>;
|
|
136
|
+
productsChanged: (s: S) => S | undefined;
|
|
137
|
+
loadMoreProducts: (s: S) => Promise<undefined>;
|
|
138
|
+
retryProducts: (s: S) => Promise<undefined>;
|
|
139
|
+
}>("spacesCollectionDetail", {
|
|
140
|
+
state: initial,
|
|
141
|
+
// A cold boot must not paint another category's products from last run's snapshot.
|
|
142
|
+
notHydrated: ["products", "productsCursor", "hideProducts", "hideProductsLoading", "productsNotErrored", "productsNoPageError", "noEmpty", "productsMessage", "loadId"],
|
|
17
143
|
actions: {
|
|
18
144
|
load: async (s) => {
|
|
145
|
+
const run = ++loadId;
|
|
19
146
|
const id = detailInputId();
|
|
20
|
-
if (!id) return { ...s, status: "ready", ...presence("missing") };
|
|
21
|
-
|
|
22
|
-
const res = await getStoreCategory(id);
|
|
23
|
-
const c = res.category;
|
|
24
|
-
if (c?.id || c?.name) {
|
|
25
|
-
return {
|
|
26
|
-
status: "ready",
|
|
27
|
-
name: c.name || "Category",
|
|
28
|
-
subtitle: categoryCount(c),
|
|
29
|
-
description: c.description || "",
|
|
30
|
-
...presence("found"),
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
} catch { /* fall through to V1 */ }
|
|
147
|
+
if (!id) return { ...s, ...NO_PRODUCTS, loadId: run, status: "ready", ...presence("missing") };
|
|
148
|
+
let header: Pick<S, "name" | "subtitle" | "description">;
|
|
34
149
|
try {
|
|
35
|
-
|
|
36
|
-
const c = (((res as unknown as { collection?: WixCollection }).collection) ?? (res as unknown as WixCollection)) as unknown as {
|
|
37
|
-
name?: string; numberOfProducts?: number; description?: string;
|
|
38
|
-
};
|
|
39
|
-
const n = c.numberOfProducts;
|
|
40
|
-
return {
|
|
41
|
-
status: "ready",
|
|
42
|
-
name: c.name || "Collection",
|
|
43
|
-
subtitle: n != null ? `${n} ${n === 1 ? "product" : "products"}` : "",
|
|
44
|
-
description: c.description || "",
|
|
45
|
-
...presence("found"),
|
|
46
|
-
};
|
|
150
|
+
header = await readCategory(id);
|
|
47
151
|
} catch (e) {
|
|
48
|
-
|
|
152
|
+
if (run !== loadId) return undefined;
|
|
153
|
+
return { ...s, ...NO_PRODUCTS, loadId: run, status: "ready", ...presenceAfterError(e) };
|
|
49
154
|
}
|
|
155
|
+
// A newer load (onAppear after the boot load, pull-to-refresh) owns the screen now.
|
|
156
|
+
if (run !== loadId) return undefined;
|
|
157
|
+
categoryId = id;
|
|
158
|
+
// Starts the first page; its snapshots land through `productsChanged`, which ignores
|
|
159
|
+
// them until THIS state (stamped `run`) is committed. The "loading" snapshot is read
|
|
160
|
+
// synchronously below, so the header paints with the products skeleton under it.
|
|
161
|
+
void products.query({});
|
|
162
|
+
return withProducts({ ...s, ...header, loadId: run, status: "ready", ...presence("found") });
|
|
50
163
|
},
|
|
164
|
+
productsChanged: (s) => (s.loadId === loadId && s.found ? withProducts(s) : undefined),
|
|
165
|
+
loadMoreProducts: async () => { await products.loadMore(); return undefined; },
|
|
166
|
+
retryProducts: async () => { await products.retry(); return undefined; },
|
|
51
167
|
},
|
|
52
168
|
});
|
|
53
169
|
svc.load();
|
|
@@ -25845,6 +25845,9 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
25845
25845
|
"actionId": "tier5.himiBrandedProgram.enroll"
|
|
25846
25846
|
}
|
|
25847
25847
|
}
|
|
25848
|
+
},
|
|
25849
|
+
"modifiers": {
|
|
25850
|
+
"hiddenPath": "$service:himiBrandedProgram.enrollHidden"
|
|
25848
25851
|
}
|
|
25849
25852
|
},
|
|
25850
25853
|
{
|
|
@@ -25858,6 +25861,44 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
25858
25861
|
"color": "textSecondary",
|
|
25859
25862
|
"collapseWhenEmpty": true
|
|
25860
25863
|
}
|
|
25864
|
+
},
|
|
25865
|
+
"modifiers": {
|
|
25866
|
+
"hiddenPath": "$service:himiBrandedProgram.enrollHidden"
|
|
25867
|
+
}
|
|
25868
|
+
},
|
|
25869
|
+
{
|
|
25870
|
+
"id": "himi_program.buy",
|
|
25871
|
+
"body": {
|
|
25872
|
+
"package": "himalaya",
|
|
25873
|
+
"type": "Button",
|
|
25874
|
+
"props": {
|
|
25875
|
+
"title": "$service:himiBrandedProgram.buyLabel",
|
|
25876
|
+
"style": "primary",
|
|
25877
|
+
"fullWidth": true,
|
|
25878
|
+
"disabledPath": "$service:himiBrandedProgram.buyDisabled",
|
|
25879
|
+
"action": {
|
|
25880
|
+
"actionId": "tier5.himiBrandedProgram.buy"
|
|
25881
|
+
}
|
|
25882
|
+
}
|
|
25883
|
+
},
|
|
25884
|
+
"modifiers": {
|
|
25885
|
+
"hiddenPath": "$service:himiBrandedProgram.buyHidden"
|
|
25886
|
+
}
|
|
25887
|
+
},
|
|
25888
|
+
{
|
|
25889
|
+
"id": "himi_program.buy_note",
|
|
25890
|
+
"body": {
|
|
25891
|
+
"package": "himalaya",
|
|
25892
|
+
"type": "Text",
|
|
25893
|
+
"props": {
|
|
25894
|
+
"text": "$service:himiBrandedProgram.buyNote",
|
|
25895
|
+
"style": "footnote",
|
|
25896
|
+
"color": "textSecondary",
|
|
25897
|
+
"collapseWhenEmpty": true
|
|
25898
|
+
}
|
|
25899
|
+
},
|
|
25900
|
+
"modifiers": {
|
|
25901
|
+
"hiddenPath": "$service:himiBrandedProgram.buyHidden"
|
|
25861
25902
|
}
|
|
25862
25903
|
},
|
|
25863
25904
|
{
|
|
@@ -1,49 +1,11 @@
|
|
|
1
|
-
// Worker-side glue for the shared IAP
|
|
2
|
-
// shared by every screen that shows or sells an
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import type {
|
|
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";
|
|
6
7
|
import { copyParam } from "./himi-copy.js";
|
|
7
8
|
|
|
8
|
-
export interface HostIapScope {
|
|
9
|
-
scope: IapCatalogScope;
|
|
10
|
-
platform: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
let hostScope: Promise<HostIapScope> | undefined;
|
|
14
|
-
|
|
15
|
-
/** Read once per worker, and only by an enabled screen: a disabled one makes no app-info call.
|
|
16
|
-
* App identity and platform come from the host, never a screen param. The site is the host-owned
|
|
17
|
-
* native preference business (the web partition uses an OAuth client ID); without one, the mapping source's
|
|
18
|
-
* bound site applies. A failed read leaves identity empty, which fails every item closed — and is
|
|
19
|
-
* not kept, so the next lookup asks the host again instead of staying invalid for good. */
|
|
20
|
-
export function hostIapScope(): Promise<HostIapScope> {
|
|
21
|
-
return hostScope ??= readHostScope().then((read) => {
|
|
22
|
-
if (!hostScopeComplete(read)) hostScope = undefined;
|
|
23
|
-
return read;
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** An incomplete scope prices every item `invalidInput`, which the shared layer never offers to
|
|
28
|
-
* retry — so a screen offers Retry itself while its own host read is what failed. */
|
|
29
|
-
export function hostScopeComplete(read: HostIapScope): boolean {
|
|
30
|
-
return !!read.scope.appId && !!read.scope.bundleId && !!read.platform;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function readHostScope(): Promise<HostIapScope> {
|
|
34
|
-
const identity = await appInfo().catch(() => undefined);
|
|
35
|
-
const siteId = identity?.platform === "web" ? "" : hostSiteId();
|
|
36
|
-
return {
|
|
37
|
-
scope: { appId: identity?.appId ?? "", bundleId: identity?.bundleId ?? "", ...(siteId ? { siteId } : {}) },
|
|
38
|
-
platform: identity?.platform ?? "",
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function hostSiteId(): string {
|
|
43
|
-
const value = (globalThis as { himalaya?: { input?: Record<string, unknown> } }).himalaya?.input?.__himiPreferenceBusiness;
|
|
44
|
-
return typeof value === "string" ? value.trim() : "";
|
|
45
|
-
}
|
|
46
|
-
|
|
47
9
|
/** Price labels from the screen's copy pack (`<prefix>.priceLoadingText`, …). */
|
|
48
10
|
export function iapPriceCopy(prefix: string): IapPriceCopy {
|
|
49
11
|
return {
|
|
@@ -54,6 +16,21 @@ export function iapPriceCopy(prefix: string): IapPriceCopy {
|
|
|
54
16
|
};
|
|
55
17
|
}
|
|
56
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
|
+
|
|
57
34
|
/** What `notHydrated` does, for a runtime that ignores the option (a binary older than it, still
|
|
58
35
|
* served new bundles): put the listed keys back to their declared values before any action runs,
|
|
59
36
|
* so a persisted price or an enabled purchase action never paints as this run's. */
|
|
@@ -14,8 +14,11 @@
|
|
|
14
14
|
// back APPROVAL_PENDING (the owner approves; there is nothing to charge). Any failure
|
|
15
15
|
// degrades to the site-directing message rather than a dead button.
|
|
16
16
|
//
|
|
17
|
-
// App Store prices
|
|
18
|
-
//
|
|
17
|
+
// App Store prices, the payment route and the App Store sale come from the shared IAP layer
|
|
18
|
+
// (`createIapCommerce` in @himalaya/iap-checkout). An app that sells nothing through the App Store
|
|
19
|
+
// reads every program as `disabled` and keeps Enrol on its own commerce. An Apple-routed program
|
|
20
|
+
// offers Get access in place of Enrol and sells only through the shared checkout, never through the
|
|
21
|
+
// Wix payment rail.
|
|
19
22
|
import { defineService } from "@himalaya/state";
|
|
20
23
|
import {
|
|
21
24
|
getProgram,
|
|
@@ -31,8 +34,9 @@ import { cleanTitle, initials } from "../_shared/himi-text.js";
|
|
|
31
34
|
import { copyParam, copyTemplate, stashCopy } from "../_shared/himi-copy.js";
|
|
32
35
|
import { payCashierOrder } from "@himalaya/checkout";
|
|
33
36
|
import { formatCurrency } from "../_shared/engine-intl.js";
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
37
|
+
import { iapPriceFields, onlineProgramItem, type IapCatalogItem, type IapPollDirective, type IapPriceFields } from "@himalaya/iap";
|
|
38
|
+
import { createIapCommerce, onlineProgramOrders, type IapPurchaseFields } from "@himalaya/iap-checkout";
|
|
39
|
+
import { iapPriceCopy, iapPurchaseCopy, resetUnhydratedKeysOnOlderRuntime } from "../_shared/iap-worker.js";
|
|
36
40
|
|
|
37
41
|
/** Wix Online Programs appDefId — the vertical a participant's cashier order is
|
|
38
42
|
* paid under (mirrors AppDefId.CHALLENGES in Wix's RN apps). */
|
|
@@ -66,6 +70,13 @@ interface State {
|
|
|
66
70
|
/** Bound to the Enrol CTA's `disabledPath`; `enroll` re-checks the route itself regardless. */
|
|
67
71
|
enrollDisabled: boolean;
|
|
68
72
|
enrollNote: string;
|
|
73
|
+
/** An App Store program sells through Get access instead. */
|
|
74
|
+
enrollHidden: boolean;
|
|
75
|
+
/** The shared checkout's action (`commerce.purchaseFields`): shown only on the App Store route. */
|
|
76
|
+
buyHidden: boolean;
|
|
77
|
+
buyLabel: string;
|
|
78
|
+
buyDisabled: boolean;
|
|
79
|
+
buyNote: string;
|
|
69
80
|
_poll?: IapPollDirective;
|
|
70
81
|
}
|
|
71
82
|
|
|
@@ -174,15 +185,6 @@ async function fetchProgram(id: string): Promise<WixProgram | undefined> {
|
|
|
174
185
|
}
|
|
175
186
|
}
|
|
176
187
|
|
|
177
|
-
async function load(state: State, arg?: unknown): Promise<State> {
|
|
178
|
-
stashCopy(arg);
|
|
179
|
-
try {
|
|
180
|
-
return ready(state, await fetchProgram(inputId()));
|
|
181
|
-
} catch {
|
|
182
|
-
return derive({ ...state, status: "error" });
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
188
|
/** Whether the existing commerce flow may start (before the join) and continue (before the Wix
|
|
187
189
|
* payment): a price refresh can move the program onto a route this flow must not charge. */
|
|
188
190
|
interface CommerceGuards {
|
|
@@ -190,8 +192,6 @@ interface CommerceGuards {
|
|
|
190
192
|
mayContinue(): boolean;
|
|
191
193
|
}
|
|
192
194
|
|
|
193
|
-
const ALWAYS_ALLOWED: CommerceGuards = { mayStart: () => true, mayContinue: () => true };
|
|
194
|
-
|
|
195
195
|
async function enrollmentMessage(name: string, guards: CommerceGuards): Promise<string> {
|
|
196
196
|
const blocked = () => iapPriceCopy("program").purchaseUnavailable;
|
|
197
197
|
if (!guards.mayStart()) return blocked();
|
|
@@ -251,27 +251,27 @@ async function enrollmentMessage(name: string, guards: CommerceGuards): Promise<
|
|
|
251
251
|
return copyTemplate("program.joinOnSiteMessage", "Enrolment happens on the website — visit the site to join {name}.", { name });
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
|
|
255
|
-
if (!inputId()) return state;
|
|
256
|
-
return derive({ ...state, actionMessage: await enrollmentMessage(state.name, ALWAYS_ALLOWED) });
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// ── App Store pricing (enabled only when a mapping backend is connected) ─────
|
|
254
|
+
// ── Content, App Store pricing and sale ─────
|
|
260
255
|
//
|
|
261
|
-
// One writer: `
|
|
262
|
-
// through the synchronous `pricingPublish`, which re-reads the latest content
|
|
263
|
-
// action returning its own state could land after a refresh and
|
|
256
|
+
// One writer: `load`, `enroll` and `buy` keep their results here and every state
|
|
257
|
+
// change goes through the synchronous `pricingPublish`, which re-reads the latest content, route
|
|
258
|
+
// and checkout state. An async action returning its own state could land after a refresh and
|
|
259
|
+
// revive a stale price, route or purchase.
|
|
264
260
|
|
|
265
|
-
const
|
|
266
|
-
const
|
|
261
|
+
const commerce = createIapCommerce({ adapters: [onlineProgramOrders], onChange: () => { void svc.pricingPublish(); } });
|
|
262
|
+
const pricing = commerce.pricing;
|
|
263
|
+
const PRICING_NOT_HYDRATED = [
|
|
264
|
+
"priceLine", "priceNote", "hidePriceRetry", "enrollDisabled", "enrollNote", "enrollHidden", "buyHidden", "buyLabel", "buyDisabled", "buyNote",
|
|
265
|
+
] as const;
|
|
267
266
|
const CONTENT_KEYS = ["status", "name", "description", "meta", "image", "initial", "emptyText"] as const;
|
|
268
267
|
type Content = Pick<State, (typeof CONTENT_KEYS)[number]>;
|
|
269
268
|
const NO_PRICE: IapPriceFields = { priceText: "", priceNote: "", priceLoading: false, hidePriceRetry: true, actionDisabled: true, actionNote: "" };
|
|
269
|
+
const NOT_APPLE: IapPurchaseFields = { appleRoute: false, accessReady: false, buyLabel: "", buyDisabled: true, buyBusy: false, buyNote: "" };
|
|
270
270
|
let content: Content;
|
|
271
271
|
let pricedItem: IapCatalogItem | undefined;
|
|
272
|
-
let scopeIncomplete = false;
|
|
273
272
|
let actionMessage = "";
|
|
274
273
|
let loadSeq = 0;
|
|
274
|
+
let commerceStarted = false;
|
|
275
275
|
|
|
276
276
|
function contentOf(state: State): Content {
|
|
277
277
|
return Object.fromEntries(CONTENT_KEYS.map((key) => [key, state[key]])) as Content;
|
|
@@ -279,7 +279,7 @@ function contentOf(state: State): Content {
|
|
|
279
279
|
|
|
280
280
|
const programReference = () => onlineProgramItem({ id: inputId() })?.reference;
|
|
281
281
|
|
|
282
|
-
async function
|
|
282
|
+
async function load(state: State, arg?: unknown): Promise<undefined> {
|
|
283
283
|
stashCopy(arg);
|
|
284
284
|
const seq = ++loadSeq;
|
|
285
285
|
const found = await fetchProgram(inputId()).then((program) => ({ program }), () => undefined);
|
|
@@ -288,20 +288,27 @@ async function loadPriced(state: State, arg?: unknown): Promise<undefined> {
|
|
|
288
288
|
pricedItem = found?.program ? onlineProgramItem({ id: inputId() }, priceOf(found.program)) : pricedItem;
|
|
289
289
|
void svc.pricingPublish();
|
|
290
290
|
if (found?.program) void priceProgram();
|
|
291
|
+
void resumeCommerce();
|
|
291
292
|
return undefined;
|
|
292
293
|
}
|
|
293
294
|
|
|
295
|
+
/** First entry recovers held purchases; every later one (a return to the screen) reconciles. */
|
|
296
|
+
async function resumeCommerce(): Promise<void> {
|
|
297
|
+
const first = !commerceStarted;
|
|
298
|
+
commerceStarted = true;
|
|
299
|
+
await (first ? commerce.start() : commerce.reconcile());
|
|
300
|
+
void svc.pricingPublish();
|
|
301
|
+
}
|
|
302
|
+
|
|
294
303
|
/** Content never waits on this, and the host scope is re-read each time, so a failed app-info
|
|
295
304
|
* read recovers on the next refresh. */
|
|
296
305
|
async function priceProgram(): Promise<void> {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
scopeIncomplete = !hostScopeComplete(host);
|
|
301
|
-
if (pricedItem) await pricing.load({ ...host, items: [pricedItem] });
|
|
306
|
+
if (!pricedItem) return;
|
|
307
|
+
await commerce.loadPrices([pricedItem]);
|
|
308
|
+
void svc.pricingPublish();
|
|
302
309
|
}
|
|
303
310
|
|
|
304
|
-
async function
|
|
311
|
+
async function enroll(): Promise<undefined> {
|
|
305
312
|
const reference = programReference();
|
|
306
313
|
if (!reference) return undefined;
|
|
307
314
|
// A tap that lands as a refresh starts is refused without a verdict: the route is unknown, not
|
|
@@ -314,13 +321,29 @@ async function enrollPriced(): Promise<undefined> {
|
|
|
314
321
|
return undefined;
|
|
315
322
|
}
|
|
316
323
|
|
|
324
|
+
/** The App Store sale: only through the shared checkout, and only for the route the price shows. */
|
|
325
|
+
async function buy(): Promise<undefined> {
|
|
326
|
+
const reference = programReference();
|
|
327
|
+
if (!reference) return undefined;
|
|
328
|
+
await commerce.buy({ reference });
|
|
329
|
+
void svc.pricingPublish();
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
332
|
+
|
|
317
333
|
function pricedState(state: State): State {
|
|
318
334
|
const reference = programReference();
|
|
319
|
-
const
|
|
335
|
+
const view = reference && pricing.view(reference);
|
|
336
|
+
const price = view ? iapPriceFields(view, iapPriceCopy("program")) : NO_PRICE;
|
|
337
|
+
const purchase = reference ? commerce.purchaseFields(reference, iapPurchaseCopy("program")) : NOT_APPLE;
|
|
338
|
+
const poll = commerce.pollDirective("himiBrandedProgram.iapPoll");
|
|
320
339
|
return derive({
|
|
321
|
-
...state, ...content,
|
|
322
|
-
|
|
323
|
-
|
|
340
|
+
...state, ...content,
|
|
341
|
+
actionMessage: purchase.accessReady ? copyTemplate("program.joinedMessage", "You're enrolled in {name}.", { name: content.name }) : actionMessage,
|
|
342
|
+
// `disabled`: this app sells nothing through the App Store, so the program shows its own price.
|
|
343
|
+
priceLine: view?.route === "disabled" ? pricedItem?.commerceDisplayPrice ?? "" : price.priceText, priceNote: price.priceNote, hidePriceRetry: view?.route === "disabled" || (price.hidePriceRetry && commerce.scopeComplete()),
|
|
344
|
+
enrollDisabled: price.actionDisabled, enrollNote: price.actionNote, enrollHidden: purchase.appleRoute,
|
|
345
|
+
buyHidden: !purchase.appleRoute || purchase.accessReady, buyLabel: purchase.buyLabel, buyDisabled: purchase.buyDisabled, buyNote: purchase.buyNote,
|
|
346
|
+
...(poll ? { _poll: poll } : {}),
|
|
324
347
|
});
|
|
325
348
|
}
|
|
326
349
|
|
|
@@ -344,34 +367,41 @@ const INITIAL_STATE: State = derive({
|
|
|
344
367
|
hideContent: true,
|
|
345
368
|
priceNote: "",
|
|
346
369
|
hidePriceRetry: true,
|
|
347
|
-
enrollDisabled:
|
|
370
|
+
enrollDisabled: true,
|
|
348
371
|
enrollNote: "",
|
|
372
|
+
enrollHidden: false,
|
|
373
|
+
buyHidden: true,
|
|
374
|
+
buyLabel: "",
|
|
375
|
+
buyDisabled: true,
|
|
376
|
+
buyNote: "",
|
|
349
377
|
});
|
|
350
378
|
|
|
351
379
|
type Actions = {
|
|
352
380
|
load: (state: State, arg?: unknown) => Promise<State | undefined>;
|
|
353
381
|
enroll: (state: State) => Promise<State | undefined>;
|
|
382
|
+
buy: (state: State) => Promise<State | undefined>;
|
|
354
383
|
pricingPublish: (state: State) => State;
|
|
355
384
|
pricingStart: (state: State) => State;
|
|
356
385
|
pricingRefresh: (state: State) => Promise<undefined>;
|
|
386
|
+
iapPoll: (state: State) => Promise<undefined>;
|
|
357
387
|
};
|
|
358
388
|
|
|
359
389
|
const svc = defineService<State, Actions>("himiBrandedProgram", {
|
|
360
390
|
state: INITIAL_STATE,
|
|
361
|
-
|
|
391
|
+
notHydrated: [...PRICING_NOT_HYDRATED],
|
|
362
392
|
actions: {
|
|
363
|
-
load
|
|
364
|
-
enroll
|
|
393
|
+
load,
|
|
394
|
+
enroll,
|
|
395
|
+
buy,
|
|
365
396
|
pricingPublish: pricedState,
|
|
366
|
-
pricingStart:
|
|
397
|
+
pricingStart: pricedState,
|
|
367
398
|
pricingRefresh: async () => { await priceProgram(); return undefined; },
|
|
399
|
+
iapPoll: async () => { await commerce.poll(); void svc.pricingPublish(); return undefined; },
|
|
368
400
|
},
|
|
369
401
|
});
|
|
370
402
|
|
|
371
403
|
content = contentOf(svc.getState());
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
void svc.pricingStart();
|
|
375
|
-
}
|
|
404
|
+
resetUnhydratedKeysOnOlderRuntime(svc, INITIAL_STATE, PRICING_NOT_HYDRATED);
|
|
405
|
+
void svc.pricingStart();
|
|
376
406
|
|
|
377
407
|
export { svc };
|