@wix/himalaya-cli 0.1240.0 → 0.1241.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/index.d.ts +2 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/pricing-contract.d.ts +81 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/pricing.d.ts +3 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap.mjs +151 -16
- package/dist/cli.mjs +2 -0
- package/guides/iap-pricing.md +225 -0
- package/guides/index.json +6 -0
- package/guides/sdk-reference.json +72 -0
- package/package.json +2 -2
package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { TransactionStore } from "./delivery-contract.js";
|
|
2
2
|
export * from "./delivery-contract.js";
|
|
3
3
|
export * from "./delivery.js";
|
|
4
|
+
export * from "./pricing-contract.js";
|
|
5
|
+
export * from "./pricing.js";
|
|
4
6
|
/** The three store product categories. */
|
|
5
7
|
export type ProductType = "subscription" | "nonConsumable" | "consumable";
|
|
6
8
|
export interface Product {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { Product } from "./index.js";
|
|
2
|
+
export interface CatalogReference {
|
|
3
|
+
kind: string;
|
|
4
|
+
id: string;
|
|
5
|
+
}
|
|
6
|
+
/** Opaque context revision changes whenever a mapping-dependent audience changes. */
|
|
7
|
+
export interface PricingScope {
|
|
8
|
+
appId: string;
|
|
9
|
+
bundleId: string;
|
|
10
|
+
siteId: string;
|
|
11
|
+
contextRevision?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface PricingItem {
|
|
14
|
+
reference: CatalogReference;
|
|
15
|
+
/** Owned/formatted by the commerce caller; used only for an explicit regular route. */
|
|
16
|
+
commerceDisplayPrice?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface PricingInput {
|
|
19
|
+
scope: PricingScope;
|
|
20
|
+
/** Trusted host platform; only "ios" performs Apple lookup. */
|
|
21
|
+
platform: string;
|
|
22
|
+
items: PricingItem[];
|
|
23
|
+
}
|
|
24
|
+
export type PricingMapping = {
|
|
25
|
+
reference: CatalogReference;
|
|
26
|
+
} & ({
|
|
27
|
+
route: "appleIap";
|
|
28
|
+
productId: string;
|
|
29
|
+
mappingRevision: string;
|
|
30
|
+
} | {
|
|
31
|
+
route: "regular";
|
|
32
|
+
} | {
|
|
33
|
+
route: "unavailable";
|
|
34
|
+
});
|
|
35
|
+
/** Internal client port, not an agreed/deployed backend API. Never requires purchase context. */
|
|
36
|
+
export interface PricingMappingProvider {
|
|
37
|
+
resolve(input: {
|
|
38
|
+
scope: PricingScope;
|
|
39
|
+
references: CatalogReference[];
|
|
40
|
+
}): Promise<PricingMapping[]>;
|
|
41
|
+
}
|
|
42
|
+
export type PricingUnavailableReason = "invalidInput" | "mappingUnavailable" | "mappingUnresolved" | "unsupportedPlatform" | "unsupportedProductType" | "priceUnavailable" | "commercePriceUnavailable";
|
|
43
|
+
export type ItemPriceState = {
|
|
44
|
+
reference: CatalogReference;
|
|
45
|
+
} & ({
|
|
46
|
+
status: "loading";
|
|
47
|
+
} | {
|
|
48
|
+
status: "ready";
|
|
49
|
+
source: "appleIap";
|
|
50
|
+
displayPrice: string;
|
|
51
|
+
productId: string;
|
|
52
|
+
mappingRevision: string;
|
|
53
|
+
} | {
|
|
54
|
+
status: "ready";
|
|
55
|
+
source: "regular";
|
|
56
|
+
displayPrice: string;
|
|
57
|
+
} | {
|
|
58
|
+
status: "unavailable";
|
|
59
|
+
reason: PricingUnavailableReason;
|
|
60
|
+
retryable: boolean;
|
|
61
|
+
});
|
|
62
|
+
/** Contains no credentials, transaction proof, entitlement, or purchase authorization. */
|
|
63
|
+
export interface PricingState {
|
|
64
|
+
revision: number;
|
|
65
|
+
scope: PricingScope | null;
|
|
66
|
+
items: ItemPriceState[];
|
|
67
|
+
}
|
|
68
|
+
export interface PricingDependencies {
|
|
69
|
+
mapping: PricingMappingProvider;
|
|
70
|
+
getProducts(productIds: string[]): Promise<Product[]>;
|
|
71
|
+
onState?(state: PricingState): void;
|
|
72
|
+
}
|
|
73
|
+
export interface PricingController {
|
|
74
|
+
/** Fresh lookup; identical in-flight input is shared. Completed prices are never cached. */
|
|
75
|
+
load(input: PricingInput): Promise<void>;
|
|
76
|
+
/** Reloads the last input, joining a matching lookup already in progress. */
|
|
77
|
+
refresh(): Promise<void>;
|
|
78
|
+
getState(): PricingState;
|
|
79
|
+
/** Clears state and suppresses subsequent calls/publications; cannot cancel native I/O. */
|
|
80
|
+
dispose(): void;
|
|
81
|
+
}
|
package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/pricing.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { PricingController, PricingDependencies } from "./pricing-contract.js";
|
|
2
|
+
/** Creates one worker's read-only pricing controller with injected mapping, store lookup, and state publication. */
|
|
3
|
+
export declare function createPricingController(deps: PricingDependencies): PricingController;
|
|
@@ -25,7 +25,7 @@ function createPurchaseCoordinator(deps) {
|
|
|
25
25
|
let outcome = "idle";
|
|
26
26
|
let purchasing = false;
|
|
27
27
|
let signInPrompt = false;
|
|
28
|
-
let
|
|
28
|
+
let unavailable2 = null;
|
|
29
29
|
let pendingApproval = null;
|
|
30
30
|
let errorCode = null;
|
|
31
31
|
let errorFromReconcile = false;
|
|
@@ -61,7 +61,7 @@ function createPurchaseCoordinator(deps) {
|
|
|
61
61
|
if (disposed || !sessionMatches()) return { phase: "idle", errorCode: null, items: [], unfinishedCount: null, nextWakeAt: null };
|
|
62
62
|
const snapshot = [...items.values()].map((item) => ({ ...item.state }));
|
|
63
63
|
const due = snapshot.map((item) => item.nextAttemptAt).filter((at) => at !== null);
|
|
64
|
-
return { phase: currentPhase(), errorCode: errorCode ??
|
|
64
|
+
return { phase: currentPhase(), errorCode: errorCode ?? unavailable2?.code ?? null, items: snapshot, unfinishedCount, nextWakeAt: due.length > 0 ? Math.min(...due) : null };
|
|
65
65
|
}
|
|
66
66
|
function dispose() {
|
|
67
67
|
disposed = true;
|
|
@@ -73,7 +73,7 @@ function createPurchaseCoordinator(deps) {
|
|
|
73
73
|
setError(null, false);
|
|
74
74
|
outcome = "idle";
|
|
75
75
|
pendingApproval = null;
|
|
76
|
-
|
|
76
|
+
unavailable2 = null;
|
|
77
77
|
releaseSignIn();
|
|
78
78
|
if (session.kind !== "member") {
|
|
79
79
|
requireSignIn();
|
|
@@ -132,7 +132,7 @@ function createPurchaseCoordinator(deps) {
|
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
134
|
function clearReconcileProblem() {
|
|
135
|
-
if (
|
|
135
|
+
if (unavailable2?.from === "reconcile") unavailable2 = null;
|
|
136
136
|
if (errorFromReconcile) setError(null, false);
|
|
137
137
|
}
|
|
138
138
|
async function runRetry() {
|
|
@@ -297,7 +297,7 @@ function createPurchaseCoordinator(deps) {
|
|
|
297
297
|
outcome = "idle";
|
|
298
298
|
purchasing = false;
|
|
299
299
|
signInPrompt = false;
|
|
300
|
-
|
|
300
|
+
unavailable2 = null;
|
|
301
301
|
pendingApproval = null;
|
|
302
302
|
unfinishedCount = null;
|
|
303
303
|
setError(null, false);
|
|
@@ -355,8 +355,8 @@ function createPurchaseCoordinator(deps) {
|
|
|
355
355
|
}
|
|
356
356
|
function failStore(code, fromReconcile) {
|
|
357
357
|
setError(code, fromReconcile);
|
|
358
|
-
if (UNAVAILABLE_CODES.has(code) &&
|
|
359
|
-
|
|
358
|
+
if (UNAVAILABLE_CODES.has(code) && unavailable2?.from !== "purchase") {
|
|
359
|
+
unavailable2 = { from: fromReconcile ? "reconcile" : "purchase", code };
|
|
360
360
|
}
|
|
361
361
|
publish();
|
|
362
362
|
}
|
|
@@ -373,9 +373,9 @@ function createPurchaseCoordinator(deps) {
|
|
|
373
373
|
}
|
|
374
374
|
function currentPhase() {
|
|
375
375
|
if (purchasing) return "purchasing";
|
|
376
|
-
if (
|
|
376
|
+
if (unavailable2?.from === "purchase") return "unavailable";
|
|
377
377
|
if (signInPrompt) return "signInRequired";
|
|
378
|
-
if (
|
|
378
|
+
if (unavailable2?.from === "reconcile") return "unavailable";
|
|
379
379
|
return outcome;
|
|
380
380
|
}
|
|
381
381
|
function setError(code, fromReconcile) {
|
|
@@ -514,6 +514,140 @@ function createSerialQueue() {
|
|
|
514
514
|
};
|
|
515
515
|
}
|
|
516
516
|
|
|
517
|
+
// ../../core/sdk/iap/src/pricing.ts
|
|
518
|
+
function createPricingController(deps) {
|
|
519
|
+
let state = { revision: 0, scope: null, items: [] };
|
|
520
|
+
let lastInput;
|
|
521
|
+
let active;
|
|
522
|
+
let disposed = false;
|
|
523
|
+
function load(input) {
|
|
524
|
+
if (disposed) return Promise.resolve();
|
|
525
|
+
const captured = captureInput(input);
|
|
526
|
+
const key = JSON.stringify(captured);
|
|
527
|
+
if (active?.key === key) return active.promise;
|
|
528
|
+
lastInput = captured;
|
|
529
|
+
const revision = state.revision + 1;
|
|
530
|
+
let complete;
|
|
531
|
+
const promise = new Promise((resolve) => {
|
|
532
|
+
complete = resolve;
|
|
533
|
+
});
|
|
534
|
+
active = { key, promise };
|
|
535
|
+
publish({ revision, scope: captured.scope, items: captured.items.map((item) => ({ reference: item.reference, status: "loading" })) });
|
|
536
|
+
void run(captured, revision).finally(complete);
|
|
537
|
+
return promise;
|
|
538
|
+
}
|
|
539
|
+
async function run(input, revision) {
|
|
540
|
+
const current = () => !disposed && state.revision === revision;
|
|
541
|
+
if (!current()) return;
|
|
542
|
+
const items = await resolvePrices(input, deps, current);
|
|
543
|
+
if (!current()) return;
|
|
544
|
+
active = void 0;
|
|
545
|
+
publish({ revision, scope: input.scope, items });
|
|
546
|
+
}
|
|
547
|
+
function publish(next) {
|
|
548
|
+
state = next;
|
|
549
|
+
try {
|
|
550
|
+
deps.onState?.(getState());
|
|
551
|
+
} catch {
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
function getState() {
|
|
555
|
+
return {
|
|
556
|
+
...state,
|
|
557
|
+
scope: state.scope ? { ...state.scope } : null,
|
|
558
|
+
items: state.items.map((item) => ({ ...item, reference: { ...item.reference } }))
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
function dispose() {
|
|
562
|
+
disposed = true;
|
|
563
|
+
active = void 0;
|
|
564
|
+
lastInput = void 0;
|
|
565
|
+
state = { revision: state.revision + 1, scope: null, items: [] };
|
|
566
|
+
}
|
|
567
|
+
return { load, refresh: () => lastInput ? load(lastInput) : Promise.resolve(), getState, dispose };
|
|
568
|
+
}
|
|
569
|
+
function captureInput(input) {
|
|
570
|
+
const scope = {
|
|
571
|
+
appId: input.scope.appId,
|
|
572
|
+
bundleId: input.scope.bundleId,
|
|
573
|
+
siteId: input.scope.siteId,
|
|
574
|
+
...input.scope.contextRevision === void 0 ? {} : { contextRevision: input.scope.contextRevision }
|
|
575
|
+
};
|
|
576
|
+
const items = /* @__PURE__ */ new Map();
|
|
577
|
+
for (const item of input.items) {
|
|
578
|
+
const reference = { kind: item.reference.kind, id: item.reference.id };
|
|
579
|
+
const key = JSON.stringify([reference.kind, reference.id]);
|
|
580
|
+
if (!items.has(key)) items.set(key, { reference, ...item.commerceDisplayPrice === void 0 ? {} : { commerceDisplayPrice: item.commerceDisplayPrice } });
|
|
581
|
+
}
|
|
582
|
+
return { scope, platform: input.platform, items: [...items.values()] };
|
|
583
|
+
}
|
|
584
|
+
async function resolvePrices(input, deps, current) {
|
|
585
|
+
if (input.items.length === 0) return [];
|
|
586
|
+
if (![input.scope.appId, input.scope.bundleId, input.scope.siteId].every(nonempty)) {
|
|
587
|
+
return input.items.map((item) => unavailable(item.reference, "invalidInput"));
|
|
588
|
+
}
|
|
589
|
+
const mappings = await resolveMappings(input, deps);
|
|
590
|
+
if (!current()) return [];
|
|
591
|
+
if (mappings === null) return input.items.map((item) => unavailable(item.reference, "mappingUnavailable"));
|
|
592
|
+
const routes = input.items.map((item) => resolveRoute(item, mappings, input.platform));
|
|
593
|
+
const ids = [...new Set(routes.flatMap((route) => route.status === "loading" ? [route.mapping.productId] : []))];
|
|
594
|
+
const products = ids.length ? await lookupProducts(ids, deps) : [];
|
|
595
|
+
return routes.map((route) => route.status === "loading" ? applePrice(route.mapping, products) : route);
|
|
596
|
+
}
|
|
597
|
+
async function resolveMappings(input, deps) {
|
|
598
|
+
try {
|
|
599
|
+
const references = input.items.filter((item) => nonempty(item.reference.kind) && nonempty(item.reference.id)).map((item) => ({ ...item.reference }));
|
|
600
|
+
if (!references.length) return [];
|
|
601
|
+
const result = await deps.mapping.resolve({ scope: { ...input.scope }, references });
|
|
602
|
+
return Array.isArray(result) ? result : [];
|
|
603
|
+
} catch {
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
async function lookupProducts(ids, deps) {
|
|
608
|
+
try {
|
|
609
|
+
const products = await deps.getProducts(ids);
|
|
610
|
+
return Array.isArray(products) ? products : [];
|
|
611
|
+
} catch {
|
|
612
|
+
return [];
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
function resolveRoute(item, mappings, platform) {
|
|
616
|
+
if (!nonempty(item.reference.kind) || !nonempty(item.reference.id)) return unavailable(item.reference, "invalidInput");
|
|
617
|
+
const matches = mappings.filter((value) => isObject2(value) && sameReference(value.reference, item.reference));
|
|
618
|
+
if (matches.length !== 1) return unavailable(item.reference, "mappingUnresolved");
|
|
619
|
+
const mapping = matches[0];
|
|
620
|
+
if (mapping.route === "regular") return regularPrice(item);
|
|
621
|
+
if (mapping.route !== "appleIap" || !nonempty(mapping.productId) || mapping.productId.includes(",") || !nonempty(mapping.mappingRevision)) {
|
|
622
|
+
return unavailable(item.reference, "mappingUnresolved");
|
|
623
|
+
}
|
|
624
|
+
if (platform !== "ios") return unavailable(item.reference, "unsupportedPlatform");
|
|
625
|
+
return { status: "loading", mapping: { reference: item.reference, route: "appleIap", productId: mapping.productId, mappingRevision: mapping.mappingRevision } };
|
|
626
|
+
}
|
|
627
|
+
function regularPrice(item) {
|
|
628
|
+
return nonempty(item.commerceDisplayPrice) ? { reference: item.reference, status: "ready", source: "regular", displayPrice: item.commerceDisplayPrice } : unavailable(item.reference, "commercePriceUnavailable");
|
|
629
|
+
}
|
|
630
|
+
function applePrice(mapping, products) {
|
|
631
|
+
const matches = products.filter((value) => isObject2(value) && value.id === mapping.productId);
|
|
632
|
+
const product = matches[0];
|
|
633
|
+
if (matches.length !== 1 || !nonempty(product?.displayPrice)) return unavailable(mapping.reference, "priceUnavailable");
|
|
634
|
+
if (product?.type !== "nonConsumable") return unavailable(mapping.reference, "unsupportedProductType");
|
|
635
|
+
return {
|
|
636
|
+
reference: mapping.reference,
|
|
637
|
+
status: "ready",
|
|
638
|
+
source: "appleIap",
|
|
639
|
+
productId: mapping.productId,
|
|
640
|
+
mappingRevision: mapping.mappingRevision,
|
|
641
|
+
displayPrice: product.displayPrice
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
function unavailable(reference, reason) {
|
|
645
|
+
return { reference, status: "unavailable", reason, retryable: !["invalidInput", "unsupportedPlatform", "unsupportedProductType", "commercePriceUnavailable"].includes(reason) };
|
|
646
|
+
}
|
|
647
|
+
var isObject2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
648
|
+
var nonempty = (value) => typeof value === "string" && value.trim().length > 0;
|
|
649
|
+
var sameReference = (value, reference) => isObject2(value) && value.kind === reference.kind && value.id === reference.id;
|
|
650
|
+
|
|
517
651
|
// ../../core/sdk/iap/src/index.ts
|
|
518
652
|
async function getPaywallConfig() {
|
|
519
653
|
const r = await dispatch("capability.iap.getPaywallConfig");
|
|
@@ -561,7 +695,7 @@ function normalizeStatus(s) {
|
|
|
561
695
|
}
|
|
562
696
|
var version = 2;
|
|
563
697
|
async function purchaseForVerification(input) {
|
|
564
|
-
if (!
|
|
698
|
+
if (!nonempty2(input?.productId) || !isUUID(input?.appAccountToken)) return failure("invalidArgument");
|
|
565
699
|
const result = await managedDispatch(() => dispatch("capability.iap.purchaseForVerification", input));
|
|
566
700
|
if (result?.status === "purchased" && isAppleTransaction(result.transaction)) {
|
|
567
701
|
return { status: "purchased", transaction: decodeTransaction(result.transaction) };
|
|
@@ -577,7 +711,7 @@ async function getUnfinishedTransactions() {
|
|
|
577
711
|
return decodeFailure(result);
|
|
578
712
|
}
|
|
579
713
|
async function finishTransaction(input) {
|
|
580
|
-
if (!
|
|
714
|
+
if (!nonempty2(input?.transactionId)) return failure("invalidArgument");
|
|
581
715
|
const result = await managedDispatch(() => dispatch("capability.iap.finishTransaction", input));
|
|
582
716
|
if (result?.status === "finished" || result?.status === "notFound") return { status: result.status };
|
|
583
717
|
return decodeFailure(result);
|
|
@@ -585,13 +719,13 @@ async function finishTransaction(input) {
|
|
|
585
719
|
var appleManagedStore = { purchaseForVerification, getUnfinishedTransactions, finishTransaction };
|
|
586
720
|
var transactionErrors = /* @__PURE__ */ new Set(["unsupported", "unavailable", "invalidArgument", "invalidResponse", "notEnabled", "unsupportedProductType", "unknownProduct", "verificationFailed", "persistenceFailed", "ownershipConflict", "storeError", "busy"]);
|
|
587
721
|
var failure = (code) => ({ status: "failed", code });
|
|
588
|
-
var
|
|
722
|
+
var nonempty2 = (value) => typeof value === "string" && value.trim().length > 0;
|
|
589
723
|
var isUUID = (value) => typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value) && value.length === 36;
|
|
590
|
-
var
|
|
724
|
+
var isObject3 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
591
725
|
async function managedDispatch(invoke) {
|
|
592
726
|
try {
|
|
593
727
|
const value = await invoke();
|
|
594
|
-
return
|
|
728
|
+
return isObject3(value) ? value : null;
|
|
595
729
|
} catch {
|
|
596
730
|
return failure("unavailable");
|
|
597
731
|
}
|
|
@@ -600,8 +734,8 @@ function decodeFailure(value) {
|
|
|
600
734
|
return value?.status === "failed" && transactionErrors.has(value.code) ? failure(value.code) : failure("invalidResponse");
|
|
601
735
|
}
|
|
602
736
|
function isAppleTransaction(value) {
|
|
603
|
-
if (!
|
|
604
|
-
return value.store === "apple" && typeof value.signedTransaction === "string" && /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(value.signedTransaction) &&
|
|
737
|
+
if (!isObject3(value)) return false;
|
|
738
|
+
return value.store === "apple" && typeof value.signedTransaction === "string" && /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(value.signedTransaction) && nonempty2(value.transactionId) && nonempty2(value.originalTransactionId) && nonempty2(value.productId) && (value.appAccountToken === null || isUUID(value.appAccountToken)) && ["production", "sandbox", "xcode", "unknown"].includes(value.environment);
|
|
605
739
|
}
|
|
606
740
|
function decodeTransaction(value) {
|
|
607
741
|
const { store, signedTransaction, transactionId, originalTransactionId, productId, appAccountToken, environment } = value;
|
|
@@ -612,6 +746,7 @@ export {
|
|
|
612
746
|
MAX_RETRY_DELAY_MS,
|
|
613
747
|
appleManagedStore,
|
|
614
748
|
consume,
|
|
749
|
+
createPricingController,
|
|
615
750
|
createPurchaseCoordinator,
|
|
616
751
|
entitlements,
|
|
617
752
|
finishTransaction,
|
package/dist/cli.mjs
CHANGED
|
@@ -6878,6 +6878,7 @@ function resolveTranslation(value, catalog, sourceCatalog) {
|
|
|
6878
6878
|
var BINDING_SIGILS, BINDING_SIGIL_RE, TRANSLATION_PREFIX, TRANSLATION_KEY_RE;
|
|
6879
6879
|
var init_bindings = __esm({
|
|
6880
6880
|
"../../core/ts/src/bindings.ts"() {
|
|
6881
|
+
"use strict";
|
|
6881
6882
|
BINDING_SIGILS = ["service", "flag", "item", "t"];
|
|
6882
6883
|
BINDING_SIGIL_RE = new RegExp(
|
|
6883
6884
|
`^\\$(?:${BINDING_SIGILS.filter((s2) => s2 !== "t").join("|")})\\b|^\\$t:`
|
|
@@ -13431,6 +13432,7 @@ function resolveIdiom(tokens2, ctx = {}) {
|
|
|
13431
13432
|
var IDIOM_CONTEXTS, UNSAFE_KEYS;
|
|
13432
13433
|
var init_idiom = __esm({
|
|
13433
13434
|
"../../core/ts/src/idiom.ts"() {
|
|
13435
|
+
"use strict";
|
|
13434
13436
|
IDIOM_CONTEXTS = {
|
|
13435
13437
|
ios: { native: ["ios"] },
|
|
13436
13438
|
android: { native: ["material"] },
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# Shared Apple IAP pricing
|
|
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.
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
// pricing-worker.ts
|
|
63
|
+
import { defineService } from "@himalaya/state";
|
|
64
|
+
import {
|
|
65
|
+
createPricingController, getProducts,
|
|
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
|
+
}
|
|
75
|
+
|
|
76
|
+
interface PriceRow {
|
|
77
|
+
id: string;
|
|
78
|
+
status: "loading" | "ready" | "unavailable";
|
|
79
|
+
displayPrice: string;
|
|
80
|
+
message: string;
|
|
81
|
+
retryable: boolean;
|
|
82
|
+
loading: boolean;
|
|
83
|
+
hideLoading: boolean;
|
|
84
|
+
hideRetry: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface PriceServiceState {
|
|
88
|
+
pricing: PricingState;
|
|
89
|
+
rows: PriceRow[];
|
|
90
|
+
canPurchase: boolean;
|
|
91
|
+
purchaseDisabled: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function registerPricingService(
|
|
95
|
+
mapping: PricingMappingProvider, initialInput: PricingInput, copy: PriceCopy,
|
|
96
|
+
) {
|
|
97
|
+
let input = initialInput;
|
|
98
|
+
let disposed = false;
|
|
99
|
+
const controller = createPricingController({
|
|
100
|
+
mapping, getProducts,
|
|
101
|
+
onState: () => { void service.publish(); },
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
function project(pricing: PricingState): PriceRow[] {
|
|
105
|
+
return pricing.items.map(item => ({
|
|
106
|
+
id: JSON.stringify([item.reference.kind, item.reference.id]),
|
|
107
|
+
status: item.status,
|
|
108
|
+
displayPrice: item.status === "ready" ? item.displayPrice : "",
|
|
109
|
+
message: item.status === "loading" ? copy.loading : item.status === "unavailable"
|
|
110
|
+
? copy.unavailable : item.source === "appleIap" ? copy.apple : copy.regular,
|
|
111
|
+
retryable: item.status === "unavailable" && item.retryable,
|
|
112
|
+
loading: item.status === "loading",
|
|
113
|
+
hideLoading: item.status !== "loading",
|
|
114
|
+
hideRetry: item.status !== "unavailable" || !item.retryable,
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const actions = {
|
|
119
|
+
start: (state: PriceServiceState) => ({ ...state, _poll: {
|
|
120
|
+
id: "pricing", action: "pricing.reload", intervalMs: 60_000, immediate: true,
|
|
121
|
+
} }),
|
|
122
|
+
publish: (state: PriceServiceState) => {
|
|
123
|
+
const pricing = controller.getState();
|
|
124
|
+
return { ...state, pricing, rows: project(pricing), canPurchase: false, purchaseDisabled: true };
|
|
125
|
+
},
|
|
126
|
+
reload: async () => {
|
|
127
|
+
if (!disposed) await controller.load(input);
|
|
128
|
+
// Publication uses a separate reducer; returning captured state would race.
|
|
129
|
+
return undefined;
|
|
130
|
+
},
|
|
131
|
+
retry: async () => {
|
|
132
|
+
if (!disposed) await controller.refresh();
|
|
133
|
+
return undefined;
|
|
134
|
+
},
|
|
135
|
+
setInput: async (_state: PriceServiceState, next: PricingInput) => {
|
|
136
|
+
if (!disposed) { input = next; await controller.load(input); }
|
|
137
|
+
return undefined;
|
|
138
|
+
},
|
|
139
|
+
dispose: (state: PriceServiceState) => {
|
|
140
|
+
disposed = true;
|
|
141
|
+
controller.dispose();
|
|
142
|
+
return { ...state, pricing: controller.getState(), rows: [], canPurchase: false, purchaseDisabled: true };
|
|
143
|
+
},
|
|
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
|
+
});
|
|
152
|
+
void service.start();
|
|
153
|
+
return service;
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The bundle must declare `iap`. To derive platform/app identity from the host,
|
|
158
|
+
declare `app_info` too and call `@himalaya/app-info.get()` before registering the
|
|
159
|
+
service. Use its `platform`, `appId`, and `bundleId`, with the caller's site and
|
|
160
|
+
opaque context revision. If the call rejects or identity is absent, keep the
|
|
161
|
+
Apple route unavailable; never assume iOS. Its `environment: dev | prod` describes
|
|
162
|
+
Himalaya serving, **not** Apple Sandbox/production. Do not introduce an app-info
|
|
163
|
+
dependency into the pricing package itself.
|
|
164
|
+
|
|
165
|
+
Bind price text to `$service:pricing.rows.0.displayPrice` for a single card/detail/
|
|
166
|
+
checkout item, and use the existing bound list template for multiple rows. Bind
|
|
167
|
+
loading visibility to `hideLoading`, Retry visibility to `hideRetry`, and the
|
|
168
|
+
purchase button's `disabledPath` to `$service:pricing.purchaseDisabled`. The Retry
|
|
169
|
+
action is `tier5.pricing.retry`. Existing Text, ActivityIndicator, and Button components
|
|
170
|
+
are sufficient; keep labels in the consuming copy pack and use stable component
|
|
171
|
+
IDs. Regular commerce route/checkout remains owned by the existing commerce flow.
|
|
172
|
+
|
|
173
|
+
## Lifecycle and purchase boundaries
|
|
174
|
+
|
|
175
|
+
The service arms `_poll` at module evaluation with an immediate first load and a
|
|
176
|
+
60-second visible cadence. The **host** schedules it: worker JS has no ordinary
|
|
177
|
+
timers. Current iOS pooling pauses off-screen polls, resumes immediately on return,
|
|
178
|
+
and the lifecycle scheduler pauses in the background and fires on resume. Worker
|
|
179
|
+
teardown cancels polling. `_poll` has no explicit stop directive; a disposed
|
|
180
|
+
service ignores subsequent ticks until its worker is destroyed.
|
|
181
|
+
|
|
182
|
+
Use the same module-evaluation registration on card, detail, and checkout entry;
|
|
183
|
+
checkout opens with a fresh controller/lookup, or call `reload` if retaining its
|
|
184
|
+
controller. Route supported caller scope/reference changes through `setInput`.
|
|
185
|
+
Do not use descriptor `onAppear` as a reliable iOS return callback: retained
|
|
186
|
+
screens do not necessarily remount. Prices and derived fields must be excluded
|
|
187
|
+
from persisted worker hydration, as above, on hosts supporting `notHydrated`.
|
|
188
|
+
|
|
189
|
+
Every new lookup clears the amount immediately. A failed periodic/background
|
|
190
|
+
refresh shows unavailable with Retry rather than keeping a prior amount as freshly
|
|
191
|
+
confirmed. Overlapping entry, poll, checkout, and Retry requests join matching
|
|
192
|
+
in-flight work. No polling continues while the iOS host has pooled/backgrounded
|
|
193
|
+
the worker.
|
|
194
|
+
|
|
195
|
+
No Storefront update bridge is implemented in this slice. Supported invalidations
|
|
196
|
+
refresh prices; a storefront change while visible may remain undetected until the
|
|
197
|
+
next poll or explicit refresh. This does not establish immediate storefront
|
|
198
|
+
notification. Native `Storefront.updates` remains a separate follow-up.
|
|
199
|
+
|
|
200
|
+
Price metadata is not purchase authorization or entitlement. A future activated
|
|
201
|
+
checkout must compare its purchase context's Apple product ID with the displayed
|
|
202
|
+
ready state; if they differ, refresh mapping/price and show the new amount before
|
|
203
|
+
allowing purchase. The existing purchase coordinator remains unchanged, and this
|
|
204
|
+
preview never invokes it. Mapping fixtures and their failure scenarios belong only
|
|
205
|
+
in the local demonstration. Production mapping, checkout activation, and template
|
|
206
|
+
migration are still required before rollout.
|
|
207
|
+
|
|
208
|
+
The dedicated iOS LiftGear QA binary opts into the existing managed transaction
|
|
209
|
+
API before shared capability registration. Its native identity requires both
|
|
210
|
+
`com.alexchudnovsky.liftgear` and baked app
|
|
211
|
+
`alexanderch3.fit-gear-2-push-test`; the tenant is `liftgear-sandbox-lab`.
|
|
212
|
+
This opt-in cannot be selected by worker input, member identity, or OTA content.
|
|
213
|
+
Other Branded Lite identities retain their existing registration behavior.
|
|
214
|
+
For this dedicated QA identity, a pinned baked config with the
|
|
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.
|
package/guides/index.json
CHANGED
|
@@ -178,6 +178,12 @@
|
|
|
178
178
|
"category": "Workers & SDKs",
|
|
179
179
|
"file": "capabilities.md"
|
|
180
180
|
},
|
|
181
|
+
{
|
|
182
|
+
"id": "iap-pricing",
|
|
183
|
+
"title": "Apple IAP pricing in a worker",
|
|
184
|
+
"category": "Workers & SDKs",
|
|
185
|
+
"file": "iap-pricing.md"
|
|
186
|
+
},
|
|
181
187
|
{
|
|
182
188
|
"id": "wix-api-usage",
|
|
183
189
|
"title": "Call Wix APIs from a worker",
|
|
@@ -1705,6 +1705,78 @@
|
|
|
1705
1705
|
"jsdoc": "/** Creates a coordinator owned by the host/service lifecycle, not by a screen or render.\n * Closures, not classes: Tier-5 bundles must also evaluate on Hermes, which has no `class`. */",
|
|
1706
1706
|
"decl": "export function createPurchaseCoordinator(deps: PurchaseCoordinatorDependencies): PurchaseCoordinator;"
|
|
1707
1707
|
},
|
|
1708
|
+
{
|
|
1709
|
+
"name": "CatalogReference",
|
|
1710
|
+
"kind": "interface",
|
|
1711
|
+
"jsdoc": "",
|
|
1712
|
+
"decl": "export interface CatalogReference {\n kind: string;\n id: string;\n}"
|
|
1713
|
+
},
|
|
1714
|
+
{
|
|
1715
|
+
"name": "PricingScope",
|
|
1716
|
+
"kind": "interface",
|
|
1717
|
+
"jsdoc": "/** Opaque context revision changes whenever a mapping-dependent audience changes. */",
|
|
1718
|
+
"decl": "export interface PricingScope {\n appId: string;\n bundleId: string;\n siteId: string;\n contextRevision?: string;\n}"
|
|
1719
|
+
},
|
|
1720
|
+
{
|
|
1721
|
+
"name": "PricingItem",
|
|
1722
|
+
"kind": "interface",
|
|
1723
|
+
"jsdoc": "",
|
|
1724
|
+
"decl": "export interface PricingItem {\n reference: CatalogReference;\n /** Owned/formatted by the commerce caller; used only for an explicit regular route. */\n commerceDisplayPrice?: string;\n}"
|
|
1725
|
+
},
|
|
1726
|
+
{
|
|
1727
|
+
"name": "PricingInput",
|
|
1728
|
+
"kind": "interface",
|
|
1729
|
+
"jsdoc": "",
|
|
1730
|
+
"decl": "export interface PricingInput {\n scope: PricingScope;\n /** Trusted host platform; only \"ios\" performs Apple lookup. */\n platform: string;\n items: PricingItem[];\n}"
|
|
1731
|
+
},
|
|
1732
|
+
{
|
|
1733
|
+
"name": "PricingMapping",
|
|
1734
|
+
"kind": "type",
|
|
1735
|
+
"jsdoc": "",
|
|
1736
|
+
"decl": "export type PricingMapping = { reference: CatalogReference } & (\n | { route: \"appleIap\"; productId: string; mappingRevision: string }\n | { route: \"regular\" }\n | { route: \"unavailable\" }\n);"
|
|
1737
|
+
},
|
|
1738
|
+
{
|
|
1739
|
+
"name": "PricingMappingProvider",
|
|
1740
|
+
"kind": "interface",
|
|
1741
|
+
"jsdoc": "/** Internal client port, not an agreed/deployed backend API. Never requires purchase context. */",
|
|
1742
|
+
"decl": "export interface PricingMappingProvider {\n resolve(input: { scope: PricingScope; references: CatalogReference[] }): Promise<PricingMapping[]>;\n}"
|
|
1743
|
+
},
|
|
1744
|
+
{
|
|
1745
|
+
"name": "PricingUnavailableReason",
|
|
1746
|
+
"kind": "type",
|
|
1747
|
+
"jsdoc": "",
|
|
1748
|
+
"decl": "export type PricingUnavailableReason = \"invalidInput\" | \"mappingUnavailable\" | \"mappingUnresolved\"\n | \"unsupportedPlatform\" | \"unsupportedProductType\" | \"priceUnavailable\" | \"commercePriceUnavailable\";"
|
|
1749
|
+
},
|
|
1750
|
+
{
|
|
1751
|
+
"name": "ItemPriceState",
|
|
1752
|
+
"kind": "type",
|
|
1753
|
+
"jsdoc": "",
|
|
1754
|
+
"decl": "export type ItemPriceState = { reference: CatalogReference } & (\n | { status: \"loading\" }\n | { status: \"ready\"; source: \"appleIap\"; displayPrice: string; productId: string; mappingRevision: string }\n | { status: \"ready\"; source: \"regular\"; displayPrice: string }\n | { status: \"unavailable\"; reason: PricingUnavailableReason; retryable: boolean }\n);"
|
|
1755
|
+
},
|
|
1756
|
+
{
|
|
1757
|
+
"name": "PricingState",
|
|
1758
|
+
"kind": "interface",
|
|
1759
|
+
"jsdoc": "/** Contains no credentials, transaction proof, entitlement, or purchase authorization. */",
|
|
1760
|
+
"decl": "export interface PricingState {\n revision: number;\n scope: PricingScope | null;\n items: ItemPriceState[];\n}"
|
|
1761
|
+
},
|
|
1762
|
+
{
|
|
1763
|
+
"name": "PricingDependencies",
|
|
1764
|
+
"kind": "interface",
|
|
1765
|
+
"jsdoc": "",
|
|
1766
|
+
"decl": "export interface PricingDependencies {\n mapping: PricingMappingProvider;\n getProducts(productIds: string[]): Promise<Product[]>;\n onState?(state: PricingState): void;\n}"
|
|
1767
|
+
},
|
|
1768
|
+
{
|
|
1769
|
+
"name": "PricingController",
|
|
1770
|
+
"kind": "interface",
|
|
1771
|
+
"jsdoc": "",
|
|
1772
|
+
"decl": "export interface PricingController {\n /** Fresh lookup; identical in-flight input is shared. Completed prices are never cached. */\n load(input: PricingInput): Promise<void>;\n /** Reloads the last input, joining a matching lookup already in progress. */\n refresh(): Promise<void>;\n getState(): PricingState;\n /** Clears state and suppresses subsequent calls/publications; cannot cancel native I/O. */\n dispose(): void;\n}"
|
|
1773
|
+
},
|
|
1774
|
+
{
|
|
1775
|
+
"name": "createPricingController",
|
|
1776
|
+
"kind": "function",
|
|
1777
|
+
"jsdoc": "/** Creates one worker's read-only pricing controller with injected mapping, store lookup, and state publication. */",
|
|
1778
|
+
"decl": "export function createPricingController(deps: PricingDependencies): PricingController;"
|
|
1779
|
+
},
|
|
1708
1780
|
{
|
|
1709
1781
|
"name": "ProductType",
|
|
1710
1782
|
"kind": "type",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/himalaya-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1241.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": "5e95fe6c20dcd9b9fbc99564f2535787252632211a307e0d5b9908f9"
|
|
71
71
|
}
|