@wix/himalaya-cli 0.1243.0 → 0.1245.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/sdk/iap/src/catalog-adapters/online-programs.d.ts +6 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/catalog-price-fields.d.ts +4 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/catalog-pricing-contract.d.ts +87 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/catalog-pricing-core.d.ts +10 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/catalog-pricing.d.ts +6 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/index.d.ts +4 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/mapping-source.d.ts +5 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/checkout/src/index.d.ts +3 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/checkout.mjs +3 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap.mjs +192 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/screens.mjs +3 -0
- package/dist/cli.mjs +18 -18
- package/dist/preview-assets.generated.bin +0 -0
- package/guides/getting-started.md +6 -0
- package/guides/himi-skill.md +6 -0
- package/guides/iap-pricing.md +162 -211
- package/guides/index.json +7 -6
- package/guides/sdk-reference.json +96 -0
- package/guides/tier-5-add-a-screen.md +9 -0
- package/guides/wix-api-usage.md +11 -0
- package/guides/wix-site-to-himi-app.md +1 -1
- package/package.json +2 -2
- package/templates/beauty-salon-owner/dev/index.ts +1 -1
- package/templates/branded-lite/dev/index.ts +110 -3
- package/templates/branded-lite/tier5-src/_shared/iap-worker.ts +65 -0
- package/templates/branded-lite/tier5-src/himi-branded-lite-program/index.ts +179 -52
- package/templates/branded-lite/tier5-src/himi-branded-lite-programs/index.ts +158 -38
- package/templates/community-club-owner/dev/index.ts +1 -1
- package/templates/events-meetups-owner/dev/index.ts +1 -1
- package/templates/gym-fitness-owner/dev/index.ts +1 -1
- package/templates/nonprofit-cause-owner/dev/index.ts +1 -1
- package/templates/owner-lite/dev/index.ts +1 -1
- package/templates/podcast-creator-owner/dev/index.ts +1 -1
- package/templates/portfolio-creative-owner/dev/index.ts +1 -1
- package/templates/restaurant-owner/dev/index.ts +1 -1
- package/templates/retail-store-owner/dev/index.ts +1 -1
- package/templates/yoga-studio/dev/index.ts +110 -3
- package/templates/yoga-studio/tier5-src/_shared/iap-worker.ts +65 -0
- package/templates/yoga-studio/tier5-src/himi-branded-lite-program/index.ts +179 -52
- package/templates/yoga-studio/tier5-src/himi-branded-lite-programs/index.ts +158 -38
- package/templates/yoga-studio-owner/dev/index.ts +1 -1
package/guides/iap-pricing.md
CHANGED
|
@@ -1,225 +1,176 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
`@himalaya/iap` resolves read-only payment mappings and retrieves Apple metadata for
|
|
4
|
-
cards, details, and checkout previews. It neither buys anything nor proves access.
|
|
5
|
-
The mapping provider is an internal client port; a deployed production backend is
|
|
6
|
-
still required. Do not call the purchase-delivery coordinator for price lookup.
|
|
7
|
-
|
|
8
|
-
## Inputs and states
|
|
9
|
-
|
|
10
|
-
Create one controller per worker/service with `createPricingController({ mapping,
|
|
11
|
-
getProducts, onState })`. Inject the existing SDK `getProducts` as the real store
|
|
12
|
-
adapter; tests may inject another function. `load(input)` accepts:
|
|
13
|
-
|
|
14
|
-
- `scope`: app ID, bundle ID, Wix site ID, and optional opaque `contextRevision`.
|
|
15
|
-
Change the revision whenever a member/audience change affects mapping. Pass no
|
|
16
|
-
credential or raw member identity.
|
|
17
|
-
- `platform`: the actual trusted host platform. Only `ios` resolves Apple prices.
|
|
18
|
-
- `items`: `{ reference: { kind, id }, commerceDisplayPrice? }`. References are
|
|
19
|
-
unique by the pair; repeated references retain the first occurrence and its
|
|
20
|
-
commerce input. Different entity kinds cannot collide.
|
|
21
|
-
|
|
22
|
-
The injected batch `mapping.resolve({ scope, references })` returns one mapping
|
|
23
|
-
per requested reference: `appleIap` with `productId` and `mappingRevision`,
|
|
24
|
-
`regular`, or `unavailable`. Missing, duplicate, unknown, or malformed routes are
|
|
25
|
-
unresolved. This is not an agreed Scala wire contract or a deployed endpoint.
|
|
26
|
-
|
|
27
|
-
`getState()` returns a detached serializable snapshot with revision, captured
|
|
28
|
-
scope, and per-reference items. Loading and unavailable items contain **no price**.
|
|
29
|
-
Apple ready items carry `source: "appleIap"`, product ID, mapping revision, and
|
|
30
|
-
exact StoreKit `displayPrice`; regular ready items carry `source: "regular"` and
|
|
31
|
-
the caller's existing commerce price. Copy remains the screen's responsibility.
|
|
32
|
-
When that commerce price is missing or blank, `commercePriceUnavailable` is
|
|
33
|
-
non-retryable: the caller must supply a usable price in a new `load(input)`.
|
|
34
|
-
Refreshing the same input cannot supply it.
|
|
35
|
-
|
|
36
|
-
Apple strings are displayed unchanged, including spaces and currency placement;
|
|
37
|
-
they are not reconstructed from micros or phone locale. See Apple's
|
|
38
|
-
[displayPrice contract](https://developer.apple.com/documentation/storekit/product/displayprice).
|
|
39
|
-
Only non-consumables are supported here. A lookup failure, empty response, missing
|
|
40
|
-
product, or blank price is `priceUnavailable`, not proof of a network failure or
|
|
41
|
-
nonexistent product: the current native adapter collapses store errors into `[]`.
|
|
42
|
-
There is never a Wix price or external-checkout fallback for an Apple route.
|
|
43
|
-
|
|
44
|
-
`load` batches unique Apple IDs and joins identical work already in flight within
|
|
45
|
-
this controller. Input identity includes scope, platform, item order/references,
|
|
46
|
-
and commerce display prices. Every call after completion performs a fresh lookup.
|
|
47
|
-
`refresh()` reloads the last captured input, also joining identical in-flight work.
|
|
48
|
-
Changing input invalidates prior work before its results can publish; obsolete
|
|
49
|
-
mapping results do not start a store lookup. `dispose()` clears readable state,
|
|
50
|
-
suppresses later publications/calls, and cannot cancel store/network I/O already
|
|
51
|
-
sent. No persistent or app-wide cache exists; separate workers perform independent
|
|
52
|
-
lookups.
|
|
53
|
-
|
|
54
|
-
## Complete Tier-5 service example
|
|
55
|
-
|
|
56
|
-
This example is compiled and executed by `pricing-integration.test.ts` against the
|
|
57
|
-
actual worker bootstrap and SDK dispatch seam. Register it separately in each
|
|
58
|
-
screen worker, injecting the mapping provider and trusted initial input. It is a
|
|
59
|
-
checkout **preview**: purchase readiness stays false independently of metadata.
|
|
1
|
+
# Showing App Store prices on any screen
|
|
60
2
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
type PricingInput, type PricingMappingProvider, type PricingState,
|
|
67
|
-
} from "@himalaya/iap";
|
|
68
|
-
|
|
69
|
-
export interface PriceCopy {
|
|
70
|
-
loading: string;
|
|
71
|
-
unavailable: string;
|
|
72
|
-
apple: string;
|
|
73
|
-
regular: string;
|
|
74
|
-
}
|
|
3
|
+
`@himalaya/iap` gives every screen one shared way to show the App Store price of an item it
|
|
4
|
+
displays or sells: a program, a product, a plan, or anything else you registered with
|
|
5
|
+
`himi store iap`. Your worker passes its scope and the items on screen. The shared layer looks up
|
|
6
|
+
which items are sold through the App Store, fetches their prices from Apple, and returns display
|
|
7
|
+
state plus a payment route. Your screen binds the result to its own UI.
|
|
75
8
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
message: string;
|
|
81
|
-
retryable: boolean;
|
|
82
|
-
loading: boolean;
|
|
83
|
-
hideLoading: boolean;
|
|
84
|
-
hideRetry: boolean;
|
|
85
|
-
}
|
|
9
|
+
> **Not switched on yet.** Wix has not connected the service that maps your catalog to App Store
|
|
10
|
+
> products. Until it does, every screen keeps its existing price and checkout. Apple purchasing is
|
|
11
|
+
> not available in any case: this layer shows prices and blocks the wrong checkout, but never buys.
|
|
12
|
+
> Only one-time purchases (non-consumables) are supported. Subscriptions and consumables are not.
|
|
86
13
|
|
|
87
|
-
|
|
88
|
-
pricing: PricingState;
|
|
89
|
-
rows: PriceRow[];
|
|
90
|
-
canPurchase: boolean;
|
|
91
|
-
purchaseDisabled: boolean;
|
|
92
|
-
}
|
|
14
|
+
## What you get per item
|
|
93
15
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
16
|
+
`pricing.view(reference)` returns plain data you can put in state. Its `route` tells the screen
|
|
17
|
+
what to do:
|
|
18
|
+
|
|
19
|
+
| `route` | Show | Existing checkout |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| `disabled` | Your screen's own price, exactly as before | Allowed |
|
|
22
|
+
| `resolving` | A "Loading price…" label, never your own price | Blocked |
|
|
23
|
+
| `appleIap` | `displayPrice` exactly as Apple formats it, plus a "One-time purchase" note | Blocked; purchasing is not available yet |
|
|
24
|
+
| `regular` | `displayPrice` (the commerce price you passed), or nothing | Allowed |
|
|
25
|
+
| `unavailable` | "Price unavailable", plus Retry when `retryable` | Blocked |
|
|
26
|
+
|
|
27
|
+
Never fall back to your own price or checkout for an item on an App Store route. A missing price
|
|
28
|
+
does not mean the item is free. Only an explicit `regular` route keeps your existing flow.
|
|
29
|
+
|
|
30
|
+
`iapPriceFields(view, copy)` turns a view into bindable fields (`priceText`, `priceNote`,
|
|
31
|
+
`priceLoading`, `hidePriceRetry`, `actionDisabled`, `actionNote`) using labels from your own copy.
|
|
32
|
+
|
|
33
|
+
## Catalog references
|
|
34
|
+
|
|
35
|
+
An item is identified by `{ kind, id }`. The kind is the same entity type `himi store iap` uses
|
|
36
|
+
when it registers the item with Apple:
|
|
37
|
+
|
|
38
|
+
| `kind` | Item | Helper |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| `onlineProgram` | An Online Programs program | `onlineProgramItem(program, commercePrice)` |
|
|
41
|
+
| `storesProduct` | A Wix Stores product | Build `{ kind: "storesProduct", id }` |
|
|
42
|
+
| `pricingPlan` | A one-time Pricing Plans plan | Build `{ kind: "pricingPlan", id }` |
|
|
43
|
+
|
|
44
|
+
## Add it to a worker
|
|
45
|
+
|
|
46
|
+
This is a complete worker for one screen. Copy the pattern, not the course names.
|
|
103
47
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
48
|
+
```ts
|
|
49
|
+
// catalog-pricing-worker.ts
|
|
50
|
+
import { defineService } from "@himalaya/state";
|
|
51
|
+
import { get as appInfo } from "@himalaya/app-info";
|
|
52
|
+
import { createIapCatalogPricing, iapPriceFields, onlineProgramItem } from "@himalaya/iap";
|
|
53
|
+
|
|
54
|
+
const COPY = {
|
|
55
|
+
loading: "Loading price…",
|
|
56
|
+
unavailable: "Price unavailable",
|
|
57
|
+
oneTimePurchase: "One-time purchase",
|
|
58
|
+
purchaseUnavailable: "Purchasing is not available yet.",
|
|
59
|
+
};
|
|
60
|
+
// This run's prices and buy state must never be restored from a cold-boot snapshot.
|
|
61
|
+
const PRICE_KEYS = ["priceText", "priceNote", "hidePriceRetry", "buyDisabled", "buyNote"];
|
|
62
|
+
|
|
63
|
+
interface Course { id: string; title: string; commercePrice: string }
|
|
64
|
+
type CourseState = Record<string, unknown>;
|
|
65
|
+
|
|
66
|
+
/** `startCheckout` is the screen's existing commerce flow. */
|
|
67
|
+
export function registerCourseScreen(course: Course, startCheckout: () => Promise<string>) {
|
|
68
|
+
const pricing = createIapCatalogPricing({ onChange: () => { void service.publish(); } });
|
|
69
|
+
const item = onlineProgramItem({ id: course.id }, course.commercePrice)!;
|
|
70
|
+
let message = "";
|
|
71
|
+
let scopeIncomplete = false;
|
|
72
|
+
|
|
73
|
+
async function hostScope() {
|
|
74
|
+
const info = await appInfo().catch(() => undefined);
|
|
75
|
+
scopeIncomplete = !info?.appId || !info?.bundleId || !info?.platform;
|
|
76
|
+
return { scope: { appId: info?.appId ?? "", bundleId: info?.bundleId ?? "" }, platform: info?.platform ?? "" };
|
|
116
77
|
}
|
|
117
78
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
79
|
+
async function loadPrices() {
|
|
80
|
+
if (pricing.enabled) await pricing.load({ ...(await hostScope()), items: [item] });
|
|
81
|
+
void service.publish();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function present(state: CourseState): CourseState {
|
|
85
|
+
if (!pricing.enabled) return { ...state, priceText: course.commercePrice, buyDisabled: false, message };
|
|
86
|
+
const price = iapPriceFields(pricing.view(item.reference), COPY);
|
|
87
|
+
return { ...state, priceText: price.priceText, priceNote: price.priceNote, hidePriceRetry: price.hidePriceRetry && !scopeIncomplete,
|
|
88
|
+
buyDisabled: price.actionDisabled, buyNote: price.actionNote, message };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const service = defineService("course", {
|
|
92
|
+
state: { title: course.title, priceText: "", priceNote: "", hidePriceRetry: true, buyDisabled: pricing.enabled, buyNote: "", message: "" } as CourseState,
|
|
93
|
+
notHydrated: pricing.enabled ? PRICE_KEYS : [],
|
|
94
|
+
actions: {
|
|
95
|
+
// The screen starts the first lookup; the host's poll and Retry only refresh one that exists.
|
|
96
|
+
start: (state: CourseState) => {
|
|
97
|
+
void loadPrices();
|
|
98
|
+
return { ...present(state), _poll: pricing.pollDirective("course.refreshPrice") };
|
|
99
|
+
},
|
|
100
|
+
publish: (state: CourseState) => present(state),
|
|
101
|
+
// Re-reads the host too: `pricing.refresh()` would replay a scope whose identity read failed.
|
|
102
|
+
refreshPrice: async () => { await loadPrices(); return undefined; },
|
|
103
|
+
buy: async () => {
|
|
104
|
+
// Re-check at the moment of the mutation: a price refresh can change the route after render.
|
|
105
|
+
// While a refresh resolves the route is unknown, so refuse without claiming anything.
|
|
106
|
+
const start = pricing.checkoutGuard(item.reference);
|
|
107
|
+
if (start.allowed) message = await startCheckout();
|
|
108
|
+
else message = start.route === "resolving" ? "" : COPY.purchaseUnavailable;
|
|
109
|
+
void service.publish();
|
|
110
|
+
return undefined;
|
|
111
|
+
},
|
|
143
112
|
},
|
|
144
|
-
};
|
|
145
|
-
const initialPricing: PricingState = { ...controller.getState(), items: initialInput.items.map(item => ({
|
|
146
|
-
reference: { kind: item.reference.kind, id: item.reference.id }, status: "loading",
|
|
147
|
-
})) };
|
|
148
|
-
const service = defineService<PriceServiceState, typeof actions>("pricing", {
|
|
149
|
-
state: { pricing: initialPricing, rows: project(initialPricing), canPurchase: false, purchaseDisabled: true },
|
|
150
|
-
notHydrated: ["pricing", "rows", "canPurchase", "purchaseDisabled"], actions,
|
|
151
113
|
});
|
|
152
114
|
void service.start();
|
|
153
115
|
return service;
|
|
154
116
|
}
|
|
155
117
|
```
|
|
156
118
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
the
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
`
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
`liftgear-pricing` scheme and a pricing-card home supplies the navigation menu
|
|
216
|
-
and initial screen. This keeps the embedded demonstration visible after an
|
|
217
|
-
update over an existing LiftGear installation or a live config refresh, without
|
|
218
|
-
changing stored member data or managed purchase policy. Ordinary or incomplete
|
|
219
|
-
baked configs continue to use the cached navigation. This is a bounded preview
|
|
220
|
-
host rule, not a general fix for distribution release routing.
|
|
221
|
-
The separate, explicitly armed purchase demonstration calls
|
|
222
|
-
`purchaseForVerification` with a labelled synthetic association, reports Apple's
|
|
223
|
-
actual transaction environment, and leaves the transaction unfinished. It has
|
|
224
|
-
no transaction-server integration or program-access delivery. Pricing alone
|
|
225
|
-
does not arm the demonstration or start a purchase.
|
|
119
|
+
Bind the fields in the screen descriptor and declare both capabilities the worker calls. To show
|
|
120
|
+
real App Store prices, the app must also grant `iap` (and `app_info`) in its `config.ts`
|
|
121
|
+
`capabilities`, so its native shell registers the store bridge; until it does, the screen fails
|
|
122
|
+
closed to "Price unavailable" rather than showing a price it could not confirm.
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"logicBundle": { "declaredCapabilities": ["iap", "app_info"] },
|
|
127
|
+
"price": { "type": "Text", "props": { "text": "$service:course.priceText", "collapseWhenEmpty": true } },
|
|
128
|
+
"note": { "type": "Text", "props": { "text": "$service:course.priceNote", "collapseWhenEmpty": true } },
|
|
129
|
+
"retry": { "type": "Button", "props": { "title": "Try again", "action": { "actionId": "tier5.course.refreshPrice" } },
|
|
130
|
+
"modifiers": { "hiddenPath": "$service:course.hidePriceRetry" } },
|
|
131
|
+
"buy": { "type": "Button", "props": { "title": "Enrol", "disabledPath": "$service:course.buyDisabled",
|
|
132
|
+
"action": { "actionId": "tier5.course.buy" } } },
|
|
133
|
+
"buyNote": { "type": "Text", "props": { "text": "$service:course.buyNote", "collapseWhenEmpty": true } }
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Rules
|
|
138
|
+
|
|
139
|
+
- **One pricing instance per worker, one `load` per set of visible items.** Pass every item the
|
|
140
|
+
screen shows in a single call; the layer batches the lookup and fetches each Apple product once.
|
|
141
|
+
- **Guard the action, not only the button.** Call `checkoutGuard` immediately before a commerce flow
|
|
142
|
+
starts; actions can run without a tap. Before each later step of a flow that already started (a
|
|
143
|
+
payment after a join or reservation), call `continueCheckoutGuard`: it lets the step through on the
|
|
144
|
+
route the last lookup settled, so a routine refresh cannot strand the flow halfway, and stops it
|
|
145
|
+
once a refresh settles on an App Store route. If the flow pays through `payOrder` from
|
|
146
|
+
`@wix/himalaya/checkout`, pass the same check as `mayCharge`; it runs again right before the wallet
|
|
147
|
+
charges and before the hosted cashier opens.
|
|
148
|
+
- **One writer.** Publish through a synchronous action that reads `pricing.view` at that moment.
|
|
149
|
+
An async action that returns its own state can land after a newer price and repaint a stale one.
|
|
150
|
+
- **No timers.** `pollDirective` asks the app to refresh every 60 seconds while the screen is
|
|
151
|
+
visible and on return to it. Point it and Retry at an action that reads the host and calls `load`
|
|
152
|
+
again, so a failed identity read recovers; `refresh` replays the last scope unchanged. Every
|
|
153
|
+
lookup clears the old price first.
|
|
154
|
+
- **Keep prices out of hydration** with `notHydrated`, so a cold start never shows an old amount.
|
|
155
|
+
- **Content first.** A pricing failure is per item. Never replace the screen with an error because a
|
|
156
|
+
price could not load.
|
|
157
|
+
- **iOS only.** On Android, web and macOS, App Store routes come back `unavailable`. Regular
|
|
158
|
+
routes are unaffected.
|
|
159
|
+
|
|
160
|
+
## Adopting it
|
|
161
|
+
|
|
162
|
+
- **Starter templates with Online Programs** (`branded-lite`, `yoga-studio`) already include it on
|
|
163
|
+
their Programs list and program screens.
|
|
164
|
+
- **Existing screens, including apps created from a template before this existed:** an SDK update
|
|
165
|
+
never changes a screen that does not call this layer. Add the worker calls, the bindings and the
|
|
166
|
+
two capabilities above. After that, screens pick up layer fixes on their next build.
|
|
167
|
+
- **Custom screens:** follow the worker above for any item you sell, whatever its vertical.
|
|
168
|
+
- **New item types:** use the kind `himi store iap` registers for it, build `{ kind, id }`, and call
|
|
169
|
+
the same layer. Never write your own mapping lookup, App Store price request or payment guard.
|
|
170
|
+
|
|
171
|
+
## What turns it on
|
|
172
|
+
|
|
173
|
+
Register your digital products with `himi store iap` (see `himi docs iap-products`). When Wix
|
|
174
|
+
connects the mapping service those registrations feed, screens that use this layer start showing
|
|
175
|
+
App Store prices on their next build — in an app that grants the `iap` capability in `config.ts`.
|
|
176
|
+
Screens that do not use the layer are unaffected.
|
package/guides/index.json
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
],
|
|
21
21
|
"guardrails": [
|
|
22
22
|
"Prefer Tier-5 (SDUI descriptor + JS worker) over native screens — reach for native only when SDUI genuinely can't express it.",
|
|
23
|
+
"A screen that shows or sells an item that may be an App Store in-app purchase (a program, product or plan — template or custom screen alike) uses the shared layer `createIapCatalogPricing` from `@himalaya/iap` (`himi docs iap-pricing`): it batches the mapping lookup, fetches Apple prices and decides the payment route. Never hand-wire a mapping, an Apple price request or a payment guard in a worker. Call `checkoutGuard` immediately before a commerce flow starts, and `continueCheckoutGuard` before each later step of a flow already under way (a payment after a join), so a routine price refresh cannot strand it halfway. It is off until Wix connects the mapping service, and Apple purchasing is not available yet.",
|
|
23
24
|
"Keep structure descriptor-authored: use FragmentSlot for enumerable variants and bound list/grid templates for repetition. Use WorkerSubtree only for runtime-unknowable structure, with an explicit screen allowlist, policy budgets, and shipped fallback.",
|
|
24
25
|
"Bindings ($service:<svc>.<path>, $item.<field>, $flag:<key>) are plain strings in ANY prop — INCLUDING Modifiers token refs (background, color, tint), so a worker can drive per-item theming, not just text. They CANNOT target strict-enum fields (style, size, alignment, severity, presentation, …) — those fail at render, not validation.",
|
|
25
26
|
"Workers are sandboxed: no DOM, no timers, no raw fetch. Produce state synchronously in actions; do not rely on an ambient refresh tick.",
|
|
@@ -178,12 +179,6 @@
|
|
|
178
179
|
"category": "Workers & SDKs",
|
|
179
180
|
"file": "capabilities.md"
|
|
180
181
|
},
|
|
181
|
-
{
|
|
182
|
-
"id": "iap-pricing",
|
|
183
|
-
"title": "Apple IAP pricing in a worker",
|
|
184
|
-
"category": "Workers & SDKs",
|
|
185
|
-
"file": "iap-pricing.md"
|
|
186
|
-
},
|
|
187
182
|
{
|
|
188
183
|
"id": "wix-api-usage",
|
|
189
184
|
"title": "Call Wix APIs from a worker",
|
|
@@ -327,6 +322,12 @@
|
|
|
327
322
|
"title": "Registering digital products as App Store in-app purchases",
|
|
328
323
|
"category": "CLI & Loop",
|
|
329
324
|
"file": "iap-products.md"
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
"id": "iap-pricing",
|
|
328
|
+
"title": "Showing App Store prices on any screen",
|
|
329
|
+
"category": "Workers & SDKs",
|
|
330
|
+
"file": "iap-pricing.md"
|
|
330
331
|
}
|
|
331
332
|
],
|
|
332
333
|
"componentReferenceFile": "component-reference.json",
|
|
@@ -1777,6 +1777,102 @@
|
|
|
1777
1777
|
"jsdoc": "/** Creates one worker's read-only pricing controller with injected mapping, store lookup, and state publication. */",
|
|
1778
1778
|
"decl": "export function createPricingController(deps: PricingDependencies): PricingController;"
|
|
1779
1779
|
},
|
|
1780
|
+
{
|
|
1781
|
+
"name": "IAP_CATALOG_KINDS",
|
|
1782
|
+
"kind": "const",
|
|
1783
|
+
"jsdoc": "/** The Wix entity types `himi store iap` writes into its Wix→Apple mapping, so a mapping\n * backend can serve that export without translating kinds. */",
|
|
1784
|
+
"decl": "export const IAP_CATALOG_KINDS = [\"onlineProgram\", \"storesProduct\", \"pricingPlan\"] as const;"
|
|
1785
|
+
},
|
|
1786
|
+
{
|
|
1787
|
+
"name": "IapCatalogKind",
|
|
1788
|
+
"kind": "type",
|
|
1789
|
+
"jsdoc": "",
|
|
1790
|
+
"decl": "export type IapCatalogKind = typeof IAP_CATALOG_KINDS[number];"
|
|
1791
|
+
},
|
|
1792
|
+
{
|
|
1793
|
+
"name": "IapCatalogItem",
|
|
1794
|
+
"kind": "interface",
|
|
1795
|
+
"jsdoc": "",
|
|
1796
|
+
"decl": "export interface IapCatalogItem {\n reference: CatalogReference;\n /** The screen's own formatted commerce price; shown only on an explicit regular route. */\n commerceDisplayPrice?: string;\n}"
|
|
1797
|
+
},
|
|
1798
|
+
{
|
|
1799
|
+
"name": "IapCatalogScope",
|
|
1800
|
+
"kind": "interface",
|
|
1801
|
+
"jsdoc": "",
|
|
1802
|
+
"decl": "export interface IapCatalogScope {\n appId: string;\n bundleId: string;\n /** Wix metaSiteId. Omit it to use the connected mapping source's bound site. */\n siteId?: string;\n contextRevision?: string;\n}"
|
|
1803
|
+
},
|
|
1804
|
+
{
|
|
1805
|
+
"name": "IapCatalogInput",
|
|
1806
|
+
"kind": "interface",
|
|
1807
|
+
"jsdoc": "",
|
|
1808
|
+
"decl": "export interface IapCatalogInput {\n scope: IapCatalogScope;\n /** The host's own platform (from `@himalaya/app-info`); only \"ios\" resolves Apple prices. */\n platform: string;\n items: IapCatalogItem[];\n}"
|
|
1809
|
+
},
|
|
1810
|
+
{
|
|
1811
|
+
"name": "IapPaymentRoute",
|
|
1812
|
+
"kind": "type",
|
|
1813
|
+
"jsdoc": "/** `disabled` means no backend is connected: the screen's existing commerce behavior applies. */",
|
|
1814
|
+
"decl": "export type IapPaymentRoute = \"disabled\" | \"regular\" | \"appleIap\" | \"resolving\" | \"unavailable\";"
|
|
1815
|
+
},
|
|
1816
|
+
{
|
|
1817
|
+
"name": "IapItemView",
|
|
1818
|
+
"kind": "interface",
|
|
1819
|
+
"jsdoc": "/** Serializable per-item state a screen binds. Carries no credential, transaction or entitlement. */",
|
|
1820
|
+
"decl": "export interface IapItemView {\n reference: CatalogReference;\n route: IapPaymentRoute;\n /** Apple: StoreKit's string verbatim. Regular: the caller's commerce price. Otherwise \"\". */\n displayPrice: string;\n reason?: PricingUnavailableReason;\n retryable: boolean;\n /** True only when the existing commerce checkout may run: disabled or an explicit regular route. */\n canStartRegularCheckout: boolean;\n /** Apple purchasing is not available until a backend verifies transactions. */\n purchasable: false;\n}"
|
|
1821
|
+
},
|
|
1822
|
+
{
|
|
1823
|
+
"name": "IapCheckoutDecision",
|
|
1824
|
+
"kind": "interface",
|
|
1825
|
+
"jsdoc": "",
|
|
1826
|
+
"decl": "export interface IapCheckoutDecision {\n allowed: boolean;\n route: IapPaymentRoute;\n}"
|
|
1827
|
+
},
|
|
1828
|
+
{
|
|
1829
|
+
"name": "IapMappingSource",
|
|
1830
|
+
"kind": "interface",
|
|
1831
|
+
"jsdoc": "/** A deployed Wix→Apple mapping backend. Never supplied by a screen. */",
|
|
1832
|
+
"decl": "export interface IapMappingSource {\n mapping: PricingMappingProvider;\n /** The site this backend serves, used when the caller passes no site. */\n siteId?: string;\n}"
|
|
1833
|
+
},
|
|
1834
|
+
{
|
|
1835
|
+
"name": "IapPollDirective",
|
|
1836
|
+
"kind": "interface",
|
|
1837
|
+
"jsdoc": "",
|
|
1838
|
+
"decl": "export interface IapPollDirective {\n id: string;\n action: string;\n intervalMs: number;\n immediate: boolean;\n}"
|
|
1839
|
+
},
|
|
1840
|
+
{
|
|
1841
|
+
"name": "IapCatalogPricing",
|
|
1842
|
+
"kind": "interface",
|
|
1843
|
+
"jsdoc": "",
|
|
1844
|
+
"decl": "export interface IapCatalogPricing {\n readonly enabled: boolean;\n /** Batched mapping + Apple lookup for the items a screen shows. */\n load(input: IapCatalogInput): Promise<void>;\n refresh(): Promise<void>;\n view(reference: CatalogReference): IapItemView;\n /** May a commerce flow START now? Re-read immediately before the first mutation: the route as it\n * is now, so a refresh in flight (`resolving`) blocks. */\n checkoutGuard(reference: CatalogReference): IapCheckoutDecision;\n /** May a flow that already STARTED take its next commerce step (e.g. pay after a join)? While no\n * new answer exists (a refresh in flight, or a mapping backend that did not answer) it uses the\n * route the last settled lookup gave for the current scope, so a routine refresh cannot strand a\n * regular checkout halfway; any new answer other than regular stops it. */\n continueCheckoutGuard(reference: CatalogReference): IapCheckoutDecision;\n /** The host-scheduled visible refresh; undefined while disabled. */\n pollDirective(action: string): IapPollDirective | undefined;\n dispose(): void;\n}"
|
|
1845
|
+
},
|
|
1846
|
+
{
|
|
1847
|
+
"name": "IapPriceCopy",
|
|
1848
|
+
"kind": "interface",
|
|
1849
|
+
"jsdoc": "/** Labels come from the screen's copy pack; the shared layer only chooses between them. */",
|
|
1850
|
+
"decl": "export interface IapPriceCopy {\n loading: string;\n unavailable: string;\n oneTimePurchase: string;\n purchaseUnavailable: string;\n}"
|
|
1851
|
+
},
|
|
1852
|
+
{
|
|
1853
|
+
"name": "IapPriceFields",
|
|
1854
|
+
"kind": "interface",
|
|
1855
|
+
"jsdoc": "",
|
|
1856
|
+
"decl": "export interface IapPriceFields {\n priceText: string;\n priceNote: string;\n priceLoading: boolean;\n hidePriceRetry: boolean;\n actionDisabled: boolean;\n actionNote: string;\n}"
|
|
1857
|
+
},
|
|
1858
|
+
{
|
|
1859
|
+
"name": "createIapCatalogPricing",
|
|
1860
|
+
"kind": "function",
|
|
1861
|
+
"jsdoc": "/** One screen worker's catalog pricing. Activation comes only from the connected mapping\n * source, so a screen cannot turn Apple prices on by supplying its own mapping. */",
|
|
1862
|
+
"decl": "export function createIapCatalogPricing(options: { onChange?(): void } = {}): IapCatalogPricing;"
|
|
1863
|
+
},
|
|
1864
|
+
{
|
|
1865
|
+
"name": "iapPriceFields",
|
|
1866
|
+
"kind": "function",
|
|
1867
|
+
"jsdoc": "/** Bindable display state for one item. A screen spreads these into its own state and binds\n * them; the labels stay in the screen's copy pack. */",
|
|
1868
|
+
"decl": "export function iapPriceFields(view: IapItemView, copy: IapPriceCopy): IapPriceFields;"
|
|
1869
|
+
},
|
|
1870
|
+
{
|
|
1871
|
+
"name": "onlineProgramItem",
|
|
1872
|
+
"kind": "function",
|
|
1873
|
+
"jsdoc": "/** An Online Programs program as a shared IAP catalog item. A program without a Wix ID cannot be\n * mapped (a title fallback is not an identity), so it has no item and its screen shows no price. */",
|
|
1874
|
+
"decl": "export function onlineProgramItem(program: { id?: string | null }, commerceDisplayPrice?: string): IapCatalogItem | undefined;"
|
|
1875
|
+
},
|
|
1780
1876
|
{
|
|
1781
1877
|
"name": "ProductType",
|
|
1782
1878
|
"kind": "type",
|
|
@@ -479,6 +479,15 @@ This is what makes the fail-closed capability gates readable. `local_database: e
|
|
|
479
479
|
"X" is not declared in the app's dataSchema` is a message written to be read — before
|
|
480
480
|
#2288 an author only ever saw an eternal spinner.
|
|
481
481
|
|
|
482
|
+
### Show App Store prices for catalog items
|
|
483
|
+
|
|
484
|
+
A screen that shows or sells a program, product or plan that may be an App Store in-app purchase
|
|
485
|
+
does not look up mappings or Apple prices itself. It uses the shared catalog layer
|
|
486
|
+
(`createIapCatalogPricing` in `@himalaya/iap`), which also hands it the `_poll` directive below and
|
|
487
|
+
the guards a commerce flow must call: `checkoutGuard` before it starts, `continueCheckoutGuard`
|
|
488
|
+
before each later step. The adoption guide, with a compiled and tested
|
|
489
|
+
worker example, is `himi docs iap-pricing` ([iap-pricing-for-screens.md](iap-pricing-for-screens.md)).
|
|
490
|
+
|
|
482
491
|
### Refresh on a cadence with `_poll` (the worker has no clock)
|
|
483
492
|
|
|
484
493
|
A worker is a pure reducer with **no `setTimeout` and no `setInterval`** — its sandbox
|
package/guides/wix-api-usage.md
CHANGED
|
@@ -281,6 +281,17 @@ If a record genuinely carries no currency, fall back to a constant your app owns
|
|
|
281
281
|
site-properties read that will 403. An owner-scope app (a dashboard/owner surface holding a `User`
|
|
282
282
|
instance) *can* read the site currency; a consumer/visitor app cannot.
|
|
283
283
|
|
|
284
|
+
### Items that may be sold as App Store in-app purchases
|
|
285
|
+
|
|
286
|
+
A program, product or plan the owner registered with `himi store iap` may have to be sold through
|
|
287
|
+
the App Store instead of your commerce flow. Do not decide that in your worker. Pass the items on
|
|
288
|
+
screen to the shared `createIapCatalogPricing` from `@himalaya/iap` (`himi docs iap-pricing`): it
|
|
289
|
+
looks up each item's route in one batch, fetches Apple's localized price, and returns what to show
|
|
290
|
+
and whether your existing checkout may run. Bind its fields, call `checkoutGuard` right before a
|
|
291
|
+
commerce flow starts and `continueCheckoutGuard` before each later step of it, and declare `iap` and
|
|
292
|
+
`app_info`. While Wix has not connected the mapping service
|
|
293
|
+
it reports `disabled` and your screen behaves exactly as before.
|
|
294
|
+
|
|
284
295
|
## Testing
|
|
285
296
|
|
|
286
297
|
Fake the bridge — stub `globalThis.himalaya.wixApi.call` to return a canned parsed object per `(method, path)` — or use
|
|
@@ -124,7 +124,7 @@ for **behavior** is the website; design authority for **layout** is
|
|
|
124
124
|
|
|
125
125
|
| Website | Mobile |
|
|
126
126
|
|---|---|
|
|
127
|
-
| Vertical (book, shop, events, forum, members, blog) | Template/overlay screen; bind with `himi sdk` + live `@himalaya/wix
|
|
127
|
+
| Vertical (book, shop, events, forum, members, blog) | Template/overlay screen; bind with `himi sdk` + live `@himalaya/wix-*`. Any screen showing a digital item's price (program, product, plan) also uses the shared IAP pricing layer — `himi docs iap-pricing` — whether it came from a template or you wrote it |
|
|
128
128
|
| Custom content / CMS / forms | One T5 screen per *job*; data = adopted CMS (`wixId`) or a form worker (`himi docs recipe-form-submit`) |
|
|
129
129
|
| Velo page modules | Briefing only: field names, collection ids, submit flow. **Rewrite** as a worker. Never copy Velo JS. Site HTTP functions (`/_functions/`) are a separate API: `himi docs wix-site-http-functions` / `himi site function` — analyze never invokes them |
|
|
130
130
|
| Lightboxes, popups, multi-column editors | Push screen or sheet — not a ported lightbox |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/himalaya-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1245.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "himi — the Himalaya authoring CLI. Build a Tier-5 content package on your own machine, push it to the production serve as a draft, preview it on web + Himi Player, and read worker logs back — the remote dev loop. Wraps @himalaya/serve-cli for transport.",
|
|
6
6
|
"keywords": [
|
|
@@ -67,5 +67,5 @@
|
|
|
67
67
|
"artifactId": "himalaya-cli"
|
|
68
68
|
}
|
|
69
69
|
},
|
|
70
|
-
"falconPackageHash": "
|
|
70
|
+
"falconPackageHash": "09f05672ac223575259bf3c6b41d9dbdb7ad92bd5de82076c956864c"
|
|
71
71
|
}
|
|
@@ -52293,7 +52293,7 @@ const SCREENS = runtimeSetting({
|
|
|
52293
52293
|
"package": "himalaya",
|
|
52294
52294
|
"type": "Button",
|
|
52295
52295
|
"props": {
|
|
52296
|
-
"title": "Sign out",
|
|
52296
|
+
"title": "Sign out of Wix",
|
|
52297
52297
|
"style": "secondary",
|
|
52298
52298
|
"fullWidth": true,
|
|
52299
52299
|
"action": {
|