@wix/himalaya-cli 0.1352.0 → 0.1354.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/cli.mjs +87 -27
- package/dist/preview-assets.generated.bin +0 -0
- package/guides/web-page-native-sdk.md +2 -3
- package/guides/web-page-sdk-reference.md +1 -1
- package/package.json +2 -2
- package/templates/beauty-salon/tier5-src/himi-branded-lite-plans/index.ts +17 -0
- package/templates/beauty-salon/tier5-src/wix-spaces-plan-detail/index.ts +30 -4
- package/templates/branded-lite/tier5-src/himi-branded-lite-plans/index.ts +17 -0
- package/templates/branded-lite/tier5-src/himi-events-detail/index.ts +17 -3
- package/templates/branded-lite/tier5-src/wix-spaces-plan-detail/index.ts +30 -4
- package/templates/community-club/tier5-src/_shared/hosted-checkout.ts +42 -0
- package/templates/community-club/tier5-src/himi-events-detail/index.ts +17 -3
- package/templates/events-meetups/tier5-src/_shared/hosted-checkout.ts +42 -0
- package/templates/events-meetups/tier5-src/himi-events-detail/index.ts +17 -3
- package/templates/gym-fitness/tier5-src/himi-branded-lite-plans/index.ts +17 -0
- package/templates/gym-fitness/tier5-src/himi-events-detail/index.ts +17 -3
- package/templates/gym-fitness/tier5-src/wix-spaces-plan-detail/index.ts +30 -4
- package/templates/nonprofit-cause/tier5-src/_shared/hosted-checkout.ts +42 -0
- package/templates/nonprofit-cause/tier5-src/himi-branded-lite-plans/index.ts +17 -0
- package/templates/nonprofit-cause/tier5-src/himi-events-detail/index.ts +17 -3
- package/templates/nonprofit-cause/tier5-src/wix-spaces-plan-detail/index.ts +30 -4
- package/templates/podcast-creator/tier5-src/_shared/hosted-checkout.ts +42 -0
- package/templates/podcast-creator/tier5-src/himi-branded-lite-plans/index.ts +17 -0
- package/templates/podcast-creator/tier5-src/himi-events-detail/index.ts +17 -3
- package/templates/podcast-creator/tier5-src/wix-spaces-plan-detail/index.ts +30 -4
- package/templates/portfolio-creative/tier5-src/himi-events-detail/index.ts +17 -3
- package/templates/restaurant/tier5-src/_shared/hosted-checkout.ts +42 -0
- package/templates/restaurant/tier5-src/himi-events-detail/index.ts +17 -3
- package/templates/yoga-studio/tier5-src/himi-branded-lite-plans/index.ts +17 -0
- package/templates/yoga-studio/tier5-src/himi-events-detail/index.ts +17 -3
- package/templates/yoga-studio/tier5-src/wix-spaces-plan-detail/index.ts +30 -4
package/dist/cli.mjs
CHANGED
|
@@ -22017,7 +22017,7 @@ function usageStringCopyFor(permissionCopy) {
|
|
|
22017
22017
|
}
|
|
22018
22018
|
return out2;
|
|
22019
22019
|
}
|
|
22020
|
-
var BRIDGE_CAPABILITY_IDS, KNOWN_CAPABILITY_IDS, CAPABILITY_LITERAL, BRIDGE_MARKERS, NEEDS_BOOT_WIRING,
|
|
22020
|
+
var BRIDGE_CAPABILITY_IDS, KNOWN_CAPABILITY_IDS, CAPABILITY_LITERAL, BRIDGE_MARKERS, NEEDS_BOOT_WIRING, NOT_DIST_ADDABLE;
|
|
22021
22021
|
var init_known = __esm({
|
|
22022
22022
|
"../../core/sdk/capability-catalog/src/known.ts"() {
|
|
22023
22023
|
init_builtins();
|
|
@@ -22059,13 +22059,7 @@ var init_known = __esm({
|
|
|
22059
22059
|
// on Android the shell must pick Godot's libc++_shared over the core's (himi-game's godotStl).
|
|
22060
22060
|
godot: "needs the shell's GameSurface view and, on Android, its libc++_shared pick for the engine, which a build cannot add"
|
|
22061
22061
|
});
|
|
22062
|
-
|
|
22063
|
-
calls: "links the community libwebrtc engine (HimalayaCoreCalls / :himalaya-core-calls), whose third-party sign-off for store-shipping apps is still open, so a dist build does not add it to a customer binary"
|
|
22064
|
-
});
|
|
22065
|
-
NOT_DIST_ADDABLE = Object.freeze({
|
|
22066
|
-
...NEEDS_BOOT_WIRING,
|
|
22067
|
-
...AWAITING_THIRD_PARTY_SIGN_OFF
|
|
22068
|
-
});
|
|
22062
|
+
NOT_DIST_ADDABLE = NEEDS_BOOT_WIRING;
|
|
22069
22063
|
}
|
|
22070
22064
|
});
|
|
22071
22065
|
|
|
@@ -35387,6 +35381,69 @@ function resolveServeBase() {
|
|
|
35387
35381
|
}
|
|
35388
35382
|
return (raw ?? "").replace(/\/$/, "");
|
|
35389
35383
|
}
|
|
35384
|
+
function resolvePlaneControlReadBase() {
|
|
35385
|
+
const explicit = process.env.HIMALAYA_PLAYER_CONTROL_URL;
|
|
35386
|
+
if (explicit) return explicit.replace(/\/$/, "");
|
|
35387
|
+
const override = process.env.HIMALAYA_PLAYER_SERVE_URL;
|
|
35388
|
+
if (override) {
|
|
35389
|
+
const base = resolveServeBase();
|
|
35390
|
+
return sameOrigin(base, LOCAL_ORIGINS.serve) ? LOCAL_ORIGINS.control : base;
|
|
35391
|
+
}
|
|
35392
|
+
let control = "";
|
|
35393
|
+
try {
|
|
35394
|
+
control = resolveTarget().control ?? "";
|
|
35395
|
+
} catch {
|
|
35396
|
+
}
|
|
35397
|
+
const serve = resolveServeBase();
|
|
35398
|
+
return isLocalHostUrl(control) && isLocalHostUrl(serve) && !sameUrlOrigin(control, serve) ? control.replace(/\/$/, "") : serve;
|
|
35399
|
+
}
|
|
35400
|
+
function isLocalHostUrl(url) {
|
|
35401
|
+
let h2;
|
|
35402
|
+
try {
|
|
35403
|
+
h2 = new URL(url).hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
35404
|
+
} catch {
|
|
35405
|
+
return false;
|
|
35406
|
+
}
|
|
35407
|
+
if (h2 === "localhost" || h2 === "::1" || h2.endsWith(".local")) return true;
|
|
35408
|
+
const p2 = h2.split(".").map(Number);
|
|
35409
|
+
if (p2.length !== 4 || p2.some((n2) => !Number.isInteger(n2))) return false;
|
|
35410
|
+
return p2[0] === 127 || p2[0] === 10 || p2[0] === 192 && p2[1] === 168 || p2[0] === 169 && p2[1] === 254 || p2[0] === 172 && p2[1] >= 16 && p2[1] <= 31;
|
|
35411
|
+
}
|
|
35412
|
+
function sameUrlOrigin(a2, b2) {
|
|
35413
|
+
try {
|
|
35414
|
+
return new URL(a2).origin === new URL(b2).origin;
|
|
35415
|
+
} catch {
|
|
35416
|
+
return false;
|
|
35417
|
+
}
|
|
35418
|
+
}
|
|
35419
|
+
function isLoopbackUrl(url) {
|
|
35420
|
+
try {
|
|
35421
|
+
return ["127.0.0.1", "localhost", "[::1]"].includes(new URL(url).hostname);
|
|
35422
|
+
} catch {
|
|
35423
|
+
return false;
|
|
35424
|
+
}
|
|
35425
|
+
}
|
|
35426
|
+
function sameOrigin(url, local) {
|
|
35427
|
+
try {
|
|
35428
|
+
const a2 = new URL(url);
|
|
35429
|
+
const b2 = new URL(local);
|
|
35430
|
+
return isLoopbackUrl(url) && a2.port === b2.port && a2.protocol === b2.protocol && (a2.pathname === "/" || a2.pathname === "");
|
|
35431
|
+
} catch {
|
|
35432
|
+
return false;
|
|
35433
|
+
}
|
|
35434
|
+
}
|
|
35435
|
+
function tenantHostPlaneRead(url, headers2, hostApp, method = "GET") {
|
|
35436
|
+
if (!hostApp || !isTenantHost(hostApp) || method !== "GET" && method !== "HEAD") return null;
|
|
35437
|
+
const [path2, query = ""] = url.split("?");
|
|
35438
|
+
if (path2 === "/v1/site/member-app/resolve") return resolvePlaneControlReadBase() || null;
|
|
35439
|
+
if (path2 === "/v1/release/current") {
|
|
35440
|
+
const app = new URLSearchParams(query).get("app")?.trim();
|
|
35441
|
+
if (!app || app === hostApp || HOSTED_TENANTS[hostApp].includes(app)) return null;
|
|
35442
|
+
if (targetTenant(url, headers2, hostApp) !== null) return null;
|
|
35443
|
+
return resolveServeBase() || null;
|
|
35444
|
+
}
|
|
35445
|
+
return null;
|
|
35446
|
+
}
|
|
35390
35447
|
function targetTenant(url, headers2, hostApp) {
|
|
35391
35448
|
const hdr = headers2["x-himalaya-app"];
|
|
35392
35449
|
const headerApp = Array.isArray(hdr) ? hdr[0] : hdr;
|
|
@@ -35454,6 +35511,7 @@ var PLAYER_APP_ID, HOSTED_TENANTS, TENANT_PREFIX, LOOPBACK_PEERS, STRIP_RESPONSE
|
|
|
35454
35511
|
var init_player_proxy = __esm({
|
|
35455
35512
|
"../../core/dev-server/src/player-proxy.ts"() {
|
|
35456
35513
|
init_config();
|
|
35514
|
+
init_serve_origins();
|
|
35457
35515
|
PLAYER_APP_ID = "himi-player";
|
|
35458
35516
|
HOSTED_TENANTS = {
|
|
35459
35517
|
"himi-spaces-lite": ["himi-branded-lite"]
|
|
@@ -132468,6 +132526,8 @@ function createDevServer(opts = {}) {
|
|
|
132468
132526
|
forwardAs: clientHost ? { host: clientHost, prefix: `${TENANT_PREFIX}${encodeURIComponent(tenant)}` } : void 0
|
|
132469
132527
|
});
|
|
132470
132528
|
}
|
|
132529
|
+
const planeRead = tenantHostPlaneRead(url, req.headers, state.appName, req.method);
|
|
132530
|
+
if (planeRead) return await proxyToServe(planeRead, req, res);
|
|
132471
132531
|
}
|
|
132472
132532
|
if (url.split("?")[0] === "/v1/app-icon" && req.method === "GET") {
|
|
132473
132533
|
const iconApp = new URLSearchParams(url.split("?")[1] ?? "").get("app") ?? "";
|
|
@@ -156718,25 +156778,25 @@ var init_preview_assets_generated = __esm({
|
|
|
156718
156778
|
"../../packages/serve/src/preview-assets.generated.ts"() {
|
|
156719
156779
|
init_preview_assets_format();
|
|
156720
156780
|
PREVIEW_ASSET_MANIFEST = [
|
|
156721
|
-
{ key: "assets/index-
|
|
156722
|
-
{ key: "assets/pdf-
|
|
156723
|
-
{ key: "assets/pdf.worker.min-BQa_JDET.js", contentType: "application/javascript; charset=utf-8", offset:
|
|
156724
|
-
{ key: "assets/pdf.worker.min-BmVo14Nb.mjs", contentType: "application/javascript; charset=utf-8", offset:
|
|
156725
|
-
{ key: "assets/pdfDocument-
|
|
156726
|
-
{ key: "fonts/Roboto-LICENSE.txt", contentType: "application/octet-stream", offset:
|
|
156727
|
-
{ key: "fonts/Roboto-Variable.ttf", contentType: "font/ttf", offset:
|
|
156728
|
-
{ key: "fonts/WixMadeforDisplay-Bold.ttf", contentType: "font/ttf", offset:
|
|
156729
|
-
{ key: "fonts/WixMadeforDisplay-ExtraBold.ttf", contentType: "font/ttf", offset:
|
|
156730
|
-
{ key: "fonts/WixMadeforDisplay-Medium.ttf", contentType: "font/ttf", offset:
|
|
156731
|
-
{ key: "fonts/WixMadeforDisplay-Regular.ttf", contentType: "font/ttf", offset:
|
|
156732
|
-
{ key: "fonts/WixMadeforDisplay-SemiBold.ttf", contentType: "font/ttf", offset:
|
|
156733
|
-
{ key: "fonts/WixMadeforText-Bold.ttf", contentType: "font/ttf", offset:
|
|
156734
|
-
{ key: "fonts/WixMadeforText-ExtraBold.ttf", contentType: "font/ttf", offset:
|
|
156735
|
-
{ key: "fonts/WixMadeforText-Medium.ttf", contentType: "font/ttf", offset:
|
|
156736
|
-
{ key: "fonts/WixMadeforText-Regular.ttf", contentType: "font/ttf", offset:
|
|
156737
|
-
{ key: "fonts/WixMadeforText-SemiBold.ttf", contentType: "font/ttf", offset:
|
|
156738
|
-
{ key: "index.html", contentType: "text/html; charset=utf-8", offset:
|
|
156739
|
-
{ key: "t5-frame.html", contentType: "text/html; charset=utf-8", offset:
|
|
156781
|
+
{ key: "assets/index-CfDtWTd9.js", contentType: "application/javascript; charset=utf-8", offset: 8, length: 1271649, sha256: "b09aff3c4ca8b6d279140f389c9c2c85988dd12e78414ac3e1584f72456ee566" },
|
|
156782
|
+
{ key: "assets/pdf-CVazw9Ub.js", contentType: "application/javascript; charset=utf-8", offset: 1271657, length: 488199, sha256: "e42ce9b09b54fe70642b68c38e7209b4a357177f277ef8c40faa3262f1853cde" },
|
|
156783
|
+
{ key: "assets/pdf.worker.min-BQa_JDET.js", contentType: "application/javascript; charset=utf-8", offset: 1759856, length: 73, sha256: "41bcfda4f3c8c0a81c785ea87c69afc8e7d0ed8c333324aee94c273d881bb6aa" },
|
|
156784
|
+
{ key: "assets/pdf.worker.min-BmVo14Nb.mjs", contentType: "application/javascript; charset=utf-8", offset: 1759929, length: 1317034, sha256: "a33cfe728c584fdba4fcc1fd54bcdc2f9f2f13889ddbb5b2bd1d0f8cbe49b84e" },
|
|
156785
|
+
{ key: "assets/pdfDocument-HGrmnRP0.js", contentType: "application/javascript; charset=utf-8", offset: 3076963, length: 2858, sha256: "abf488e7017dd90516a2ada53ec9f0f9bf9402173c8f5b7483917c4d60b03411" },
|
|
156786
|
+
{ key: "fonts/Roboto-LICENSE.txt", contentType: "application/octet-stream", offset: 3079821, length: 4610, sha256: "994062dd7af4186a739de55dd9e151c63b6301f8e2cbafc608d35c2e87da0cd2" },
|
|
156787
|
+
{ key: "fonts/Roboto-Variable.ttf", contentType: "font/ttf", offset: 3084431, length: 488584, sha256: "d7598e12c5dbef095ff8272cfc55da0250bd07fbdecbac8a530b9b277872a134" },
|
|
156788
|
+
{ key: "fonts/WixMadeforDisplay-Bold.ttf", contentType: "font/ttf", offset: 3573015, length: 90924, sha256: "ec4c7d63772d980d6e147b2df6215fd2737e4b04cebe0cf7e11dc695fb63cdcf" },
|
|
156789
|
+
{ key: "fonts/WixMadeforDisplay-ExtraBold.ttf", contentType: "font/ttf", offset: 3663939, length: 90984, sha256: "bf4c16a2774edd38a921edf3099469866cd74c094fd596f3fa4e81f54a04dfaa" },
|
|
156790
|
+
{ key: "fonts/WixMadeforDisplay-Medium.ttf", contentType: "font/ttf", offset: 3754923, length: 90980, sha256: "368faca44fb9f7207e4af784e94750b1ee59c1098b8689e0f4a918926f244fb5" },
|
|
156791
|
+
{ key: "fonts/WixMadeforDisplay-Regular.ttf", contentType: "font/ttf", offset: 3845903, length: 90872, sha256: "97ed8f1a6772abe26e3d8af5cecfc4b02faa94497d585b0398b5053b739a70c1" },
|
|
156792
|
+
{ key: "fonts/WixMadeforDisplay-SemiBold.ttf", contentType: "font/ttf", offset: 3936775, length: 90980, sha256: "642feab8a48ddf6c7a89d7c2a7c0edfa0e6e773f671287e460e222703563191e" },
|
|
156793
|
+
{ key: "fonts/WixMadeforText-Bold.ttf", contentType: "font/ttf", offset: 4027755, length: 108636, sha256: "37c179b4fefb63ea9637322b84583a2df53aa226c150294eea1675c14b1b8e29" },
|
|
156794
|
+
{ key: "fonts/WixMadeforText-ExtraBold.ttf", contentType: "font/ttf", offset: 4136391, length: 109084, sha256: "62e2fe4718129f36ecdb0ae4de385bd6b81ecc876146447167c6c140fad699a9" },
|
|
156795
|
+
{ key: "fonts/WixMadeforText-Medium.ttf", contentType: "font/ttf", offset: 4245475, length: 109048, sha256: "4ecf270136af928612c76fe4ebade91d8156975e5ea4c0cf9225501dd1788e02" },
|
|
156796
|
+
{ key: "fonts/WixMadeforText-Regular.ttf", contentType: "font/ttf", offset: 4354523, length: 107700, sha256: "dee56f09fd3efc127c1ea95557edc74110c1460a46287da36e0af25efd341ef5" },
|
|
156797
|
+
{ key: "fonts/WixMadeforText-SemiBold.ttf", contentType: "font/ttf", offset: 4462223, length: 88108, sha256: "dced97a77624bc5f8b1ae65850360908297f4588e09feb6f22792d29dc7e765b" },
|
|
156798
|
+
{ key: "index.html", contentType: "text/html; charset=utf-8", offset: 4550331, length: 4947, sha256: "d9b156d2824937902486f47a6250a5589f6e33f224eef7bd512506612779159e" },
|
|
156799
|
+
{ key: "t5-frame.html", contentType: "text/html; charset=utf-8", offset: 4555278, length: 17512, sha256: "c5c8612a8b3f5a1828d0c6593106036a180eea11da80232f5181548a50679137" }
|
|
156740
156800
|
];
|
|
156741
156801
|
}
|
|
156742
156802
|
});
|
|
Binary file
|
|
@@ -39,10 +39,9 @@ Your page can only use capabilities the **app owner** turned on for your site. I
|
|
|
39
39
|
`capabilities`, allowing **every** method the capability's page lists — except
|
|
40
40
|
`presigned_upload.putFile`, which reads a device file and is allowed only when `files` is enabled
|
|
41
41
|
too. So enabling a capability is enough; the owner does not list methods.
|
|
42
|
-
- **What a Brandy app cannot enable:** `app_info`, `
|
|
42
|
+
- **What a Brandy app cannot enable:** `app_info`, `godot`, `himi_apps`, `live_activity`,
|
|
43
43
|
`local_database`, `release_control`, `system_intents`, `ui` and `wix_platform` — most need wiring
|
|
44
|
-
a website app's shell does not have,
|
|
45
|
-
engine, so plan your page without them. `base44` works only when the
|
|
44
|
+
a website app's shell does not have, so plan your page without them. `base44` works only when the
|
|
46
45
|
app also has a Base44 binding. Every other capability in the reference can be enabled; other Himi
|
|
47
46
|
apps (not Brandy) may offer these too.
|
|
48
47
|
- Enabling a capability changes the app's native permissions, so the owner ships a **new app
|
|
@@ -20,7 +20,7 @@ the SDK sources, so it matches the shipped SDKs.
|
|
|
20
20
|
- [`biometrics`](https://apps.wix.com/_api/himi-serve/authoring/resource/web-sdk%2Fbiometrics?format=md) — Biometric authentication capability. 2 methods.
|
|
21
21
|
- [`browser_auth`](https://apps.wix.com/_api/himi-serve/authoring/resource/web-sdk%2Fbrowser_auth?format=md) — Native browser SSO. 1 method.
|
|
22
22
|
- [`calendar`](https://apps.wix.com/_api/himi-serve/authoring/resource/web-sdk%2Fcalendar?format=md) — Device calendar. 6 methods.
|
|
23
|
-
- [`calls`](https://apps.wix.com/_api/himi-serve/authoring/resource/web-sdk%2Fcalls?format=md) — Real-time 1:1 audio calls (Stage 1). 16 methods
|
|
23
|
+
- [`calls`](https://apps.wix.com/_api/himi-serve/authoring/resource/web-sdk%2Fcalls?format=md) — Real-time 1:1 audio calls (Stage 1). 16 methods.
|
|
24
24
|
- [`camera`](https://apps.wix.com/_api/himi-serve/authoring/resource/web-sdk%2Fcamera?format=md) — Camera capability. 3 methods.
|
|
25
25
|
- [`clipboard`](https://apps.wix.com/_api/himi-serve/authoring/resource/web-sdk%2Fclipboard?format=md) — System clipboard. 3 methods.
|
|
26
26
|
- [`contacts`](https://apps.wix.com/_api/himi-serve/authoring/resource/web-sdk%2Fcontacts?format=md) — Device address book. 4 methods.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/himalaya-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1354.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": "07e69214f20355a26a4e813ff46d40f5f5e4f8eaff658f0598038078"
|
|
75
75
|
}
|
|
@@ -9,6 +9,7 @@ import { hostSessionFromGlobal, iapPriceFields, type IapCatalogItem } from "@him
|
|
|
9
9
|
import { createIapCommerce, pricingPlanOrders } from "@himalaya/iap-checkout";
|
|
10
10
|
import { iapPriceCopy, iapPurchaseCopy, resetUnhydratedKeysOnOlderRuntime } from "../_shared/iap-worker.js";
|
|
11
11
|
import { formatCurrency } from "../_shared/engine-intl.js";
|
|
12
|
+
import { isHostedWorker } from "../_shared/hosted-checkout.js";
|
|
12
13
|
|
|
13
14
|
/** Wix Pricing Plans appDefId — the vertical that owns a plan order, so the `appId` its
|
|
14
15
|
* cashier order is paid under (mirrors AppDefId.PRICING_PLANS in Wix's RN apps). */
|
|
@@ -49,6 +50,8 @@ interface PlanRow {
|
|
|
49
50
|
hasBadge: boolean;
|
|
50
51
|
/** Purchasable URL when the plan data carries one; "" otherwise. */
|
|
51
52
|
joinUrl: string;
|
|
53
|
+
/** No price (or a zero price): an online order activates it at once, with nothing to pay. */
|
|
54
|
+
free: boolean;
|
|
52
55
|
joined: boolean;
|
|
53
56
|
appleSupported: boolean;
|
|
54
57
|
joinLabel: string;
|
|
@@ -117,6 +120,11 @@ function joinUrl(plan: WixPlan): string {
|
|
|
117
120
|
return "";
|
|
118
121
|
}
|
|
119
122
|
|
|
123
|
+
function isFreePlan(plan: WixPlan): boolean {
|
|
124
|
+
const raw = plan.pricing?.price?.value;
|
|
125
|
+
return raw == null || raw === "" || Number(raw) === 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
120
128
|
function planRow(plan: WixPlan): PlanRow {
|
|
121
129
|
const name = (plan.name || copyParam("plans.planFallbackName", "Plan")).trim();
|
|
122
130
|
const perks = (plan.perks?.values ?? [])
|
|
@@ -142,6 +150,7 @@ function planRow(plan: WixPlan): PlanRow {
|
|
|
142
150
|
noBadge: !badge,
|
|
143
151
|
hasBadge: !!badge,
|
|
144
152
|
joinUrl: joinUrl(plan),
|
|
153
|
+
free: isFreePlan(plan),
|
|
145
154
|
appleSupported: plan.pricing?.singlePaymentUnlimited === true,
|
|
146
155
|
joined: false, joinLabel: copyParam("plans.joinCta", "Join"), joinDisabled: true, joinNote: "",
|
|
147
156
|
};
|
|
@@ -215,6 +224,14 @@ async function regularJoinMessage(row: PlanRow, reference: { kind: string; id: s
|
|
|
215
224
|
} catch {
|
|
216
225
|
}
|
|
217
226
|
}
|
|
227
|
+
// Hosted inside a host shell (Spaces) a plan cannot be paid in-app: classic Pricing Plans is not on
|
|
228
|
+
// ecom, so the host's member checkout has no checkout to open, and the redirect session needs a
|
|
229
|
+
// headless OAuth client a hosted app does not have. An online order created here would only be
|
|
230
|
+
// left behind as a DRAFT, so none is — the member joins on the site. A FREE plan has nothing to
|
|
231
|
+
// pay: its online order activates at once, so it still joins in-app.
|
|
232
|
+
if (isHostedWorker() && !row.free) {
|
|
233
|
+
return copyTemplate("plans.joinOnSiteMessage", "Memberships are purchased on the website — visit the site to join {name}.", { name: row.name });
|
|
234
|
+
}
|
|
218
235
|
try {
|
|
219
236
|
const created = await createOnlineOrder(row.id);
|
|
220
237
|
const snapshotId = created.purchaseFlowId;
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
// Tier-5 bundle: Wix Spaces clone — Pricing plan detail (ON-DEVICE, site-scoped).
|
|
2
|
-
// himalaya.input.id carries the plan id
|
|
2
|
+
// himalaya.input.id carries the plan id.
|
|
3
|
+
//
|
|
4
|
+
// Read from the PUBLIC plans (GET /pricing-plans/v2/plans/public — what the plans list
|
|
5
|
+
// shows), not from getPlan: GET /pricing-plans/v2/plans/{id} is the management read, and a
|
|
6
|
+
// hosted site's member or visitor gets a 403 from it, which rendered "couldn't be loaded"
|
|
7
|
+
// for every plan the list had just shown. getPlan stays as the fallback for a plan that is
|
|
8
|
+
// not public, for a caller that may read it (an owner-scope host).
|
|
3
9
|
import { withIapPresentation } from "../_shared/iap-presentation.js";
|
|
4
|
-
import { getPlan, type WixPlan } from "@himalaya/wix-pricing-plans";
|
|
10
|
+
import { getPlan, listPublicPlans, type WixPlan } from "@himalaya/wix-pricing-plans";
|
|
5
11
|
import { presence, presenceAfterError, detailInputId, type PresenceFlags } from "../_shared/detail-presence.js";
|
|
6
12
|
|
|
7
13
|
interface S extends PresenceFlags { status: "loading" | "ready"; name: string; price: string; description: string; perks: string }
|
|
@@ -12,6 +18,26 @@ function priceOf(p: WixPlan): string {
|
|
|
12
18
|
return m.formattedAmount || [m.amount ?? m.value, m.currency].filter(Boolean).join(" ");
|
|
13
19
|
}
|
|
14
20
|
|
|
21
|
+
/** The plan, or null when it is not public and the caller may not read it otherwise. */
|
|
22
|
+
async function readPlan(id: string): Promise<WixPlan | null> {
|
|
23
|
+
let publicRead = false;
|
|
24
|
+
try {
|
|
25
|
+
const listed = await listPublicPlans();
|
|
26
|
+
publicRead = true;
|
|
27
|
+
const hit = (listed?.plans ?? []).find((plan) => plan?.id === id);
|
|
28
|
+
if (hit) return hit;
|
|
29
|
+
} catch (e) { void e; }
|
|
30
|
+
try {
|
|
31
|
+
const res = await getPlan(id);
|
|
32
|
+
return ((res as unknown as { plan?: WixPlan }).plan) ?? (res as unknown as WixPlan);
|
|
33
|
+
} catch (e) {
|
|
34
|
+
// The public read worked, so the device is online: a refused or empty management
|
|
35
|
+
// read means this plan is not one the viewer can see — not-found, not offline.
|
|
36
|
+
if (publicRead) return null;
|
|
37
|
+
throw e;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
15
41
|
const defineService = withIapPresentation<S>({
|
|
16
42
|
prefix: "plans", transientKeys: ["price"], contentActions: ["load"],
|
|
17
43
|
items: state => detailInputId() ? [{ reference: { kind: "pricingPlan", id: detailInputId() }, commerceDisplayPrice: state.price }] : [],
|
|
@@ -24,8 +50,8 @@ const svc = defineService<S, { load: (s: S) => Promise<S> }>("spacesPlanDetail",
|
|
|
24
50
|
const id = detailInputId();
|
|
25
51
|
if (!id) return { ...s, status: "ready", ...presence("missing") };
|
|
26
52
|
try {
|
|
27
|
-
const
|
|
28
|
-
|
|
53
|
+
const p = await readPlan(id);
|
|
54
|
+
if (!p) return { ...s, status: "ready", ...presence("missing") };
|
|
29
55
|
const perks = (p.perks?.values ?? []).map((v) => `• ${v}`).join("\n");
|
|
30
56
|
return { status: "ready", name: p.name || "Plan", price: priceOf(p), description: p.description || "", perks, ...presence("found") };
|
|
31
57
|
} catch (e) { return { ...s, status: "ready", ...presenceAfterError(e) }; }
|
|
@@ -9,6 +9,7 @@ import { hostSessionFromGlobal, iapPriceFields, type IapCatalogItem } from "@him
|
|
|
9
9
|
import { createIapCommerce, pricingPlanOrders } from "@himalaya/iap-checkout";
|
|
10
10
|
import { iapPriceCopy, iapPurchaseCopy, resetUnhydratedKeysOnOlderRuntime } from "../_shared/iap-worker.js";
|
|
11
11
|
import { formatCurrency } from "../_shared/engine-intl.js";
|
|
12
|
+
import { isHostedWorker } from "../_shared/hosted-checkout.js";
|
|
12
13
|
|
|
13
14
|
/** Wix Pricing Plans appDefId — the vertical that owns a plan order, so the `appId` its
|
|
14
15
|
* cashier order is paid under (mirrors AppDefId.PRICING_PLANS in Wix's RN apps). */
|
|
@@ -49,6 +50,8 @@ interface PlanRow {
|
|
|
49
50
|
hasBadge: boolean;
|
|
50
51
|
/** Purchasable URL when the plan data carries one; "" otherwise. */
|
|
51
52
|
joinUrl: string;
|
|
53
|
+
/** No price (or a zero price): an online order activates it at once, with nothing to pay. */
|
|
54
|
+
free: boolean;
|
|
52
55
|
joined: boolean;
|
|
53
56
|
appleSupported: boolean;
|
|
54
57
|
joinLabel: string;
|
|
@@ -117,6 +120,11 @@ function joinUrl(plan: WixPlan): string {
|
|
|
117
120
|
return "";
|
|
118
121
|
}
|
|
119
122
|
|
|
123
|
+
function isFreePlan(plan: WixPlan): boolean {
|
|
124
|
+
const raw = plan.pricing?.price?.value;
|
|
125
|
+
return raw == null || raw === "" || Number(raw) === 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
120
128
|
function planRow(plan: WixPlan): PlanRow {
|
|
121
129
|
const name = (plan.name || copyParam("plans.planFallbackName", "Plan")).trim();
|
|
122
130
|
const perks = (plan.perks?.values ?? [])
|
|
@@ -142,6 +150,7 @@ function planRow(plan: WixPlan): PlanRow {
|
|
|
142
150
|
noBadge: !badge,
|
|
143
151
|
hasBadge: !!badge,
|
|
144
152
|
joinUrl: joinUrl(plan),
|
|
153
|
+
free: isFreePlan(plan),
|
|
145
154
|
appleSupported: plan.pricing?.singlePaymentUnlimited === true,
|
|
146
155
|
joined: false, joinLabel: copyParam("plans.joinCta", "Join"), joinDisabled: true, joinNote: "",
|
|
147
156
|
};
|
|
@@ -215,6 +224,14 @@ async function regularJoinMessage(row: PlanRow, reference: { kind: string; id: s
|
|
|
215
224
|
} catch {
|
|
216
225
|
}
|
|
217
226
|
}
|
|
227
|
+
// Hosted inside a host shell (Spaces) a plan cannot be paid in-app: classic Pricing Plans is not on
|
|
228
|
+
// ecom, so the host's member checkout has no checkout to open, and the redirect session needs a
|
|
229
|
+
// headless OAuth client a hosted app does not have. An online order created here would only be
|
|
230
|
+
// left behind as a DRAFT, so none is — the member joins on the site. A FREE plan has nothing to
|
|
231
|
+
// pay: its online order activates at once, so it still joins in-app.
|
|
232
|
+
if (isHostedWorker() && !row.free) {
|
|
233
|
+
return copyTemplate("plans.joinOnSiteMessage", "Memberships are purchased on the website — visit the site to join {name}.", { name: row.name });
|
|
234
|
+
}
|
|
218
235
|
try {
|
|
219
236
|
const created = await createOnlineOrder(row.id);
|
|
220
237
|
const snapshotId = created.purchaseFlowId;
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
queryAvailableTickets,
|
|
14
14
|
queryEvents,
|
|
15
15
|
type WixAvailableTicket,
|
|
16
|
+
type WixEventFieldset,
|
|
16
17
|
type WixEvent,
|
|
17
18
|
type WixGuestRsvp,
|
|
18
19
|
type WixGuestTicket,
|
|
@@ -34,6 +35,7 @@ import {
|
|
|
34
35
|
type EventRow,
|
|
35
36
|
} from "../_shared/himi-events";
|
|
36
37
|
import { initials } from "../_shared/himi-text.js";
|
|
38
|
+
import { isHostedWorker } from "../_shared/hosted-checkout.js";
|
|
37
39
|
import { stashCopy, copyParam, copyTemplate } from "../_shared/himi-copy.js";
|
|
38
40
|
import { openRichLink } from "../_shared/rich-content-link.js";
|
|
39
41
|
import { richBody } from "../_shared/rich-body.js";
|
|
@@ -281,6 +283,10 @@ function calendarFallbackUrl(s: S, end: number): string {
|
|
|
281
283
|
return `https://calendar.google.com/calendar/render?${parts.join("&")}`;
|
|
282
284
|
}
|
|
283
285
|
|
|
286
|
+
/** The Events v3 fieldsets the detail reads (see `WixEventFieldset`). DETAILS carries mainImage and
|
|
287
|
+
* shortDescription: REGISTRATION fills eventType, which skips the query enrichment that used to supply them. */
|
|
288
|
+
export const EVENT_DETAIL_FIELDSETS: WixEventFieldset[] = ["DETAILS", "TEXTS", "REGISTRATION", "URLS"];
|
|
289
|
+
|
|
284
290
|
/** Map the decoded registration/eventType onto the CTA variant. The decoded
|
|
285
291
|
* `eventType` already folds registration.type / registration.initialType /
|
|
286
292
|
* the legacy flat `type` (decodeEvent, #478). Unknown ⇒ RSVP (historical). */
|
|
@@ -458,8 +464,11 @@ const svc = defineService<S, {
|
|
|
458
464
|
const id = s.id || inputId();
|
|
459
465
|
if (!id) return derive({ ...s, status: "ready", errored: true });
|
|
460
466
|
try {
|
|
461
|
-
// TEXTS adds the Ricos `description` to the base fieldset (a bare read returns none)
|
|
462
|
-
|
|
467
|
+
// TEXTS adds the Ricos `description` to the base fieldset (a bare read returns none);
|
|
468
|
+
// REGISTRATION adds `registration` (the CTA's type, and an external event's own URL);
|
|
469
|
+
// URLS adds `eventPageUrl` — the registration fallback and the share link. Without URLS a
|
|
470
|
+
// ticket flow that cannot complete in-app had no page to send the buyer to.
|
|
471
|
+
const res = await getEvent(id, { fields: EVENT_DETAIL_FIELDSETS });
|
|
463
472
|
let e = ((res as unknown as { event?: WixEvent }).event ?? res) as WixEvent;
|
|
464
473
|
e = await enrichRegistration(id, e);
|
|
465
474
|
const attendees = await attendeeSummary(id);
|
|
@@ -566,7 +575,12 @@ const svc = defineService<S, {
|
|
|
566
575
|
// Every failure degrades one rung at a time, never to a dead button:
|
|
567
576
|
// native chain → hosted eventsCheckout (pinned to the SAME reservation
|
|
568
577
|
// when we got one, else the slug) → the event page / registration URL.
|
|
569
|
-
|
|
578
|
+
// Hosted inside a host shell (Spaces) neither in-app rung can finish: the classic
|
|
579
|
+
// `checkoutReservation` needs a buyer + guest form this screen does not collect, and the
|
|
580
|
+
// redirect session needs a headless OAuth client a hosted app does not have. Skip both —
|
|
581
|
+
// the native chain would also leave a ticket reservation held for nothing — and open the
|
|
582
|
+
// event's own page, where the member registers on the site.
|
|
583
|
+
if (s.registrationKind === "tickets" && s.id && !isHostedWorker()) {
|
|
570
584
|
let reservationId = "";
|
|
571
585
|
try {
|
|
572
586
|
// Buy what the buyer PICKED. `load` seeds the pick with the cheapest
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
// Tier-5 bundle: Wix Spaces clone — Pricing plan detail (ON-DEVICE, site-scoped).
|
|
2
|
-
// himalaya.input.id carries the plan id
|
|
2
|
+
// himalaya.input.id carries the plan id.
|
|
3
|
+
//
|
|
4
|
+
// Read from the PUBLIC plans (GET /pricing-plans/v2/plans/public — what the plans list
|
|
5
|
+
// shows), not from getPlan: GET /pricing-plans/v2/plans/{id} is the management read, and a
|
|
6
|
+
// hosted site's member or visitor gets a 403 from it, which rendered "couldn't be loaded"
|
|
7
|
+
// for every plan the list had just shown. getPlan stays as the fallback for a plan that is
|
|
8
|
+
// not public, for a caller that may read it (an owner-scope host).
|
|
3
9
|
import { withIapPresentation } from "../_shared/iap-presentation.js";
|
|
4
|
-
import { getPlan, type WixPlan } from "@himalaya/wix-pricing-plans";
|
|
10
|
+
import { getPlan, listPublicPlans, type WixPlan } from "@himalaya/wix-pricing-plans";
|
|
5
11
|
import { presence, presenceAfterError, detailInputId, type PresenceFlags } from "../_shared/detail-presence.js";
|
|
6
12
|
|
|
7
13
|
interface S extends PresenceFlags { status: "loading" | "ready"; name: string; price: string; description: string; perks: string }
|
|
@@ -12,6 +18,26 @@ function priceOf(p: WixPlan): string {
|
|
|
12
18
|
return m.formattedAmount || [m.amount ?? m.value, m.currency].filter(Boolean).join(" ");
|
|
13
19
|
}
|
|
14
20
|
|
|
21
|
+
/** The plan, or null when it is not public and the caller may not read it otherwise. */
|
|
22
|
+
async function readPlan(id: string): Promise<WixPlan | null> {
|
|
23
|
+
let publicRead = false;
|
|
24
|
+
try {
|
|
25
|
+
const listed = await listPublicPlans();
|
|
26
|
+
publicRead = true;
|
|
27
|
+
const hit = (listed?.plans ?? []).find((plan) => plan?.id === id);
|
|
28
|
+
if (hit) return hit;
|
|
29
|
+
} catch (e) { void e; }
|
|
30
|
+
try {
|
|
31
|
+
const res = await getPlan(id);
|
|
32
|
+
return ((res as unknown as { plan?: WixPlan }).plan) ?? (res as unknown as WixPlan);
|
|
33
|
+
} catch (e) {
|
|
34
|
+
// The public read worked, so the device is online: a refused or empty management
|
|
35
|
+
// read means this plan is not one the viewer can see — not-found, not offline.
|
|
36
|
+
if (publicRead) return null;
|
|
37
|
+
throw e;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
15
41
|
const defineService = withIapPresentation<S>({
|
|
16
42
|
prefix: "plans", transientKeys: ["price"], contentActions: ["load"],
|
|
17
43
|
items: state => detailInputId() ? [{ reference: { kind: "pricingPlan", id: detailInputId() }, commerceDisplayPrice: state.price }] : [],
|
|
@@ -24,8 +50,8 @@ const svc = defineService<S, { load: (s: S) => Promise<S> }>("spacesPlanDetail",
|
|
|
24
50
|
const id = detailInputId();
|
|
25
51
|
if (!id) return { ...s, status: "ready", ...presence("missing") };
|
|
26
52
|
try {
|
|
27
|
-
const
|
|
28
|
-
|
|
53
|
+
const p = await readPlan(id);
|
|
54
|
+
if (!p) return { ...s, status: "ready", ...presence("missing") };
|
|
29
55
|
const perks = (p.perks?.values ?? []).map((v) => `• ${v}`).join("\n");
|
|
30
56
|
return { status: "ready", name: p.name || "Plan", price: priceOf(p), description: p.description || "", perks, ...presence("found") };
|
|
31
57
|
} catch (e) { return { ...s, status: "ready", ...presenceAfterError(e) }; }
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Hosted checkout (#4089 B3). A Branded Lite hosted in a host shell (Spaces)
|
|
2
|
+
// pays as the member the host lent it: Headless `createRedirectSession` does not take a Member
|
|
3
|
+
// instance, so the HOST opens the site's checkout signed in as that member. The worker names only
|
|
4
|
+
// its ecom checkout; the host reads Wix's checkout URL, mints a one-time member session and opens
|
|
5
|
+
// the site's `createSessionCookie` → checkout. The session token never reaches this worker.
|
|
6
|
+
|
|
7
|
+
import { dispatch } from "@himalaya/dispatch";
|
|
8
|
+
import { hostSiteFromInput } from "./branded-lite-hosted.js";
|
|
9
|
+
|
|
10
|
+
/** The host verbs (registered by the host shell, never by the app itself). The first prepares the
|
|
11
|
+
* checkout and answers a one-time ticket; the second opens it. Two steps so the caller's charge guard
|
|
12
|
+
* is re-read after the host's network round trips and right before the checkout opens — the prepared
|
|
13
|
+
* URL (which carries the member's session token) stays in the host; only the ticket crosses here. */
|
|
14
|
+
export const HOSTED_CHECKOUT_ACTION = "himi.host.memberCheckout";
|
|
15
|
+
export const HOSTED_CHECKOUT_OPEN_ACTION = "himi.host.memberCheckout.open";
|
|
16
|
+
|
|
17
|
+
/** Whether this worker runs inside a host that opened a site for it. */
|
|
18
|
+
export function isHostedWorker(): boolean {
|
|
19
|
+
return hostSiteFromInput((globalThis as { himalaya?: { input?: unknown } }).himalaya?.input) !== null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type HostAnswer = { opened?: unknown; ticket?: unknown; payload?: { opened?: unknown; ticket?: unknown } } | null;
|
|
23
|
+
|
|
24
|
+
/** `payCashierOrder`'s `openCashier` for a hosted app: the host prepares [checkoutId]'s checkout as the
|
|
25
|
+
* lent member, the caller's guard is re-read, and only then does the host open it. Resolves whether it
|
|
26
|
+
* opened ("withdrawn" when the guard said no); a host without the verbs (or that refused) → false. */
|
|
27
|
+
export function hostedCheckoutOpener(checkoutId: string): (stillMayCharge: () => Promise<boolean>) => Promise<boolean | "withdrawn"> {
|
|
28
|
+
return async (stillMayCharge) => {
|
|
29
|
+
try {
|
|
30
|
+
const prepared = await dispatch(HOSTED_CHECKOUT_ACTION, { checkoutId }) as HostAnswer;
|
|
31
|
+
const ticket = prepared?.ticket ?? prepared?.payload?.ticket;
|
|
32
|
+
if (typeof ticket !== "string" || !ticket) return false;
|
|
33
|
+
// An unopened ticket simply expires in the host: nothing to cancel.
|
|
34
|
+
if (!(await stillMayCharge())) return "withdrawn";
|
|
35
|
+
const result = await dispatch(HOSTED_CHECKOUT_OPEN_ACTION, { ticket }) as HostAnswer;
|
|
36
|
+
const opened = result?.opened ?? result?.payload?.opened;
|
|
37
|
+
return opened === true || opened === "true";
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
queryAvailableTickets,
|
|
14
14
|
queryEvents,
|
|
15
15
|
type WixAvailableTicket,
|
|
16
|
+
type WixEventFieldset,
|
|
16
17
|
type WixEvent,
|
|
17
18
|
type WixGuestRsvp,
|
|
18
19
|
type WixGuestTicket,
|
|
@@ -34,6 +35,7 @@ import {
|
|
|
34
35
|
type EventRow,
|
|
35
36
|
} from "../_shared/himi-events";
|
|
36
37
|
import { initials } from "../_shared/himi-text.js";
|
|
38
|
+
import { isHostedWorker } from "../_shared/hosted-checkout.js";
|
|
37
39
|
import { stashCopy, copyParam, copyTemplate } from "../_shared/himi-copy.js";
|
|
38
40
|
import { openRichLink } from "../_shared/rich-content-link.js";
|
|
39
41
|
import { richBody } from "../_shared/rich-body.js";
|
|
@@ -281,6 +283,10 @@ function calendarFallbackUrl(s: S, end: number): string {
|
|
|
281
283
|
return `https://calendar.google.com/calendar/render?${parts.join("&")}`;
|
|
282
284
|
}
|
|
283
285
|
|
|
286
|
+
/** The Events v3 fieldsets the detail reads (see `WixEventFieldset`). DETAILS carries mainImage and
|
|
287
|
+
* shortDescription: REGISTRATION fills eventType, which skips the query enrichment that used to supply them. */
|
|
288
|
+
export const EVENT_DETAIL_FIELDSETS: WixEventFieldset[] = ["DETAILS", "TEXTS", "REGISTRATION", "URLS"];
|
|
289
|
+
|
|
284
290
|
/** Map the decoded registration/eventType onto the CTA variant. The decoded
|
|
285
291
|
* `eventType` already folds registration.type / registration.initialType /
|
|
286
292
|
* the legacy flat `type` (decodeEvent, #478). Unknown ⇒ RSVP (historical). */
|
|
@@ -458,8 +464,11 @@ const svc = defineService<S, {
|
|
|
458
464
|
const id = s.id || inputId();
|
|
459
465
|
if (!id) return derive({ ...s, status: "ready", errored: true });
|
|
460
466
|
try {
|
|
461
|
-
// TEXTS adds the Ricos `description` to the base fieldset (a bare read returns none)
|
|
462
|
-
|
|
467
|
+
// TEXTS adds the Ricos `description` to the base fieldset (a bare read returns none);
|
|
468
|
+
// REGISTRATION adds `registration` (the CTA's type, and an external event's own URL);
|
|
469
|
+
// URLS adds `eventPageUrl` — the registration fallback and the share link. Without URLS a
|
|
470
|
+
// ticket flow that cannot complete in-app had no page to send the buyer to.
|
|
471
|
+
const res = await getEvent(id, { fields: EVENT_DETAIL_FIELDSETS });
|
|
463
472
|
let e = ((res as unknown as { event?: WixEvent }).event ?? res) as WixEvent;
|
|
464
473
|
e = await enrichRegistration(id, e);
|
|
465
474
|
const attendees = await attendeeSummary(id);
|
|
@@ -566,7 +575,12 @@ const svc = defineService<S, {
|
|
|
566
575
|
// Every failure degrades one rung at a time, never to a dead button:
|
|
567
576
|
// native chain → hosted eventsCheckout (pinned to the SAME reservation
|
|
568
577
|
// when we got one, else the slug) → the event page / registration URL.
|
|
569
|
-
|
|
578
|
+
// Hosted inside a host shell (Spaces) neither in-app rung can finish: the classic
|
|
579
|
+
// `checkoutReservation` needs a buyer + guest form this screen does not collect, and the
|
|
580
|
+
// redirect session needs a headless OAuth client a hosted app does not have. Skip both —
|
|
581
|
+
// the native chain would also leave a ticket reservation held for nothing — and open the
|
|
582
|
+
// event's own page, where the member registers on the site.
|
|
583
|
+
if (s.registrationKind === "tickets" && s.id && !isHostedWorker()) {
|
|
570
584
|
let reservationId = "";
|
|
571
585
|
try {
|
|
572
586
|
// Buy what the buyer PICKED. `load` seeds the pick with the cheapest
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Hosted checkout (#4089 B3). A Branded Lite hosted in a host shell (Spaces)
|
|
2
|
+
// pays as the member the host lent it: Headless `createRedirectSession` does not take a Member
|
|
3
|
+
// instance, so the HOST opens the site's checkout signed in as that member. The worker names only
|
|
4
|
+
// its ecom checkout; the host reads Wix's checkout URL, mints a one-time member session and opens
|
|
5
|
+
// the site's `createSessionCookie` → checkout. The session token never reaches this worker.
|
|
6
|
+
|
|
7
|
+
import { dispatch } from "@himalaya/dispatch";
|
|
8
|
+
import { hostSiteFromInput } from "./branded-lite-hosted.js";
|
|
9
|
+
|
|
10
|
+
/** The host verbs (registered by the host shell, never by the app itself). The first prepares the
|
|
11
|
+
* checkout and answers a one-time ticket; the second opens it. Two steps so the caller's charge guard
|
|
12
|
+
* is re-read after the host's network round trips and right before the checkout opens — the prepared
|
|
13
|
+
* URL (which carries the member's session token) stays in the host; only the ticket crosses here. */
|
|
14
|
+
export const HOSTED_CHECKOUT_ACTION = "himi.host.memberCheckout";
|
|
15
|
+
export const HOSTED_CHECKOUT_OPEN_ACTION = "himi.host.memberCheckout.open";
|
|
16
|
+
|
|
17
|
+
/** Whether this worker runs inside a host that opened a site for it. */
|
|
18
|
+
export function isHostedWorker(): boolean {
|
|
19
|
+
return hostSiteFromInput((globalThis as { himalaya?: { input?: unknown } }).himalaya?.input) !== null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type HostAnswer = { opened?: unknown; ticket?: unknown; payload?: { opened?: unknown; ticket?: unknown } } | null;
|
|
23
|
+
|
|
24
|
+
/** `payCashierOrder`'s `openCashier` for a hosted app: the host prepares [checkoutId]'s checkout as the
|
|
25
|
+
* lent member, the caller's guard is re-read, and only then does the host open it. Resolves whether it
|
|
26
|
+
* opened ("withdrawn" when the guard said no); a host without the verbs (or that refused) → false. */
|
|
27
|
+
export function hostedCheckoutOpener(checkoutId: string): (stillMayCharge: () => Promise<boolean>) => Promise<boolean | "withdrawn"> {
|
|
28
|
+
return async (stillMayCharge) => {
|
|
29
|
+
try {
|
|
30
|
+
const prepared = await dispatch(HOSTED_CHECKOUT_ACTION, { checkoutId }) as HostAnswer;
|
|
31
|
+
const ticket = prepared?.ticket ?? prepared?.payload?.ticket;
|
|
32
|
+
if (typeof ticket !== "string" || !ticket) return false;
|
|
33
|
+
// An unopened ticket simply expires in the host: nothing to cancel.
|
|
34
|
+
if (!(await stillMayCharge())) return "withdrawn";
|
|
35
|
+
const result = await dispatch(HOSTED_CHECKOUT_OPEN_ACTION, { ticket }) as HostAnswer;
|
|
36
|
+
const opened = result?.opened ?? result?.payload?.opened;
|
|
37
|
+
return opened === true || opened === "true";
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|