@wix/himalaya-cli 0.1363.0 → 0.1365.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 +1 -0
- package/package.json +2 -2
- package/templates/beauty-salon/dev/index.ts +90 -0
- package/templates/beauty-salon/tier5-src/_shared/hosted-checkout.ts +4 -2
- package/templates/beauty-salon/tier5-src/himi-branded-lite-orders/index.ts +66 -6
- package/templates/beauty-salon/tier5-src/himi-branded-lite-plans/index.ts +23 -3
- package/templates/branded-lite/dev/index.ts +90 -0
- package/templates/branded-lite/tier5-src/_shared/hosted-checkout.ts +4 -2
- package/templates/branded-lite/tier5-src/himi-branded-lite-orders/index.ts +66 -6
- package/templates/branded-lite/tier5-src/himi-branded-lite-plans/index.ts +23 -3
- package/templates/community-club/tier5-src/_shared/hosted-checkout.ts +4 -2
- package/templates/events-meetups/tier5-src/_shared/hosted-checkout.ts +4 -2
- package/templates/gym-fitness/dev/index.ts +90 -0
- package/templates/gym-fitness/tier5-src/_shared/hosted-checkout.ts +4 -2
- package/templates/gym-fitness/tier5-src/himi-branded-lite-orders/index.ts +66 -6
- package/templates/gym-fitness/tier5-src/himi-branded-lite-plans/index.ts +23 -3
- package/templates/nonprofit-cause/tier5-src/_shared/hosted-checkout.ts +4 -2
- package/templates/nonprofit-cause/tier5-src/himi-branded-lite-plans/index.ts +23 -3
- package/templates/podcast-creator/tier5-src/_shared/hosted-checkout.ts +4 -2
- package/templates/podcast-creator/tier5-src/himi-branded-lite-plans/index.ts +23 -3
- package/templates/portfolio-creative/dev/index.ts +90 -0
- package/templates/portfolio-creative/tier5-src/_shared/hosted-checkout.ts +4 -2
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-orders/index.ts +66 -6
- package/templates/restaurant/tier5-src/_shared/hosted-checkout.ts +4 -2
- package/templates/retail-store/dev/index.ts +90 -0
- package/templates/retail-store/tier5-src/_shared/hosted-checkout.ts +4 -2
- package/templates/retail-store/tier5-src/himi-branded-lite-orders/index.ts +66 -6
- package/templates/yoga-studio/tier5-src/_shared/hosted-checkout.ts +4 -2
- package/templates/yoga-studio/tier5-src/himi-branded-lite-plans/index.ts +23 -3
package/dist/cli.mjs
CHANGED
|
@@ -7706,6 +7706,7 @@ function resolveTranslation(value, catalog3, sourceCatalog) {
|
|
|
7706
7706
|
var BINDING_SIGILS, BINDING_SIGIL_RE, TRANSLATION_PREFIX, TRANSLATION_KEY_RE;
|
|
7707
7707
|
var init_bindings = __esm({
|
|
7708
7708
|
"../../core/ts/src/bindings.ts"() {
|
|
7709
|
+
"use strict";
|
|
7709
7710
|
BINDING_SIGILS = ["service", "flag", "item", "t"];
|
|
7710
7711
|
BINDING_SIGIL_RE = new RegExp(
|
|
7711
7712
|
`^\\$(?:${BINDING_SIGILS.filter((s2) => s2 !== "t").join("|")})\\b|^\\$t:`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/himalaya-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1365.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": "ba734484945c55507351443800ba12569f99c00d2324c1d84e355d00"
|
|
75
75
|
}
|
|
@@ -12222,6 +12222,96 @@ const SCREENS = runtimeSetting({
|
|
|
12222
12222
|
"hiddenPath": "$service:himiBrandedOrders.noEmpty"
|
|
12223
12223
|
}
|
|
12224
12224
|
},
|
|
12225
|
+
{
|
|
12226
|
+
"id": "himi_orders.on_site_card",
|
|
12227
|
+
"body": {
|
|
12228
|
+
"package": "himalaya",
|
|
12229
|
+
"type": "VStack",
|
|
12230
|
+
"props": {
|
|
12231
|
+
"spacing": 8,
|
|
12232
|
+
"alignment": "leading",
|
|
12233
|
+
"children": [
|
|
12234
|
+
{
|
|
12235
|
+
"id": "",
|
|
12236
|
+
"body": {
|
|
12237
|
+
"package": "himalaya",
|
|
12238
|
+
"type": "Icon",
|
|
12239
|
+
"props": {
|
|
12240
|
+
"name": "bag",
|
|
12241
|
+
"size": 28,
|
|
12242
|
+
"tint": "textTertiary"
|
|
12243
|
+
}
|
|
12244
|
+
},
|
|
12245
|
+
"modifiers": {
|
|
12246
|
+
"accessibilityRole": "decorative"
|
|
12247
|
+
}
|
|
12248
|
+
},
|
|
12249
|
+
{
|
|
12250
|
+
"id": "",
|
|
12251
|
+
"body": {
|
|
12252
|
+
"package": "himalaya",
|
|
12253
|
+
"type": "Text",
|
|
12254
|
+
"props": {
|
|
12255
|
+
"text": "$service:himiBrandedOrders.emptyText",
|
|
12256
|
+
"style": "bodyEmphasis",
|
|
12257
|
+
"color": "textSecondary",
|
|
12258
|
+
"collapseWhenEmpty": true
|
|
12259
|
+
}
|
|
12260
|
+
}
|
|
12261
|
+
},
|
|
12262
|
+
{
|
|
12263
|
+
"id": "himi_orders.on_site",
|
|
12264
|
+
"body": {
|
|
12265
|
+
"package": "himalaya",
|
|
12266
|
+
"type": "Button",
|
|
12267
|
+
"props": {
|
|
12268
|
+
"title": "View my orders on the site",
|
|
12269
|
+
"style": "primary",
|
|
12270
|
+
"action": {
|
|
12271
|
+
"actionId": "tier5.himiBrandedOrders.openOnSite"
|
|
12272
|
+
}
|
|
12273
|
+
}
|
|
12274
|
+
}
|
|
12275
|
+
},
|
|
12276
|
+
{
|
|
12277
|
+
"id": "",
|
|
12278
|
+
"body": {
|
|
12279
|
+
"package": "himalaya",
|
|
12280
|
+
"type": "Text",
|
|
12281
|
+
"props": {
|
|
12282
|
+
"text": "$service:himiBrandedOrders.siteHint",
|
|
12283
|
+
"style": "footnote",
|
|
12284
|
+
"color": "textSecondary",
|
|
12285
|
+
"collapseWhenEmpty": true
|
|
12286
|
+
}
|
|
12287
|
+
},
|
|
12288
|
+
"modifiers": {
|
|
12289
|
+
"hiddenPath": "$service:himiBrandedOrders.noSiteHint"
|
|
12290
|
+
}
|
|
12291
|
+
}
|
|
12292
|
+
]
|
|
12293
|
+
}
|
|
12294
|
+
},
|
|
12295
|
+
"modifiers": {
|
|
12296
|
+
"background": {
|
|
12297
|
+
"token": "surfaceElevated"
|
|
12298
|
+
},
|
|
12299
|
+
"border": {
|
|
12300
|
+
"width": 1,
|
|
12301
|
+
"color": {
|
|
12302
|
+
"token": "border"
|
|
12303
|
+
}
|
|
12304
|
+
},
|
|
12305
|
+
"cornerRadius": 16,
|
|
12306
|
+
"padding": {
|
|
12307
|
+
"leading": 16,
|
|
12308
|
+
"trailing": 16,
|
|
12309
|
+
"top": 16,
|
|
12310
|
+
"bottom": 16
|
|
12311
|
+
},
|
|
12312
|
+
"hiddenPath": "$service:himiBrandedOrders.notOnSite"
|
|
12313
|
+
}
|
|
12314
|
+
},
|
|
12225
12315
|
{
|
|
12226
12316
|
"id": "himi_orders.member_gate",
|
|
12227
12317
|
"body": {
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
//
|
|
7
7
|
// Classic Wix Events tickets and classic Pricing Plans have no ecom checkout: for those the
|
|
8
8
|
// worker names the page by id (`himi.host.memberSitePage`) and the host opens the site's own event or
|
|
9
|
-
// plans page the same way, signed in — where the member picks and pays on the site.
|
|
9
|
+
// plans page the same way, signed in — where the member picks and pays on the site. A member's orders
|
|
10
|
+
// are the same (Wix lists them to no member identity), so My orders opens the site's own page too.
|
|
10
11
|
|
|
11
12
|
import { dispatch } from "@himalaya/dispatch";
|
|
12
13
|
import { hostSiteFromInput } from "./branded-lite-hosted.js";
|
|
@@ -22,7 +23,7 @@ export const HOSTED_SITE_PAGE_ACTION = "himi.host.memberSitePage";
|
|
|
22
23
|
|
|
23
24
|
/** A site page a hosted worker may ask its host for — ids only, never a URL: the host reads the URL
|
|
24
25
|
* from Wix itself and decides whether the member may be signed in there. */
|
|
25
|
-
export type HostedSitePage = { page: "event"; eventId: string } | { page: "plans"; planId?: string };
|
|
26
|
+
export type HostedSitePage = { page: "event"; eventId: string } | { page: "plans"; planId?: string } | { page: "orders" };
|
|
26
27
|
|
|
27
28
|
/** Whether this worker runs inside a host that opened a site for it. */
|
|
28
29
|
export function isHostedWorker(): boolean {
|
|
@@ -63,6 +64,7 @@ export function hostedCheckoutOpener(checkoutId: string): HostOpener {
|
|
|
63
64
|
export function hostedSitePageOpener(page: HostedSitePage): HostOpener {
|
|
64
65
|
const params: Record<string, string> = page.page === "event"
|
|
65
66
|
? { page: "event", eventId: page.eventId }
|
|
67
|
+
: page.page === "orders" ? { page: "orders" }
|
|
66
68
|
: { page: "plans", ...(page.planId ? { planId: page.planId } : {}) };
|
|
67
69
|
return hostOpener(HOSTED_SITE_PAGE_ACTION, params);
|
|
68
70
|
}
|
|
@@ -5,6 +5,7 @@ import { getAll, upsert, type DbEntity } from "@himalaya/local-db";
|
|
|
5
5
|
import { formatDateMedium } from "../_shared/engine-intl.js";
|
|
6
6
|
import { orderMember, assertOrderMember, ownedOrderCache, orderAccessDenied, orderAccessChanged, type OrderMember } from "../_shared/member-orders.js";
|
|
7
7
|
import { wixMediaUrl } from "@himalaya/wix-media";
|
|
8
|
+
import { isHostedWorker, hostedSitePageOpener } from "../_shared/hosted-checkout.js";
|
|
8
9
|
|
|
9
10
|
// Only ownership-tagged rows from this account/business/member can be reused.
|
|
10
11
|
// Legacy untagged cache rows are deliberately ignored. Missing host scope or
|
|
@@ -60,6 +61,13 @@ interface State {
|
|
|
60
61
|
pageError?: boolean;
|
|
61
62
|
loadingMore?: boolean;
|
|
62
63
|
fromCache?: boolean;
|
|
64
|
+
/** Hosted in a host shell (Spaces), signed in: the member's orders live on the site's own page. */
|
|
65
|
+
onSite?: boolean;
|
|
66
|
+
/** A search or filter narrows this page: the list (and its Clear) stays visible beside the on-site card. */
|
|
67
|
+
narrowed?: boolean;
|
|
68
|
+
notOnSite?: boolean;
|
|
69
|
+
siteHint?: string;
|
|
70
|
+
noSiteHint?: boolean;
|
|
63
71
|
}
|
|
64
72
|
|
|
65
73
|
function formatDate(value?: string): string {
|
|
@@ -129,9 +137,15 @@ function derive(state: State): State {
|
|
|
129
137
|
isErrored: state.status === "error",
|
|
130
138
|
notErrored: state.status !== "error",
|
|
131
139
|
// A guest gets the shared member gate, not the "Browse products" empty card.
|
|
132
|
-
|
|
140
|
+
// Never "No orders" while a next page could still hold one.
|
|
141
|
+
noEmpty: state.status !== "ready" || hasRows || state.needsSignIn === true || state.onSite === true || !!state.nextCursor,
|
|
133
142
|
noSignIn: state.status !== "ready" || state.needsSignIn !== true,
|
|
134
|
-
|
|
143
|
+
// The on-site card replaces the list's empty state — except under a search or filter, where the list keeps
|
|
144
|
+
// its search, filters and Clear, so the member is never stranded behind criteria they cannot clear.
|
|
145
|
+
hideList: state.status !== "ready" || state.needsSignIn === true || (state.onSite === true && state.narrowed !== true),
|
|
146
|
+
notOnSite: state.status !== "ready" || state.onSite !== true || state.needsSignIn === true,
|
|
147
|
+
// Hidden outright when empty: a collapsed text still takes the card's stack spacing.
|
|
148
|
+
noSiteHint: !state.siteHint,
|
|
135
149
|
};
|
|
136
150
|
}
|
|
137
151
|
|
|
@@ -139,11 +153,15 @@ const FULFILLMENT: Record<string, string> = {
|
|
|
139
153
|
"Not fulfilled": "NOT_FULFILLED", "Partially fulfilled": "PARTIALLY_FULFILLED", "Fulfilled": "FULFILLED",
|
|
140
154
|
};
|
|
141
155
|
let pageMember: OrderMember | undefined;
|
|
156
|
+
let siteOpenInFlight = false;
|
|
157
|
+
// Whose open failed: the hint belongs to that member and scope only, never to whoever signs in next.
|
|
158
|
+
let siteHintOwner = "";
|
|
159
|
+
const memberKey = (member: { email: string; scope?: string }) => member.email ? `${member.email}\u0000${member.scope ?? ""}` : "";
|
|
142
160
|
let cacheWrite = Promise.resolve();
|
|
143
161
|
const svc = defineBackendCollection("himiBrandedOrders", {
|
|
144
162
|
restorePagesOnLoad: true,
|
|
145
163
|
state: derive({ status: "loading", rows: [], identityLine: "", emptyText: "", isLoading: true,
|
|
146
|
-
notLoading: false, isErrored: false, notErrored: true, noEmpty: true, hideList: true }),
|
|
164
|
+
notLoading: false, isErrored: false, notErrored: true, noEmpty: true, hideList: true, siteHint: "" }),
|
|
147
165
|
source: {
|
|
148
166
|
id: (row: OrderRow) => row.id,
|
|
149
167
|
clearOnError: orderAccessDenied,
|
|
@@ -153,11 +171,14 @@ const svc = defineBackendCollection("himiBrandedOrders", {
|
|
|
153
171
|
async fetch(criteria, cursor) {
|
|
154
172
|
const member = await orderMember();
|
|
155
173
|
if (cursor && (!pageMember || member.email !== pageMember.email || member.scope !== pageMember.scope)) throw orderAccessChanged();
|
|
156
|
-
const
|
|
174
|
+
const narrowed = !!criteria.query.trim() || criteria.filter !== "all";
|
|
175
|
+
const metadata = { email: member.email, scope: member.scope, needsSignIn: !member.email, fromCache: false, narrowed };
|
|
157
176
|
if (!member.email) return { items: [], nextCursor: "", metadata };
|
|
158
177
|
const query = criteria.query.replace(/^#/, "");
|
|
159
178
|
const validNumber = /^\d+$/.test(query) && Number.isSafeInteger(Number(query));
|
|
160
|
-
if ((query && !validNumber) || (criteria.filter === "saved" && !criteria.savedIds.length))
|
|
179
|
+
if ((query && !validNumber) || (criteria.filter === "saved" && !criteria.savedIds.length)) {
|
|
180
|
+
return { items: [], nextCursor: "", metadata };
|
|
181
|
+
}
|
|
161
182
|
const filter: Record<string, unknown> = { "buyerInfo.email": { $eq: member.email } };
|
|
162
183
|
if (query) filter.number = { $eq: Number(query) };
|
|
163
184
|
if (criteria.filter === "saved") filter.id = { $in: criteria.savedIds };
|
|
@@ -175,7 +196,8 @@ const svc = defineBackendCollection("himiBrandedOrders", {
|
|
|
175
196
|
const paging = response.metadata as { hasNext?: boolean; cursors?: { next?: string | null } } | undefined;
|
|
176
197
|
const nextCursor = paging?.cursors?.next ?? "";
|
|
177
198
|
if (paging?.hasNext && !nextCursor) throw new Error("Orders response is missing its next-page cursor");
|
|
178
|
-
|
|
199
|
+
const items = (response.orders ?? []).filter(order => !!order.id && orderEmail(order) === member.email).map(orderRow);
|
|
200
|
+
return { items, nextCursor, metadata };
|
|
179
201
|
} catch (error) {
|
|
180
202
|
await assertOrderMember(member);
|
|
181
203
|
// Cached rows never impersonate a backend query or survive access denial.
|
|
@@ -195,14 +217,52 @@ const svc = defineBackendCollection("himiBrandedOrders", {
|
|
|
195
217
|
cacheWrite = cacheWrite.then(() => writeCachedOrders(page.items, member));
|
|
196
218
|
}
|
|
197
219
|
const status = page.status === "idle" ? "loading" : page.status;
|
|
220
|
+
// Hosted in a host shell as its signed-in member, an empty answer is not proof of no orders: Wix documents
|
|
221
|
+
// `ecom/v1/orders/search` for a Wix user or app identity only, and answers the member a successful EMPTY list.
|
|
222
|
+
// So hosted, "No orders" is never claimed — once every page is read (none left to load), whatever the criteria,
|
|
223
|
+
// the site's own My Orders page is offered, opened by the host (`himi.host.memberSitePage {page:"orders"}`).
|
|
224
|
+
// Rows Wix does return still list here.
|
|
225
|
+
const onSite = isHostedWorker() && !!member.email && status === "ready" && !page.items.length && !page.nextCursor
|
|
226
|
+
&& !page.pageError && page.metadata?.fromCache !== true;
|
|
198
227
|
return derive({ ...state, status, rows: page.items, nextCursor: page.nextCursor, complete: page.complete,
|
|
199
228
|
pageError: page.pageError, loadingMore: page.loadingMore, fromCache: page.metadata?.fromCache === true,
|
|
200
229
|
needsSignIn: page.metadata?.needsSignIn === true,
|
|
230
|
+
onSite,
|
|
231
|
+
narrowed: page.metadata?.narrowed === true,
|
|
232
|
+
siteHint: siteHintOwner && siteHintOwner === memberKey(member) && onSite ? state.siteHint : "",
|
|
201
233
|
identityLine: member.email ? copyTemplate("orders.identityLine", "Signed in as {email}", { email: member.email }) : "",
|
|
202
234
|
emptyText: status === "error" ? page.error : !member.email ? copyParam("orders.signInEmptyText", "Sign in to see your orders.")
|
|
235
|
+
: onSite ? copyParam("orders.onSiteText", "Your orders are on the site.")
|
|
203
236
|
: page.items.length ? "" : copyParam("orders.emptyText", "No orders for this member yet."),
|
|
204
237
|
});
|
|
205
238
|
},
|
|
239
|
+
actions: {
|
|
240
|
+
// The site's My Orders page, opened by the host signed in as the lent member where it can (a visitor who
|
|
241
|
+
// signs in on the site otherwise). Nothing is charged, so the guard only re-checks the member is unchanged.
|
|
242
|
+
async openOnSite(s: State): Promise<State | undefined> {
|
|
243
|
+
// One host open at a time: a second tap while the first is in flight would only replace its ticket.
|
|
244
|
+
if (siteOpenInFlight || s.onSite !== true) return s;
|
|
245
|
+
siteOpenInFlight = true;
|
|
246
|
+
let opened: boolean | "withdrawn";
|
|
247
|
+
let member: OrderMember;
|
|
248
|
+
try {
|
|
249
|
+
member = await orderMember();
|
|
250
|
+
opened = await hostedSitePageOpener({ page: "orders" })(async () => {
|
|
251
|
+
const now = await orderMember();
|
|
252
|
+
return now.email === member.email && now.scope === member.scope;
|
|
253
|
+
});
|
|
254
|
+
} finally {
|
|
255
|
+
siteOpenInFlight = false;
|
|
256
|
+
}
|
|
257
|
+
// A member who changed while the host worked never inherits this answer.
|
|
258
|
+
const now = await orderMember();
|
|
259
|
+
if (memberKey(now) !== memberKey(member)) return undefined;
|
|
260
|
+
siteHintOwner = opened === false ? memberKey(member) : "";
|
|
261
|
+
const latest = svc.getState() as unknown as State;
|
|
262
|
+
return derive({ ...latest, siteHint: opened === false
|
|
263
|
+
? copyParam("orders.onSiteFailedText", "The site's orders page could not be opened. Visit the site to see your orders.") : "" });
|
|
264
|
+
},
|
|
265
|
+
},
|
|
206
266
|
});
|
|
207
267
|
svc.load();
|
|
208
268
|
export { svc };
|
|
@@ -9,7 +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
|
+
import { isHostedWorker, hostedSitePageOpener } from "../_shared/hosted-checkout.js";
|
|
13
13
|
|
|
14
14
|
/** Wix Pricing Plans appDefId — the vertical that owns a plan order, so the `appId` its
|
|
15
15
|
* cashier order is paid under (mirrors AppDefId.PRICING_PLANS in Wix's RN apps). */
|
|
@@ -186,6 +186,8 @@ let started = false;
|
|
|
186
186
|
let loading = 0;
|
|
187
187
|
let contentReady = false;
|
|
188
188
|
const joining = new Set<string>();
|
|
189
|
+
/** A hosted paid plan's page is being prepared by the host (one ticket at a time). */
|
|
190
|
+
let hostedPlanOpenInFlight = false;
|
|
189
191
|
const memberSession = hostSessionFromGlobal();
|
|
190
192
|
let regularGeneration = -1;
|
|
191
193
|
let regularAccess = new Set<string>();
|
|
@@ -227,9 +229,27 @@ async function regularJoinMessage(row: PlanRow, reference: { kind: string; id: s
|
|
|
227
229
|
// Hosted inside a host shell (Spaces) a plan cannot be paid in-app: classic Pricing Plans is not on
|
|
228
230
|
// ecom, so the host's member checkout has no checkout to open, and the redirect session needs a
|
|
229
231
|
// 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
|
|
231
|
-
//
|
|
232
|
+
// left behind as a DRAFT, so none is — the member joins on the site: the HOST opens the site's own
|
|
233
|
+
// plans page signed in as the lent member (`himi.host.memberSitePage {page:"plans"}`), or this
|
|
234
|
+
// message stands when it cannot. A FREE plan has nothing to pay: its online order activates at
|
|
235
|
+
// once, so it still joins in-app.
|
|
232
236
|
if (isHostedWorker() && !row.free) {
|
|
237
|
+
// One host open at a time: the host keeps a single ticket, so a second plan's prepare would replace the
|
|
238
|
+
// first's and drop it. The checkout guard is re-read right before the host opens the page.
|
|
239
|
+
if (hostedPlanOpenInFlight) return "";
|
|
240
|
+
hostedPlanOpenInFlight = true;
|
|
241
|
+
let opened: boolean | "withdrawn";
|
|
242
|
+
try {
|
|
243
|
+
opened = await hostedSitePageOpener({ page: "plans", planId: row.id })(
|
|
244
|
+
async () => commerce.pricing.continueCheckoutGuard(reference).allowed);
|
|
245
|
+
} finally {
|
|
246
|
+
hostedPlanOpenInFlight = false;
|
|
247
|
+
}
|
|
248
|
+
if (generation !== memberSession.current().generation) return "";
|
|
249
|
+
if (opened === "withdrawn") return "";
|
|
250
|
+
if (opened === true) {
|
|
251
|
+
return copyTemplate("plans.continueOnSiteMessage", "Finish joining {name} on the site.", { name: row.name });
|
|
252
|
+
}
|
|
233
253
|
return copyTemplate("plans.joinOnSiteMessage", "Memberships are purchased on the website — visit the site to join {name}.", { name: row.name });
|
|
234
254
|
}
|
|
235
255
|
try {
|
|
@@ -13243,6 +13243,96 @@ const SCREENS = runtimeSetting({
|
|
|
13243
13243
|
"hiddenPath": "$service:himiBrandedOrders.noEmpty"
|
|
13244
13244
|
}
|
|
13245
13245
|
},
|
|
13246
|
+
{
|
|
13247
|
+
"id": "himi_orders.on_site_card",
|
|
13248
|
+
"body": {
|
|
13249
|
+
"package": "himalaya",
|
|
13250
|
+
"type": "VStack",
|
|
13251
|
+
"props": {
|
|
13252
|
+
"spacing": 8,
|
|
13253
|
+
"alignment": "leading",
|
|
13254
|
+
"children": [
|
|
13255
|
+
{
|
|
13256
|
+
"id": "",
|
|
13257
|
+
"body": {
|
|
13258
|
+
"package": "himalaya",
|
|
13259
|
+
"type": "Icon",
|
|
13260
|
+
"props": {
|
|
13261
|
+
"name": "bag",
|
|
13262
|
+
"size": 28,
|
|
13263
|
+
"tint": "textTertiary"
|
|
13264
|
+
}
|
|
13265
|
+
},
|
|
13266
|
+
"modifiers": {
|
|
13267
|
+
"accessibilityRole": "decorative"
|
|
13268
|
+
}
|
|
13269
|
+
},
|
|
13270
|
+
{
|
|
13271
|
+
"id": "",
|
|
13272
|
+
"body": {
|
|
13273
|
+
"package": "himalaya",
|
|
13274
|
+
"type": "Text",
|
|
13275
|
+
"props": {
|
|
13276
|
+
"text": "$service:himiBrandedOrders.emptyText",
|
|
13277
|
+
"style": "bodyEmphasis",
|
|
13278
|
+
"color": "textSecondary",
|
|
13279
|
+
"collapseWhenEmpty": true
|
|
13280
|
+
}
|
|
13281
|
+
}
|
|
13282
|
+
},
|
|
13283
|
+
{
|
|
13284
|
+
"id": "himi_orders.on_site",
|
|
13285
|
+
"body": {
|
|
13286
|
+
"package": "himalaya",
|
|
13287
|
+
"type": "Button",
|
|
13288
|
+
"props": {
|
|
13289
|
+
"title": "View my orders on the site",
|
|
13290
|
+
"style": "primary",
|
|
13291
|
+
"action": {
|
|
13292
|
+
"actionId": "tier5.himiBrandedOrders.openOnSite"
|
|
13293
|
+
}
|
|
13294
|
+
}
|
|
13295
|
+
}
|
|
13296
|
+
},
|
|
13297
|
+
{
|
|
13298
|
+
"id": "",
|
|
13299
|
+
"body": {
|
|
13300
|
+
"package": "himalaya",
|
|
13301
|
+
"type": "Text",
|
|
13302
|
+
"props": {
|
|
13303
|
+
"text": "$service:himiBrandedOrders.siteHint",
|
|
13304
|
+
"style": "footnote",
|
|
13305
|
+
"color": "textSecondary",
|
|
13306
|
+
"collapseWhenEmpty": true
|
|
13307
|
+
}
|
|
13308
|
+
},
|
|
13309
|
+
"modifiers": {
|
|
13310
|
+
"hiddenPath": "$service:himiBrandedOrders.noSiteHint"
|
|
13311
|
+
}
|
|
13312
|
+
}
|
|
13313
|
+
]
|
|
13314
|
+
}
|
|
13315
|
+
},
|
|
13316
|
+
"modifiers": {
|
|
13317
|
+
"background": {
|
|
13318
|
+
"token": "surfaceElevated"
|
|
13319
|
+
},
|
|
13320
|
+
"border": {
|
|
13321
|
+
"width": 1,
|
|
13322
|
+
"color": {
|
|
13323
|
+
"token": "border"
|
|
13324
|
+
}
|
|
13325
|
+
},
|
|
13326
|
+
"cornerRadius": 16,
|
|
13327
|
+
"padding": {
|
|
13328
|
+
"leading": 16,
|
|
13329
|
+
"trailing": 16,
|
|
13330
|
+
"top": 16,
|
|
13331
|
+
"bottom": 16
|
|
13332
|
+
},
|
|
13333
|
+
"hiddenPath": "$service:himiBrandedOrders.notOnSite"
|
|
13334
|
+
}
|
|
13335
|
+
},
|
|
13246
13336
|
{
|
|
13247
13337
|
"id": "himi_orders.member_gate",
|
|
13248
13338
|
"body": {
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
//
|
|
7
7
|
// Classic Wix Events tickets and classic Pricing Plans have no ecom checkout: for those the
|
|
8
8
|
// worker names the page by id (`himi.host.memberSitePage`) and the host opens the site's own event or
|
|
9
|
-
// plans page the same way, signed in — where the member picks and pays on the site.
|
|
9
|
+
// plans page the same way, signed in — where the member picks and pays on the site. A member's orders
|
|
10
|
+
// are the same (Wix lists them to no member identity), so My orders opens the site's own page too.
|
|
10
11
|
|
|
11
12
|
import { dispatch } from "@himalaya/dispatch";
|
|
12
13
|
import { hostSiteFromInput } from "./branded-lite-hosted.js";
|
|
@@ -22,7 +23,7 @@ export const HOSTED_SITE_PAGE_ACTION = "himi.host.memberSitePage";
|
|
|
22
23
|
|
|
23
24
|
/** A site page a hosted worker may ask its host for — ids only, never a URL: the host reads the URL
|
|
24
25
|
* from Wix itself and decides whether the member may be signed in there. */
|
|
25
|
-
export type HostedSitePage = { page: "event"; eventId: string } | { page: "plans"; planId?: string };
|
|
26
|
+
export type HostedSitePage = { page: "event"; eventId: string } | { page: "plans"; planId?: string } | { page: "orders" };
|
|
26
27
|
|
|
27
28
|
/** Whether this worker runs inside a host that opened a site for it. */
|
|
28
29
|
export function isHostedWorker(): boolean {
|
|
@@ -63,6 +64,7 @@ export function hostedCheckoutOpener(checkoutId: string): HostOpener {
|
|
|
63
64
|
export function hostedSitePageOpener(page: HostedSitePage): HostOpener {
|
|
64
65
|
const params: Record<string, string> = page.page === "event"
|
|
65
66
|
? { page: "event", eventId: page.eventId }
|
|
67
|
+
: page.page === "orders" ? { page: "orders" }
|
|
66
68
|
: { page: "plans", ...(page.planId ? { planId: page.planId } : {}) };
|
|
67
69
|
return hostOpener(HOSTED_SITE_PAGE_ACTION, params);
|
|
68
70
|
}
|
|
@@ -5,6 +5,7 @@ import { getAll, upsert, type DbEntity } from "@himalaya/local-db";
|
|
|
5
5
|
import { formatDateMedium } from "../_shared/engine-intl.js";
|
|
6
6
|
import { orderMember, assertOrderMember, ownedOrderCache, orderAccessDenied, orderAccessChanged, type OrderMember } from "../_shared/member-orders.js";
|
|
7
7
|
import { wixMediaUrl } from "@himalaya/wix-media";
|
|
8
|
+
import { isHostedWorker, hostedSitePageOpener } from "../_shared/hosted-checkout.js";
|
|
8
9
|
|
|
9
10
|
// Only ownership-tagged rows from this account/business/member can be reused.
|
|
10
11
|
// Legacy untagged cache rows are deliberately ignored. Missing host scope or
|
|
@@ -60,6 +61,13 @@ interface State {
|
|
|
60
61
|
pageError?: boolean;
|
|
61
62
|
loadingMore?: boolean;
|
|
62
63
|
fromCache?: boolean;
|
|
64
|
+
/** Hosted in a host shell (Spaces), signed in: the member's orders live on the site's own page. */
|
|
65
|
+
onSite?: boolean;
|
|
66
|
+
/** A search or filter narrows this page: the list (and its Clear) stays visible beside the on-site card. */
|
|
67
|
+
narrowed?: boolean;
|
|
68
|
+
notOnSite?: boolean;
|
|
69
|
+
siteHint?: string;
|
|
70
|
+
noSiteHint?: boolean;
|
|
63
71
|
}
|
|
64
72
|
|
|
65
73
|
function formatDate(value?: string): string {
|
|
@@ -129,9 +137,15 @@ function derive(state: State): State {
|
|
|
129
137
|
isErrored: state.status === "error",
|
|
130
138
|
notErrored: state.status !== "error",
|
|
131
139
|
// A guest gets the shared member gate, not the "Browse products" empty card.
|
|
132
|
-
|
|
140
|
+
// Never "No orders" while a next page could still hold one.
|
|
141
|
+
noEmpty: state.status !== "ready" || hasRows || state.needsSignIn === true || state.onSite === true || !!state.nextCursor,
|
|
133
142
|
noSignIn: state.status !== "ready" || state.needsSignIn !== true,
|
|
134
|
-
|
|
143
|
+
// The on-site card replaces the list's empty state — except under a search or filter, where the list keeps
|
|
144
|
+
// its search, filters and Clear, so the member is never stranded behind criteria they cannot clear.
|
|
145
|
+
hideList: state.status !== "ready" || state.needsSignIn === true || (state.onSite === true && state.narrowed !== true),
|
|
146
|
+
notOnSite: state.status !== "ready" || state.onSite !== true || state.needsSignIn === true,
|
|
147
|
+
// Hidden outright when empty: a collapsed text still takes the card's stack spacing.
|
|
148
|
+
noSiteHint: !state.siteHint,
|
|
135
149
|
};
|
|
136
150
|
}
|
|
137
151
|
|
|
@@ -139,11 +153,15 @@ const FULFILLMENT: Record<string, string> = {
|
|
|
139
153
|
"Not fulfilled": "NOT_FULFILLED", "Partially fulfilled": "PARTIALLY_FULFILLED", "Fulfilled": "FULFILLED",
|
|
140
154
|
};
|
|
141
155
|
let pageMember: OrderMember | undefined;
|
|
156
|
+
let siteOpenInFlight = false;
|
|
157
|
+
// Whose open failed: the hint belongs to that member and scope only, never to whoever signs in next.
|
|
158
|
+
let siteHintOwner = "";
|
|
159
|
+
const memberKey = (member: { email: string; scope?: string }) => member.email ? `${member.email}\u0000${member.scope ?? ""}` : "";
|
|
142
160
|
let cacheWrite = Promise.resolve();
|
|
143
161
|
const svc = defineBackendCollection("himiBrandedOrders", {
|
|
144
162
|
restorePagesOnLoad: true,
|
|
145
163
|
state: derive({ status: "loading", rows: [], identityLine: "", emptyText: "", isLoading: true,
|
|
146
|
-
notLoading: false, isErrored: false, notErrored: true, noEmpty: true, hideList: true }),
|
|
164
|
+
notLoading: false, isErrored: false, notErrored: true, noEmpty: true, hideList: true, siteHint: "" }),
|
|
147
165
|
source: {
|
|
148
166
|
id: (row: OrderRow) => row.id,
|
|
149
167
|
clearOnError: orderAccessDenied,
|
|
@@ -153,11 +171,14 @@ const svc = defineBackendCollection("himiBrandedOrders", {
|
|
|
153
171
|
async fetch(criteria, cursor) {
|
|
154
172
|
const member = await orderMember();
|
|
155
173
|
if (cursor && (!pageMember || member.email !== pageMember.email || member.scope !== pageMember.scope)) throw orderAccessChanged();
|
|
156
|
-
const
|
|
174
|
+
const narrowed = !!criteria.query.trim() || criteria.filter !== "all";
|
|
175
|
+
const metadata = { email: member.email, scope: member.scope, needsSignIn: !member.email, fromCache: false, narrowed };
|
|
157
176
|
if (!member.email) return { items: [], nextCursor: "", metadata };
|
|
158
177
|
const query = criteria.query.replace(/^#/, "");
|
|
159
178
|
const validNumber = /^\d+$/.test(query) && Number.isSafeInteger(Number(query));
|
|
160
|
-
if ((query && !validNumber) || (criteria.filter === "saved" && !criteria.savedIds.length))
|
|
179
|
+
if ((query && !validNumber) || (criteria.filter === "saved" && !criteria.savedIds.length)) {
|
|
180
|
+
return { items: [], nextCursor: "", metadata };
|
|
181
|
+
}
|
|
161
182
|
const filter: Record<string, unknown> = { "buyerInfo.email": { $eq: member.email } };
|
|
162
183
|
if (query) filter.number = { $eq: Number(query) };
|
|
163
184
|
if (criteria.filter === "saved") filter.id = { $in: criteria.savedIds };
|
|
@@ -175,7 +196,8 @@ const svc = defineBackendCollection("himiBrandedOrders", {
|
|
|
175
196
|
const paging = response.metadata as { hasNext?: boolean; cursors?: { next?: string | null } } | undefined;
|
|
176
197
|
const nextCursor = paging?.cursors?.next ?? "";
|
|
177
198
|
if (paging?.hasNext && !nextCursor) throw new Error("Orders response is missing its next-page cursor");
|
|
178
|
-
|
|
199
|
+
const items = (response.orders ?? []).filter(order => !!order.id && orderEmail(order) === member.email).map(orderRow);
|
|
200
|
+
return { items, nextCursor, metadata };
|
|
179
201
|
} catch (error) {
|
|
180
202
|
await assertOrderMember(member);
|
|
181
203
|
// Cached rows never impersonate a backend query or survive access denial.
|
|
@@ -195,14 +217,52 @@ const svc = defineBackendCollection("himiBrandedOrders", {
|
|
|
195
217
|
cacheWrite = cacheWrite.then(() => writeCachedOrders(page.items, member));
|
|
196
218
|
}
|
|
197
219
|
const status = page.status === "idle" ? "loading" : page.status;
|
|
220
|
+
// Hosted in a host shell as its signed-in member, an empty answer is not proof of no orders: Wix documents
|
|
221
|
+
// `ecom/v1/orders/search` for a Wix user or app identity only, and answers the member a successful EMPTY list.
|
|
222
|
+
// So hosted, "No orders" is never claimed — once every page is read (none left to load), whatever the criteria,
|
|
223
|
+
// the site's own My Orders page is offered, opened by the host (`himi.host.memberSitePage {page:"orders"}`).
|
|
224
|
+
// Rows Wix does return still list here.
|
|
225
|
+
const onSite = isHostedWorker() && !!member.email && status === "ready" && !page.items.length && !page.nextCursor
|
|
226
|
+
&& !page.pageError && page.metadata?.fromCache !== true;
|
|
198
227
|
return derive({ ...state, status, rows: page.items, nextCursor: page.nextCursor, complete: page.complete,
|
|
199
228
|
pageError: page.pageError, loadingMore: page.loadingMore, fromCache: page.metadata?.fromCache === true,
|
|
200
229
|
needsSignIn: page.metadata?.needsSignIn === true,
|
|
230
|
+
onSite,
|
|
231
|
+
narrowed: page.metadata?.narrowed === true,
|
|
232
|
+
siteHint: siteHintOwner && siteHintOwner === memberKey(member) && onSite ? state.siteHint : "",
|
|
201
233
|
identityLine: member.email ? copyTemplate("orders.identityLine", "Signed in as {email}", { email: member.email }) : "",
|
|
202
234
|
emptyText: status === "error" ? page.error : !member.email ? copyParam("orders.signInEmptyText", "Sign in to see your orders.")
|
|
235
|
+
: onSite ? copyParam("orders.onSiteText", "Your orders are on the site.")
|
|
203
236
|
: page.items.length ? "" : copyParam("orders.emptyText", "No orders for this member yet."),
|
|
204
237
|
});
|
|
205
238
|
},
|
|
239
|
+
actions: {
|
|
240
|
+
// The site's My Orders page, opened by the host signed in as the lent member where it can (a visitor who
|
|
241
|
+
// signs in on the site otherwise). Nothing is charged, so the guard only re-checks the member is unchanged.
|
|
242
|
+
async openOnSite(s: State): Promise<State | undefined> {
|
|
243
|
+
// One host open at a time: a second tap while the first is in flight would only replace its ticket.
|
|
244
|
+
if (siteOpenInFlight || s.onSite !== true) return s;
|
|
245
|
+
siteOpenInFlight = true;
|
|
246
|
+
let opened: boolean | "withdrawn";
|
|
247
|
+
let member: OrderMember;
|
|
248
|
+
try {
|
|
249
|
+
member = await orderMember();
|
|
250
|
+
opened = await hostedSitePageOpener({ page: "orders" })(async () => {
|
|
251
|
+
const now = await orderMember();
|
|
252
|
+
return now.email === member.email && now.scope === member.scope;
|
|
253
|
+
});
|
|
254
|
+
} finally {
|
|
255
|
+
siteOpenInFlight = false;
|
|
256
|
+
}
|
|
257
|
+
// A member who changed while the host worked never inherits this answer.
|
|
258
|
+
const now = await orderMember();
|
|
259
|
+
if (memberKey(now) !== memberKey(member)) return undefined;
|
|
260
|
+
siteHintOwner = opened === false ? memberKey(member) : "";
|
|
261
|
+
const latest = svc.getState() as unknown as State;
|
|
262
|
+
return derive({ ...latest, siteHint: opened === false
|
|
263
|
+
? copyParam("orders.onSiteFailedText", "The site's orders page could not be opened. Visit the site to see your orders.") : "" });
|
|
264
|
+
},
|
|
265
|
+
},
|
|
206
266
|
});
|
|
207
267
|
svc.load();
|
|
208
268
|
export { svc };
|
|
@@ -9,7 +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
|
+
import { isHostedWorker, hostedSitePageOpener } from "../_shared/hosted-checkout.js";
|
|
13
13
|
|
|
14
14
|
/** Wix Pricing Plans appDefId — the vertical that owns a plan order, so the `appId` its
|
|
15
15
|
* cashier order is paid under (mirrors AppDefId.PRICING_PLANS in Wix's RN apps). */
|
|
@@ -186,6 +186,8 @@ let started = false;
|
|
|
186
186
|
let loading = 0;
|
|
187
187
|
let contentReady = false;
|
|
188
188
|
const joining = new Set<string>();
|
|
189
|
+
/** A hosted paid plan's page is being prepared by the host (one ticket at a time). */
|
|
190
|
+
let hostedPlanOpenInFlight = false;
|
|
189
191
|
const memberSession = hostSessionFromGlobal();
|
|
190
192
|
let regularGeneration = -1;
|
|
191
193
|
let regularAccess = new Set<string>();
|
|
@@ -227,9 +229,27 @@ async function regularJoinMessage(row: PlanRow, reference: { kind: string; id: s
|
|
|
227
229
|
// Hosted inside a host shell (Spaces) a plan cannot be paid in-app: classic Pricing Plans is not on
|
|
228
230
|
// ecom, so the host's member checkout has no checkout to open, and the redirect session needs a
|
|
229
231
|
// 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
|
|
231
|
-
//
|
|
232
|
+
// left behind as a DRAFT, so none is — the member joins on the site: the HOST opens the site's own
|
|
233
|
+
// plans page signed in as the lent member (`himi.host.memberSitePage {page:"plans"}`), or this
|
|
234
|
+
// message stands when it cannot. A FREE plan has nothing to pay: its online order activates at
|
|
235
|
+
// once, so it still joins in-app.
|
|
232
236
|
if (isHostedWorker() && !row.free) {
|
|
237
|
+
// One host open at a time: the host keeps a single ticket, so a second plan's prepare would replace the
|
|
238
|
+
// first's and drop it. The checkout guard is re-read right before the host opens the page.
|
|
239
|
+
if (hostedPlanOpenInFlight) return "";
|
|
240
|
+
hostedPlanOpenInFlight = true;
|
|
241
|
+
let opened: boolean | "withdrawn";
|
|
242
|
+
try {
|
|
243
|
+
opened = await hostedSitePageOpener({ page: "plans", planId: row.id })(
|
|
244
|
+
async () => commerce.pricing.continueCheckoutGuard(reference).allowed);
|
|
245
|
+
} finally {
|
|
246
|
+
hostedPlanOpenInFlight = false;
|
|
247
|
+
}
|
|
248
|
+
if (generation !== memberSession.current().generation) return "";
|
|
249
|
+
if (opened === "withdrawn") return "";
|
|
250
|
+
if (opened === true) {
|
|
251
|
+
return copyTemplate("plans.continueOnSiteMessage", "Finish joining {name} on the site.", { name: row.name });
|
|
252
|
+
}
|
|
233
253
|
return copyTemplate("plans.joinOnSiteMessage", "Memberships are purchased on the website — visit the site to join {name}.", { name: row.name });
|
|
234
254
|
}
|
|
235
255
|
try {
|