@wix/himalaya-cli 0.1524.0 → 0.1525.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/apple-iap-contract.d.ts +5 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/cashier-charges.d.ts +3 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap-checkout.mjs +35 -9
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap.mjs +14 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/screens.mjs +34 -8
- package/dist/cli.mjs +0 -1
- package/package.json +2 -2
- package/templates/beauty-salon/tier5-src/himi-store-product/index.ts +153 -132
- package/templates/branded-lite/tier5-src/himi-store-product/index.ts +153 -132
- package/templates/gym-fitness/tier5-src/himi-store-product/index.ts +153 -132
- package/templates/portfolio-creative/tier5-src/himi-store-product/index.ts +153 -132
- package/templates/retail-store/tier5-src/himi-store-product/index.ts +153 -132
|
@@ -76,10 +76,14 @@ export type IapEligibility = {
|
|
|
76
76
|
};
|
|
77
77
|
/** `open`: a pending order the cashier can charge. `granted`: the vertical already gives the
|
|
78
78
|
* member access, so nothing is sold. */
|
|
79
|
-
export type OrderAttempt = (
|
|
79
|
+
export type OrderAttempt = (
|
|
80
|
+
/** `placed`: the order was created by this call, so it cannot have a charge yet. Only an adapter
|
|
81
|
+
* that never returns an existing order may say so; an order it reuses must be listed first. */
|
|
82
|
+
{
|
|
80
83
|
status: "open";
|
|
81
84
|
cashierOrderId: string;
|
|
82
85
|
verticalRef: VerticalRef;
|
|
86
|
+
placed?: boolean;
|
|
83
87
|
} | {
|
|
84
88
|
status: "granted";
|
|
85
89
|
} | {
|
|
@@ -15,6 +15,9 @@ export type AppleIapChargeResult = {
|
|
|
15
15
|
status: "closed" | "inOtherPayment" | "refused" | "notConfigured" | "invalidResponse" | "unavailable" | "notEnabled" | "sessionChanged";
|
|
16
16
|
};
|
|
17
17
|
export declare function ensureAppleIapCharge(bridge: SessionBoundBridge, route: CashierRoute, orderId: string): Promise<AppleIapChargeResult>;
|
|
18
|
+
/** An order created in this same call has no charges, so List would only cost a round trip. A
|
|
19
|
+
* lost answer is still reconciled through List. */
|
|
20
|
+
export declare function chargePlacedOrder(bridge: SessionBoundBridge, route: CashierRoute, orderId: string): Promise<AppleIapChargeResult>;
|
|
18
21
|
export declare function listOrderCharges(bridge: SessionBoundBridge, route: CashierRoute, orderId: string): Promise<{
|
|
19
22
|
status: "ok";
|
|
20
23
|
charges: CashierCharge[];
|
|
@@ -810,10 +810,23 @@ function enabledPricing(source, deps) {
|
|
|
810
810
|
let selling = "active";
|
|
811
811
|
let lookupRevision = 0;
|
|
812
812
|
let answeredRoutes;
|
|
813
|
+
let inFlight;
|
|
814
|
+
const sharedAnswer = (request) => {
|
|
815
|
+
const key = JSON.stringify(request);
|
|
816
|
+
if (inFlight?.key === key) return inFlight.answer;
|
|
817
|
+
const answer = Promise.resolve(source.mapping.resolve(request));
|
|
818
|
+
const asked = { key, answer };
|
|
819
|
+
inFlight = asked;
|
|
820
|
+
const settle = () => {
|
|
821
|
+
if (inFlight === asked) inFlight = void 0;
|
|
822
|
+
};
|
|
823
|
+
answer.then(settle, settle);
|
|
824
|
+
return answer;
|
|
825
|
+
};
|
|
813
826
|
const mapping = {
|
|
814
827
|
resolve: async (request) => {
|
|
815
828
|
const asked = lookupRevision;
|
|
816
|
-
const answer = await
|
|
829
|
+
const answer = await sharedAnswer(request);
|
|
817
830
|
if (asked === lookupRevision) answeredRoutes = routesAnswered(answer);
|
|
818
831
|
return answer;
|
|
819
832
|
}
|
|
@@ -1570,12 +1583,19 @@ function storesAppleSalesEnabled() {
|
|
|
1570
1583
|
|
|
1571
1584
|
// ../../stdlib/flows/iap-checkout/src/cashier-charges.ts
|
|
1572
1585
|
async function ensureAppleIapCharge(bridge, route, orderId) {
|
|
1573
|
-
|
|
1574
|
-
if (
|
|
1586
|
+
const invalid = invalidRequest(route, orderId);
|
|
1587
|
+
if (invalid) return invalid;
|
|
1575
1588
|
const listed = await listOrderCharges(bridge, route, orderId);
|
|
1576
1589
|
if (listed.status !== "ok") return listed;
|
|
1577
1590
|
return decideFromCharges(listed.charges) ?? chargeThenReconcile(bridge, route, orderId);
|
|
1578
1591
|
}
|
|
1592
|
+
async function chargePlacedOrder(bridge, route, orderId) {
|
|
1593
|
+
return invalidRequest(route, orderId) ?? chargeThenReconcile(bridge, route, orderId);
|
|
1594
|
+
}
|
|
1595
|
+
function invalidRequest(route, orderId) {
|
|
1596
|
+
if (!isRoutePrefix2(route.prefix)) return { status: "notConfigured" };
|
|
1597
|
+
return orderId ? null : { status: "invalidResponse" };
|
|
1598
|
+
}
|
|
1579
1599
|
async function listOrderCharges(bridge, route, orderId) {
|
|
1580
1600
|
const answer = await bridge.call("GET", chargesPath(route, orderId));
|
|
1581
1601
|
if (answer.status !== "ok") return failureOf(answer);
|
|
@@ -1892,7 +1912,7 @@ function createWixOrderBackend(deps) {
|
|
|
1892
1912
|
async function startOrderFor(sale) {
|
|
1893
1913
|
const ordered = await orderFor(sale);
|
|
1894
1914
|
if (isStop(ordered)) return contextResult(sale.catalogRef, ordered);
|
|
1895
|
-
const charged = await chargeFor(sale, ordered, { reordered: false });
|
|
1915
|
+
const charged = await chargeFor(sale, ordered.ref, { reordered: false, placed: ordered.placed });
|
|
1896
1916
|
if (isStop(charged)) return contextResult(sale.catalogRef, charged);
|
|
1897
1917
|
return startFor(sale, charged);
|
|
1898
1918
|
}
|
|
@@ -1952,7 +1972,9 @@ function createWixOrderBackend(deps) {
|
|
|
1952
1972
|
return true;
|
|
1953
1973
|
};
|
|
1954
1974
|
const attempt3 = existing?.cashierOrderId && existing.verticalRef ? await reopenOrOpen(sale, { cashierOrderId: existing.cashierOrderId, verticalRef: existing.verticalRef }) : await sale.adapter.openOrder(sale.item, sale.bridge, beforeCreate);
|
|
1955
|
-
|
|
1975
|
+
if (prevented) return prevented;
|
|
1976
|
+
const ref = recordOrder(sale, existing, attempt3);
|
|
1977
|
+
return isStop(ref) ? ref : { ref, placed: isPlaced(attempt3) };
|
|
1956
1978
|
}
|
|
1957
1979
|
async function reopenOrOpen(sale, order) {
|
|
1958
1980
|
const reopened = await sale.adapter.reopenOrder(order, sale.bridge);
|
|
@@ -1978,7 +2000,7 @@ function createWixOrderBackend(deps) {
|
|
|
1978
2000
|
});
|
|
1979
2001
|
}
|
|
1980
2002
|
async function chargeFor(sale, ref, options) {
|
|
1981
|
-
const charge = await ensureAppleIapCharge(sale.bridge, deps.cashier, ref.cashierOrderId ?? "");
|
|
2003
|
+
const charge = await (options.placed ? chargePlacedOrder : ensureAppleIapCharge)(sale.bridge, deps.cashier, ref.cashierOrderId ?? "");
|
|
1982
2004
|
switch (charge.status) {
|
|
1983
2005
|
case "pending":
|
|
1984
2006
|
return persist(sale, { ...ref, stage: "charged", wixTransactionId: charge.transactionId, updatedAt: now() });
|
|
@@ -2002,8 +2024,9 @@ function createWixOrderBackend(deps) {
|
|
|
2002
2024
|
async function reorder(sale) {
|
|
2003
2025
|
if (sale.adapter.repeatable) return stopWith("rejected", "orderClosed");
|
|
2004
2026
|
if (!forget(sale, sale.catalogRef)) return stopWith("rejected", "storageUnavailable");
|
|
2005
|
-
const
|
|
2006
|
-
|
|
2027
|
+
const attempt3 = await sale.adapter.openOrder(sale.item, sale.bridge);
|
|
2028
|
+
const recorded = recordOrder(sale, void 0, attempt3);
|
|
2029
|
+
return isStop(recorded) ? recorded : chargeFor(sale, recorded, { reordered: true, placed: isPlaced(attempt3) });
|
|
2007
2030
|
}
|
|
2008
2031
|
async function alreadyPaid(sale, ref) {
|
|
2009
2032
|
const access = await sale.adapter.readAccess(ref.verticalRef ?? {}, sale.bridge);
|
|
@@ -2237,6 +2260,9 @@ function unassociatedHold(transaction, updatedAt) {
|
|
|
2237
2260
|
function acknowledgementOf(transaction, scopeKey) {
|
|
2238
2261
|
return { transactionId: transaction.transactionId, productId: transaction.productId, environment: transaction.environment, scopeKey };
|
|
2239
2262
|
}
|
|
2263
|
+
function isPlaced(attempt3) {
|
|
2264
|
+
return attempt3.status === "open" && attempt3.placed === true;
|
|
2265
|
+
}
|
|
2240
2266
|
function refAt(partition, key) {
|
|
2241
2267
|
return partition.refs().find((ref) => ref.key === key);
|
|
2242
2268
|
}
|
|
@@ -3267,7 +3293,7 @@ async function readAccess3(verticalRef, bridge) {
|
|
|
3267
3293
|
async function placedOrder(body, bridge) {
|
|
3268
3294
|
if (!isObject7(body) || !isNonEmpty3(body.orderId)) return ORDER_REFUSED;
|
|
3269
3295
|
const verticalRef = { orderId: body.orderId };
|
|
3270
|
-
if (isNonEmpty3(body.paymentGatewayOrderId)) return { status: "open", cashierOrderId: body.paymentGatewayOrderId, verticalRef };
|
|
3296
|
+
if (isNonEmpty3(body.paymentGatewayOrderId)) return { status: "open", cashierOrderId: body.paymentGatewayOrderId, verticalRef, placed: true };
|
|
3271
3297
|
const access = await readAccess3(verticalRef, bridge);
|
|
3272
3298
|
if (access === "granted") return GRANTED;
|
|
3273
3299
|
return access === "sessionChanged" || access === "unavailable" ? { status: access } : ORDER_REFUSED;
|
|
@@ -787,10 +787,23 @@ function enabledPricing(source, deps) {
|
|
|
787
787
|
let selling = "active";
|
|
788
788
|
let lookupRevision = 0;
|
|
789
789
|
let answeredRoutes;
|
|
790
|
+
let inFlight;
|
|
791
|
+
const sharedAnswer = (request) => {
|
|
792
|
+
const key = JSON.stringify(request);
|
|
793
|
+
if (inFlight?.key === key) return inFlight.answer;
|
|
794
|
+
const answer = Promise.resolve(source.mapping.resolve(request));
|
|
795
|
+
const asked = { key, answer };
|
|
796
|
+
inFlight = asked;
|
|
797
|
+
const settle = () => {
|
|
798
|
+
if (inFlight === asked) inFlight = void 0;
|
|
799
|
+
};
|
|
800
|
+
answer.then(settle, settle);
|
|
801
|
+
return answer;
|
|
802
|
+
};
|
|
790
803
|
const mapping = {
|
|
791
804
|
resolve: async (request) => {
|
|
792
805
|
const asked = lookupRevision;
|
|
793
|
-
const answer = await
|
|
806
|
+
const answer = await sharedAnswer(request);
|
|
794
807
|
if (asked === lookupRevision) answeredRoutes = routesAnswered(answer);
|
|
795
808
|
return answer;
|
|
796
809
|
}
|
|
@@ -1352,10 +1352,23 @@ function enabledPricing(source, deps) {
|
|
|
1352
1352
|
let selling = "active";
|
|
1353
1353
|
let lookupRevision = 0;
|
|
1354
1354
|
let answeredRoutes;
|
|
1355
|
+
let inFlight;
|
|
1356
|
+
const sharedAnswer = (request) => {
|
|
1357
|
+
const key = JSON.stringify(request);
|
|
1358
|
+
if (inFlight?.key === key) return inFlight.answer;
|
|
1359
|
+
const answer = Promise.resolve(source.mapping.resolve(request));
|
|
1360
|
+
const asked = { key, answer };
|
|
1361
|
+
inFlight = asked;
|
|
1362
|
+
const settle = () => {
|
|
1363
|
+
if (inFlight === asked) inFlight = void 0;
|
|
1364
|
+
};
|
|
1365
|
+
answer.then(settle, settle);
|
|
1366
|
+
return answer;
|
|
1367
|
+
};
|
|
1355
1368
|
const mapping = {
|
|
1356
1369
|
resolve: async (request) => {
|
|
1357
1370
|
const asked = lookupRevision;
|
|
1358
|
-
const answer = await
|
|
1371
|
+
const answer = await sharedAnswer(request);
|
|
1359
1372
|
if (asked === lookupRevision) answeredRoutes = routesAnswered(answer);
|
|
1360
1373
|
return answer;
|
|
1361
1374
|
}
|
|
@@ -2130,12 +2143,19 @@ function itemPaymentEnabled() {
|
|
|
2130
2143
|
|
|
2131
2144
|
// ../../stdlib/flows/iap-checkout/src/cashier-charges.ts
|
|
2132
2145
|
async function ensureAppleIapCharge(bridge, route, orderId) {
|
|
2133
|
-
|
|
2134
|
-
if (
|
|
2146
|
+
const invalid = invalidRequest(route, orderId);
|
|
2147
|
+
if (invalid) return invalid;
|
|
2135
2148
|
const listed = await listOrderCharges(bridge, route, orderId);
|
|
2136
2149
|
if (listed.status !== "ok") return listed;
|
|
2137
2150
|
return decideFromCharges(listed.charges) ?? chargeThenReconcile(bridge, route, orderId);
|
|
2138
2151
|
}
|
|
2152
|
+
async function chargePlacedOrder(bridge, route, orderId) {
|
|
2153
|
+
return invalidRequest(route, orderId) ?? chargeThenReconcile(bridge, route, orderId);
|
|
2154
|
+
}
|
|
2155
|
+
function invalidRequest(route, orderId) {
|
|
2156
|
+
if (!isRoutePrefix2(route.prefix)) return { status: "notConfigured" };
|
|
2157
|
+
return orderId ? null : { status: "invalidResponse" };
|
|
2158
|
+
}
|
|
2139
2159
|
async function listOrderCharges(bridge, route, orderId) {
|
|
2140
2160
|
const answer = await bridge.call("GET", chargesPath(route, orderId));
|
|
2141
2161
|
if (answer.status !== "ok") return failureOf(answer);
|
|
@@ -2452,7 +2472,7 @@ function createWixOrderBackend(deps) {
|
|
|
2452
2472
|
async function startOrderFor(sale) {
|
|
2453
2473
|
const ordered = await orderFor(sale);
|
|
2454
2474
|
if (isStop(ordered)) return contextResult(sale.catalogRef, ordered);
|
|
2455
|
-
const charged = await chargeFor(sale, ordered, { reordered: false });
|
|
2475
|
+
const charged = await chargeFor(sale, ordered.ref, { reordered: false, placed: ordered.placed });
|
|
2456
2476
|
if (isStop(charged)) return contextResult(sale.catalogRef, charged);
|
|
2457
2477
|
return startFor(sale, charged);
|
|
2458
2478
|
}
|
|
@@ -2512,7 +2532,9 @@ function createWixOrderBackend(deps) {
|
|
|
2512
2532
|
return true;
|
|
2513
2533
|
};
|
|
2514
2534
|
const attempt4 = existing?.cashierOrderId && existing.verticalRef ? await reopenOrOpen(sale, { cashierOrderId: existing.cashierOrderId, verticalRef: existing.verticalRef }) : await sale.adapter.openOrder(sale.item, sale.bridge, beforeCreate);
|
|
2515
|
-
|
|
2535
|
+
if (prevented) return prevented;
|
|
2536
|
+
const ref = recordOrder(sale, existing, attempt4);
|
|
2537
|
+
return isStop(ref) ? ref : { ref, placed: isPlaced(attempt4) };
|
|
2516
2538
|
}
|
|
2517
2539
|
async function reopenOrOpen(sale, order) {
|
|
2518
2540
|
const reopened = await sale.adapter.reopenOrder(order, sale.bridge);
|
|
@@ -2538,7 +2560,7 @@ function createWixOrderBackend(deps) {
|
|
|
2538
2560
|
});
|
|
2539
2561
|
}
|
|
2540
2562
|
async function chargeFor(sale, ref, options) {
|
|
2541
|
-
const charge = await ensureAppleIapCharge(sale.bridge, deps.cashier, ref.cashierOrderId ?? "");
|
|
2563
|
+
const charge = await (options.placed ? chargePlacedOrder : ensureAppleIapCharge)(sale.bridge, deps.cashier, ref.cashierOrderId ?? "");
|
|
2542
2564
|
switch (charge.status) {
|
|
2543
2565
|
case "pending":
|
|
2544
2566
|
return persist(sale, { ...ref, stage: "charged", wixTransactionId: charge.transactionId, updatedAt: now() });
|
|
@@ -2562,8 +2584,9 @@ function createWixOrderBackend(deps) {
|
|
|
2562
2584
|
async function reorder(sale) {
|
|
2563
2585
|
if (sale.adapter.repeatable) return stopWith("rejected", "orderClosed");
|
|
2564
2586
|
if (!forget(sale, sale.catalogRef)) return stopWith("rejected", "storageUnavailable");
|
|
2565
|
-
const
|
|
2566
|
-
|
|
2587
|
+
const attempt4 = await sale.adapter.openOrder(sale.item, sale.bridge);
|
|
2588
|
+
const recorded = recordOrder(sale, void 0, attempt4);
|
|
2589
|
+
return isStop(recorded) ? recorded : chargeFor(sale, recorded, { reordered: true, placed: isPlaced(attempt4) });
|
|
2567
2590
|
}
|
|
2568
2591
|
async function alreadyPaid(sale, ref) {
|
|
2569
2592
|
const access = await sale.adapter.readAccess(ref.verticalRef ?? {}, sale.bridge);
|
|
@@ -2797,6 +2820,9 @@ function unassociatedHold(transaction, updatedAt) {
|
|
|
2797
2820
|
function acknowledgementOf(transaction, scopeKey) {
|
|
2798
2821
|
return { transactionId: transaction.transactionId, productId: transaction.productId, environment: transaction.environment, scopeKey };
|
|
2799
2822
|
}
|
|
2823
|
+
function isPlaced(attempt4) {
|
|
2824
|
+
return attempt4.status === "open" && attempt4.placed === true;
|
|
2825
|
+
}
|
|
2800
2826
|
function refAt(partition, key) {
|
|
2801
2827
|
return partition.refs().find((ref) => ref.key === key);
|
|
2802
2828
|
}
|
package/dist/cli.mjs
CHANGED
|
@@ -22307,7 +22307,6 @@ function resolveIdiom(tokens2, ctx = {}) {
|
|
|
22307
22307
|
var IDIOM_CONTEXTS, UNSAFE_KEYS;
|
|
22308
22308
|
var init_idiom = __esm({
|
|
22309
22309
|
"../../core/ts/src/idiom.ts"() {
|
|
22310
|
-
"use strict";
|
|
22311
22310
|
IDIOM_CONTEXTS = {
|
|
22312
22311
|
ios: { native: ["ios"] },
|
|
22313
22312
|
android: { native: ["material"] },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/himalaya-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1525.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": [
|
|
@@ -71,5 +71,5 @@
|
|
|
71
71
|
"artifactId": "himalaya-cli"
|
|
72
72
|
}
|
|
73
73
|
},
|
|
74
|
-
"falconPackageHash": "
|
|
74
|
+
"falconPackageHash": "4d57d913fdf7e7ed0944f6605a9a5093b4fe45fb1efa09885218796d"
|
|
75
75
|
}
|
|
@@ -93,6 +93,25 @@ function productPolicyBody(product: StoreProduct, kind: "returns" | "shipping"):
|
|
|
93
93
|
* DIFFERENT product (drop the picks) from the same product coming back (keep them). */
|
|
94
94
|
let lastLoadedId: string | undefined;
|
|
95
95
|
let productLoadSequence = 0;
|
|
96
|
+
let booting = true;
|
|
97
|
+
let bootLoad: { id: string; state: Promise<S> } | undefined;
|
|
98
|
+
|
|
99
|
+
/** The host replays the screen's onAppear load right after the module-scope one: that one replay
|
|
100
|
+
* joins the boot load instead of reading the product, cart and App Store prices again. Any later
|
|
101
|
+
* entry loads afresh, since the cart, the saved flag or a purchase may have changed meanwhile. */
|
|
102
|
+
function joinedLoad(id: string, load: () => Promise<S>): Promise<S> {
|
|
103
|
+
if (booting) {
|
|
104
|
+
booting = false;
|
|
105
|
+
const current = { id, state: load() };
|
|
106
|
+
bootLoad = current;
|
|
107
|
+
const settle = () => { if (bootLoad === current) bootLoad = undefined; };
|
|
108
|
+
current.state.then(settle, settle);
|
|
109
|
+
return current.state;
|
|
110
|
+
}
|
|
111
|
+
const replayed = bootLoad;
|
|
112
|
+
bootLoad = undefined;
|
|
113
|
+
return replayed?.id === id ? replayed.state : load();
|
|
114
|
+
}
|
|
96
115
|
|
|
97
116
|
interface OptionSlots {
|
|
98
117
|
opt1Label: string; opt1Chips: OptionChip[]; opt1Selected: string[]; opt1SelectedId: string; opt1Placeholder: string; noOpt1: boolean;
|
|
@@ -576,144 +595,146 @@ const svc = defineService<S, {
|
|
|
576
595
|
purchasedNote: "",
|
|
577
596
|
}),
|
|
578
597
|
actions: {
|
|
579
|
-
load:
|
|
598
|
+
load: (s, arg): Promise<S> => {
|
|
580
599
|
stashCopy(arg);
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
const id = inputId();
|
|
585
|
-
// Storage problems must not prevent browsing or buying a product.
|
|
586
|
-
let saved = false;
|
|
587
|
-
try { saved = isProductSaved(id); } catch { /* save action reports a retryable error */ }
|
|
588
|
-
s = { ...s, saved, saveHint: "", shareHint: "", adding: false, added: false, addedSheetPresented: false, quantity: 1, showOptionErrors: false, returnPolicyExpanded: false, shippingInfoExpanded: false };
|
|
589
|
-
// A fresh navigation into the product resets any quantity left over
|
|
590
|
-
// from a previous visit to the same worker instance.
|
|
591
|
-
if (!id) {
|
|
600
|
+
return joinedLoad(inputId(), async (): Promise<S> => {
|
|
601
|
+
enterCheckout();
|
|
602
|
+
const sequence = ++productLoadSequence;
|
|
592
603
|
lastProduct = undefined;
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
const
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
604
|
+
const id = inputId();
|
|
605
|
+
// Storage problems must not prevent browsing or buying a product.
|
|
606
|
+
let saved = false;
|
|
607
|
+
try { saved = isProductSaved(id); } catch { /* save action reports a retryable error */ }
|
|
608
|
+
s = { ...s, saved, saveHint: "", shareHint: "", adding: false, added: false, addedSheetPresented: false, quantity: 1, showOptionErrors: false, returnPolicyExpanded: false, shippingInfoExpanded: false };
|
|
609
|
+
// A fresh navigation into the product resets any quantity left over
|
|
610
|
+
// from a previous visit to the same worker instance.
|
|
611
|
+
if (!id) {
|
|
612
|
+
lastProduct = undefined;
|
|
613
|
+
lastLoadedId = undefined;
|
|
614
|
+
return derive({ ...s, status: "ready", quantity: 1, optionGroups: [], selection: {}, optionsUnknown: false, descriptionJson: "", emptyText: copyParam("product.notFoundText", "Product not found.") });
|
|
615
|
+
}
|
|
616
|
+
// The cart count only feeds the top-bar badge: ask for it NOW, beside the product, instead
|
|
617
|
+
// of after it — it used to add a whole cart round trip before the page could paint.
|
|
618
|
+
const cartCount = cartItemCount();
|
|
619
|
+
// The badge must never hold the page: remember the count if it is already in, and
|
|
620
|
+
// publish it separately if it lands after the product (#3468 review).
|
|
621
|
+
let cartKnown: number | undefined;
|
|
622
|
+
void cartCount.then((n) => { cartKnown = n; }, () => {});
|
|
623
|
+
// The live product read goes out FIRST too — the local-db read below must never delay it.
|
|
624
|
+
// Settled into a value so a failure is handled where it is awaited, never unhandled.
|
|
625
|
+
// DESCRIPTION widens the default fieldset with the V3 Ricos description (a V1 catalog ignores it).
|
|
626
|
+
const live = getStoreProduct(id, ["DESCRIPTION"]).then(
|
|
627
|
+
(r) => ({ ok: true as const, r }),
|
|
628
|
+
() => ({ ok: false as const }),
|
|
629
|
+
);
|
|
630
|
+
// Paint at once from the row the list already synced (name, price, image) — the page used
|
|
631
|
+
// to sit on its skeleton for the product round trip even when the list had just shown it.
|
|
632
|
+
// The live read then fills in the description and gallery.
|
|
633
|
+
const synced = await readCachedProduct(id);
|
|
634
|
+
let painted = false;
|
|
635
|
+
if (synced && s.status === "loading") {
|
|
636
|
+
await svc.paintCached(synced);
|
|
637
|
+
painted = true;
|
|
638
|
+
}
|
|
639
|
+
const outcome = await live;
|
|
640
|
+
if (sequence !== productLoadSequence || id !== inputId()) return svc.getState() as S;
|
|
641
|
+
// Once painted the page is INTERACTIVE while the live read is on the wire: the visitor may
|
|
642
|
+
// save, change the quantity or add to cart. Build on the state as it is NOW, and keep what
|
|
643
|
+
// they did — overwriting it would untick a stored save, reset the quantity, and drop the
|
|
644
|
+
// in-flight add's `adding` guard (a second tap would add the line twice).
|
|
645
|
+
const base: S = painted ? (svc.getState() as S) : s;
|
|
646
|
+
const keep = painted
|
|
647
|
+
? { saved: base.saved, saveHint: base.saveHint, quantity: base.quantity, adding: base.adding, added: base.added, cartHint: base.cartHint }
|
|
648
|
+
: { cartHint: "", quantity: 1 };
|
|
649
|
+
const badge = (): number => {
|
|
650
|
+
if (painted && (base.adding || base.added)) return base.cartCount;
|
|
651
|
+
if (cartKnown !== undefined) return cartKnown;
|
|
652
|
+
const epoch = cartBadgeEpoch;
|
|
653
|
+
void cartCount.then((n) => { void svc.setCartCount({ count: n, epoch }); }, () => {});
|
|
654
|
+
return base.cartCount;
|
|
655
|
+
};
|
|
656
|
+
if (outcome.ok) {
|
|
657
|
+
const p = (outcome.r.product ?? (outcome.r as unknown as StoreProduct)) as StoreProduct;
|
|
658
|
+
const name = cleanTitle(p.name) || "Product";
|
|
659
|
+
const image = productImage(p, WIX_DETAIL_IMAGE);
|
|
660
|
+
lastProduct = p;
|
|
661
|
+
// A group with exactly one choice is chosen for the shopper; a real choice
|
|
662
|
+
// is left blank so the pick is deliberate.
|
|
663
|
+
const optionGroups = productOptions(p);
|
|
664
|
+
const sale = productSale(p);
|
|
665
|
+
// A pooled screen re-appearing re-runs `load`. Wiping `selection` there means a
|
|
666
|
+
// trip to the cart and back silently clears the size the shopper picked and
|
|
667
|
+
// re-disables the CTA with no explanation. Carry the picks when this is the SAME
|
|
668
|
+
// product, and only for a choice the product still offers — a stale id would
|
|
669
|
+
// resolve to no variant and block the cart just as silently.
|
|
670
|
+
const carried: StoreOptionSelection = {};
|
|
671
|
+
if (id === lastLoadedId) {
|
|
672
|
+
for (const g of optionGroups) {
|
|
673
|
+
const chosen = s.selection[g.id];
|
|
674
|
+
if (chosen && g.choices.some((c) => c.id === chosen)) carried[g.id] = chosen;
|
|
675
|
+
}
|
|
655
676
|
}
|
|
677
|
+
lastLoadedId = id;
|
|
678
|
+
return derive({
|
|
679
|
+
...base,
|
|
680
|
+
...keep,
|
|
681
|
+
status: "ready",
|
|
682
|
+
optionGroups,
|
|
683
|
+
selection: { ...defaultSelection(optionGroups), ...carried },
|
|
684
|
+
optionsUnknown: false,
|
|
685
|
+
name,
|
|
686
|
+
...sale,
|
|
687
|
+
commercePrice: sale.price,
|
|
688
|
+
skuLabel: skuOf(p) ? `SKU: ${skuOf(p)}` : "",
|
|
689
|
+
returnPolicyBody: productPolicyBody(p, "returns") || copyParam("product.returnPolicyBody", "") || copyParam("product.policyUnavailable", "Policy details have not been provided by this store."),
|
|
690
|
+
shippingInfoBody: productPolicyBody(p, "shipping") || copyParam("product.shippingInfoBody", "") || copyParam("product.policyUnavailable", "Policy details have not been provided by this store."),
|
|
691
|
+
...productBody(p),
|
|
692
|
+
image,
|
|
693
|
+
gallery: productGallery(p),
|
|
694
|
+
initial: (name[0] || "#").toUpperCase(),
|
|
695
|
+
shareUrl: productShareUrl(p),
|
|
696
|
+
cartCount: badge(),
|
|
697
|
+
emptyText: "",
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
// Offline: fall back to the last-synced cached product row before the error state.
|
|
701
|
+
if (synced) {
|
|
702
|
+
const name = String(synced.name ?? "") || "Product";
|
|
703
|
+
const image = String(synced.image ?? "");
|
|
704
|
+
// The cached list row carries no options, and guessing "none" would offer
|
|
705
|
+
// a one-tap Add to cart for a product that needs a colour and a size. So
|
|
706
|
+
// the offline detail shows no picker AND marks the options UNKNOWN, which
|
|
707
|
+
// keeps `optionsIncomplete` true: the CTA stays disabled and `addToCart`
|
|
708
|
+
// refuses. `cartHint` carries the reason, because the options card is hidden
|
|
709
|
+
// on `noOptions` and would otherwise leave a dead button unexplained.
|
|
710
|
+
lastProduct = undefined;
|
|
711
|
+
lastLoadedId = undefined;
|
|
712
|
+
return derive({
|
|
713
|
+
...base,
|
|
714
|
+
...keep,
|
|
715
|
+
status: "ready",
|
|
716
|
+
optionGroups: [],
|
|
717
|
+
selection: {},
|
|
718
|
+
optionsUnknown: true,
|
|
719
|
+
name,
|
|
720
|
+
price: String(synced.subtitle ?? ""),
|
|
721
|
+
wasPrice: "",
|
|
722
|
+
skuLabel: "",
|
|
723
|
+
returnPolicyBody: "",
|
|
724
|
+
shippingInfoBody: "",
|
|
725
|
+
description: "",
|
|
726
|
+
descriptionJson: "",
|
|
727
|
+
image,
|
|
728
|
+
initial: String(synced.initial ?? "") || (name[0] || "#").toUpperCase(),
|
|
729
|
+
cartHint: copyParam("product.cartHintOffline", "Reconnect to choose options and add this to your cart."),
|
|
730
|
+
cartCount: badge(),
|
|
731
|
+
emptyText: "",
|
|
732
|
+
});
|
|
656
733
|
}
|
|
657
|
-
lastLoadedId = id;
|
|
658
|
-
return derive({
|
|
659
|
-
...base,
|
|
660
|
-
...keep,
|
|
661
|
-
status: "ready",
|
|
662
|
-
optionGroups,
|
|
663
|
-
selection: { ...defaultSelection(optionGroups), ...carried },
|
|
664
|
-
optionsUnknown: false,
|
|
665
|
-
name,
|
|
666
|
-
...sale,
|
|
667
|
-
commercePrice: sale.price,
|
|
668
|
-
skuLabel: skuOf(p) ? `SKU: ${skuOf(p)}` : "",
|
|
669
|
-
returnPolicyBody: productPolicyBody(p, "returns") || copyParam("product.returnPolicyBody", "") || copyParam("product.policyUnavailable", "Policy details have not been provided by this store."),
|
|
670
|
-
shippingInfoBody: productPolicyBody(p, "shipping") || copyParam("product.shippingInfoBody", "") || copyParam("product.policyUnavailable", "Policy details have not been provided by this store."),
|
|
671
|
-
...productBody(p),
|
|
672
|
-
image,
|
|
673
|
-
gallery: productGallery(p),
|
|
674
|
-
initial: (name[0] || "#").toUpperCase(),
|
|
675
|
-
shareUrl: productShareUrl(p),
|
|
676
|
-
cartCount: badge(),
|
|
677
|
-
emptyText: "",
|
|
678
|
-
});
|
|
679
|
-
}
|
|
680
|
-
// Offline: fall back to the last-synced cached product row before the error state.
|
|
681
|
-
if (synced) {
|
|
682
|
-
const name = String(synced.name ?? "") || "Product";
|
|
683
|
-
const image = String(synced.image ?? "");
|
|
684
|
-
// The cached list row carries no options, and guessing "none" would offer
|
|
685
|
-
// a one-tap Add to cart for a product that needs a colour and a size. So
|
|
686
|
-
// the offline detail shows no picker AND marks the options UNKNOWN, which
|
|
687
|
-
// keeps `optionsIncomplete` true: the CTA stays disabled and `addToCart`
|
|
688
|
-
// refuses. `cartHint` carries the reason, because the options card is hidden
|
|
689
|
-
// on `noOptions` and would otherwise leave a dead button unexplained.
|
|
690
734
|
lastProduct = undefined;
|
|
691
735
|
lastLoadedId = undefined;
|
|
692
|
-
return derive({
|
|
693
|
-
|
|
694
|
-
...keep,
|
|
695
|
-
status: "ready",
|
|
696
|
-
optionGroups: [],
|
|
697
|
-
selection: {},
|
|
698
|
-
optionsUnknown: true,
|
|
699
|
-
name,
|
|
700
|
-
price: String(synced.subtitle ?? ""),
|
|
701
|
-
wasPrice: "",
|
|
702
|
-
skuLabel: "",
|
|
703
|
-
returnPolicyBody: "",
|
|
704
|
-
shippingInfoBody: "",
|
|
705
|
-
description: "",
|
|
706
|
-
descriptionJson: "",
|
|
707
|
-
image,
|
|
708
|
-
initial: String(synced.initial ?? "") || (name[0] || "#").toUpperCase(),
|
|
709
|
-
cartHint: copyParam("product.cartHintOffline", "Reconnect to choose options and add this to your cart."),
|
|
710
|
-
cartCount: badge(),
|
|
711
|
-
emptyText: "",
|
|
712
|
-
});
|
|
713
|
-
}
|
|
714
|
-
lastProduct = undefined;
|
|
715
|
-
lastLoadedId = undefined;
|
|
716
|
-
return derive({ ...s, status: "error", optionGroups: [], selection: {}, optionsUnknown: false, descriptionJson: "", emptyText: copyParam("product.errorText", "Product could not load.") });
|
|
736
|
+
return derive({ ...s, status: "error", optionGroups: [], selection: {}, optionsUnknown: false, descriptionJson: "", emptyText: copyParam("product.errorText", "Product could not load.") });
|
|
737
|
+
});
|
|
717
738
|
},
|
|
718
739
|
// The stepper fires onChange with { value } on every tap; clamp 1–99 and
|
|
719
740
|
// ignore unparsable/garbage args instead of silently zeroing the quantity.
|