@wix/himalaya-cli 0.983.0 → 0.985.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/wix-sdk/notification-preferences/src/index.d.ts +105 -33
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-notification-preferences.mjs +66 -5
- package/dist/cli.mjs +19 -15
- package/dist/preview-assets.generated.bin +0 -0
- package/fixtures/wix-canned.json +36 -29
- package/guides/sdk-reference.json +86 -49
- package/package.json +2 -2
- package/templates/beauty-salon/dev/index.ts +27 -2
- package/templates/beauty-salon/tier5-src/himi-branded-lite-order/index.ts +14 -6
- package/templates/beauty-salon/tier5-src/himi-store-account/index.ts +59 -20
- package/templates/branded-lite/dev/index.ts +27 -2
- package/templates/branded-lite/tier5-src/himi-branded-lite-notifsettings/index.ts +42 -14
- package/templates/branded-lite/tier5-src/himi-branded-lite-order/index.ts +14 -6
- package/templates/branded-lite/tier5-src/himi-store-account/index.ts +59 -20
- package/templates/community-club/tier5-src/himi-store-account/index.ts +59 -20
- package/templates/events-meetups/tier5-src/himi-store-account/index.ts +59 -20
- package/templates/gym-fitness/dev/index.ts +27 -2
- package/templates/gym-fitness/tier5-src/himi-branded-lite-order/index.ts +14 -6
- package/templates/gym-fitness/tier5-src/himi-store-account/index.ts +59 -20
- package/templates/nonprofit-cause/tier5-src/himi-store-account/index.ts +59 -20
- package/templates/podcast-creator/tier5-src/himi-store-account/index.ts +59 -20
- package/templates/portfolio-creative/dev/index.ts +25 -1
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-order/index.ts +14 -6
- package/templates/portfolio-creative/tier5-src/himi-store-account/index.ts +59 -20
- package/templates/restaurant/tier5-src/himi-store-account/index.ts +59 -20
- package/templates/retail-store/dev/index.ts +25 -1
- package/templates/retail-store/tier5-src/himi-branded-lite-order/index.ts +14 -6
- package/templates/retail-store/tier5-src/himi-store-account/index.ts +59 -20
- package/templates/yoga-studio/dev/index.ts +2 -1
- package/templates/yoga-studio/tier5-src/himi-store-account/index.ts +59 -20
|
@@ -63,6 +63,10 @@ interface State extends LiveSessionFields {
|
|
|
63
63
|
cannotReorder: boolean;
|
|
64
64
|
reorderMessage: string;
|
|
65
65
|
emptyText: string;
|
|
66
|
+
/** No member session is what stands between the visitor and this order — drives the
|
|
67
|
+
* sign-in CTA, which returns here with `id` once the member has signed in. */
|
|
68
|
+
needsSignIn: boolean;
|
|
69
|
+
noSignIn: boolean;
|
|
66
70
|
isLoading: boolean;
|
|
67
71
|
notLoading: boolean;
|
|
68
72
|
isErrored: boolean;
|
|
@@ -180,6 +184,7 @@ function derive(state: State): State {
|
|
|
180
184
|
isErrored: state.status === "error",
|
|
181
185
|
notErrored: state.status !== "error",
|
|
182
186
|
noEmpty: state.status !== "ready" || hasContent,
|
|
187
|
+
noSignIn: !state.needsSignIn,
|
|
183
188
|
hideContent: !hasContent,
|
|
184
189
|
hasShipping: !!state.shipping,
|
|
185
190
|
noShipping: !state.shipping,
|
|
@@ -249,7 +254,7 @@ function orderState(order: WixOrder): Partial<State> {
|
|
|
249
254
|
let loadVersion = 0;
|
|
250
255
|
let loadedMember: OrderMember | undefined;
|
|
251
256
|
function clearOrder(state: State): State {
|
|
252
|
-
return derive({ ...state, title: "", placedAt: "", fulfillmentStatus: "", paymentStatus: "", subtotal: "", shipping: "", tax: "", discount: "", total: "", rows: [], reorderRefs: [], reorderMessage: "", emptyText: "" });
|
|
257
|
+
return derive({ ...state, title: "", placedAt: "", fulfillmentStatus: "", paymentStatus: "", subtotal: "", shipping: "", tax: "", discount: "", total: "", rows: [], reorderRefs: [], reorderMessage: "", emptyText: "", needsSignIn: false });
|
|
253
258
|
}
|
|
254
259
|
async function load(state: State, arg?: unknown): Promise<State | undefined> {
|
|
255
260
|
const version = ++loadVersion;
|
|
@@ -259,9 +264,10 @@ async function load(state: State, arg?: unknown): Promise<State | undefined> {
|
|
|
259
264
|
const finish = (next: State) => version === loadVersion ? derive(next) : undefined;
|
|
260
265
|
const id = inputId();
|
|
261
266
|
const member = await orderMember();
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
267
|
+
const signInText = copyParam("order.signInEmptyText", "Sign in to view this order.");
|
|
268
|
+
if (!id) return finish({ ...state, status: "ready", emptyText: member.email ? copyParam("order.notFoundEmptyText", "Order not found.") : signInText });
|
|
269
|
+
const signInRequired = (status: State["status"]) => finish({ ...state, id, status, needsSignIn: true, emptyText: signInText });
|
|
270
|
+
if (!member.email) return signInRequired("ready");
|
|
265
271
|
try {
|
|
266
272
|
const order = await findOwnedOrder(id, member.email);
|
|
267
273
|
await assertOrderMember(member);
|
|
@@ -272,11 +278,11 @@ async function load(state: State, arg?: unknown): Promise<State | undefined> {
|
|
|
272
278
|
} catch (error) {
|
|
273
279
|
if (version !== loadVersion) return undefined;
|
|
274
280
|
try { await assertOrderMember(member); } catch {
|
|
275
|
-
return
|
|
281
|
+
return signInRequired("error");
|
|
276
282
|
}
|
|
277
283
|
const cached = orderAccessDenied(error) ? null : await readCachedOrder(id, member);
|
|
278
284
|
try { await assertOrderMember(member); } catch {
|
|
279
|
-
return
|
|
285
|
+
return signInRequired("error");
|
|
280
286
|
}
|
|
281
287
|
if (cached) {
|
|
282
288
|
return finish({ ...state, status: "ready", title: String(cached.title ?? "") || copyTemplate("order.orderTitle", "Order #{number}", { number: id }),
|
|
@@ -326,6 +332,8 @@ const svc = defineService<State, {
|
|
|
326
332
|
cannotReorder: true,
|
|
327
333
|
reorderMessage: "",
|
|
328
334
|
emptyText: "",
|
|
335
|
+
needsSignIn: false,
|
|
336
|
+
noSignIn: true,
|
|
329
337
|
isLoading: true,
|
|
330
338
|
notLoading: false,
|
|
331
339
|
isErrored: false,
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// `wix-site-id` header and nothing else, so every read stayed on the baked site while the
|
|
9
9
|
// UI claimed otherwise. Managing several businesses is Owner Lite's job (account auth, a
|
|
10
10
|
// real Sites List, and per-call site scope via `site.forSite`).
|
|
11
|
-
import { defineService } from "@himalaya/state";
|
|
11
|
+
import { defineService, navigate, type NavDirective } from "@himalaya/state";
|
|
12
12
|
import { dispatch } from "@himalaya/dispatch";
|
|
13
13
|
import { searchOrders, type WixOrder } from "@himalaya/wix-ecom";
|
|
14
14
|
import { guestProfile, loadMemberProfile, strOf, fmtDate, titleCase, type AccountActivityRow } from "../_shared/himi-account";
|
|
@@ -21,6 +21,9 @@ interface AuthRouteInput {
|
|
|
21
21
|
returnId?: string;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
// The screen's boot input survives refresh; consuming it must survive too.
|
|
25
|
+
let routeInputConsumed = false;
|
|
26
|
+
|
|
24
27
|
/** Mid-flow open params land on `himalaya.input` (paramKeys on himi_account). */
|
|
25
28
|
function routeInput(): AuthRouteInput {
|
|
26
29
|
const input = (globalThis as { himalaya?: { input?: AuthRouteInput } }).himalaya?.input;
|
|
@@ -35,7 +38,7 @@ function strField(arg: unknown, key: string): string {
|
|
|
35
38
|
|
|
36
39
|
/** Mid-flow auth (UX F3): intent + return path from himi.open / himalaya.input. */
|
|
37
40
|
function authIntentFromArg(arg: unknown, prev: Pick<S, "authIntent" | "authContextName" | "returnScreenId" | "returnId">): Pick<S, "authIntent" | "authContextName" | "returnScreenId" | "returnId"> {
|
|
38
|
-
const route = routeInput();
|
|
41
|
+
const route = routeInputConsumed ? {} : routeInput();
|
|
39
42
|
return {
|
|
40
43
|
authIntent: strField(arg, "intent") || route.intent || prev.authIntent || "",
|
|
41
44
|
authContextName: strField(arg, "contextName") || route.contextName || prev.authContextName || "",
|
|
@@ -44,6 +47,42 @@ function authIntentFromArg(arg: unknown, prev: Pick<S, "authIntent" | "authConte
|
|
|
44
47
|
};
|
|
45
48
|
}
|
|
46
49
|
|
|
50
|
+
/** Where a successful sign-in returns to, or null. Never Account itself — that would
|
|
51
|
+
* only push a second copy of the screen the member is already on. */
|
|
52
|
+
function resumeTarget(s: Pick<S, "returnScreenId" | "returnId">): NavDirective | null {
|
|
53
|
+
if (!s.returnScreenId || s.returnScreenId === "himi_account") return null;
|
|
54
|
+
return s.returnId ? { screenId: s.returnScreenId, args: { id: s.returnId } } : { screenId: s.returnScreenId };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Resume the member-only screen that sent the visitor here, once. The return path is
|
|
58
|
+
* consumed so a later refresh cannot fire it again; the host opens the target only if
|
|
59
|
+
* it is linkable, the same check an authored `himi.open` gets. */
|
|
60
|
+
function resumeAfterAuth(next: S): S {
|
|
61
|
+
const target = resumeTarget(next);
|
|
62
|
+
if (!next.isMember || !target) return next;
|
|
63
|
+
routeInputConsumed = true;
|
|
64
|
+
return navigate(derive({ ...next, authIntent: "", authContextName: "", returnScreenId: "", returnId: "" }), target);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Actions are not serialized: two quick Sign in taps both run against the pre-tap
|
|
68
|
+
// state, so without this both would submit credentials and both would resume.
|
|
69
|
+
let authInFlight = false;
|
|
70
|
+
|
|
71
|
+
async function authenticate(s: S, submit: () => Promise<unknown>, notMemberMessage: string, failedMessage: string): Promise<S | undefined> {
|
|
72
|
+
if (authInFlight) return undefined;
|
|
73
|
+
authInFlight = true;
|
|
74
|
+
try {
|
|
75
|
+
await submit();
|
|
76
|
+
const next = await probe(optimisticMember(s, s.email));
|
|
77
|
+
if (!next.isMember) return derive({ ...next, message: notMemberMessage });
|
|
78
|
+
return resumeAfterAuth(derive({ ...next, message: "" }));
|
|
79
|
+
} catch {
|
|
80
|
+
return derive({ ...s, message: failedMessage });
|
|
81
|
+
} finally {
|
|
82
|
+
authInFlight = false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
47
86
|
// Next value of a focus token: monotonic, and never empty once requested. A counter
|
|
48
87
|
// rather than Date.now() — deterministic under test, and immune to two focus
|
|
49
88
|
// requests landing inside the same millisecond.
|
|
@@ -123,7 +162,8 @@ interface S {
|
|
|
123
162
|
authContextName: string;
|
|
124
163
|
returnScreenId: string;
|
|
125
164
|
returnId: string;
|
|
126
|
-
/** Show Continue
|
|
165
|
+
/** Show Continue when an already-signed-in member opens Account with a return path.
|
|
166
|
+
* A sign-in made here resumes on its own (resumeAfterAuth). */
|
|
127
167
|
hasReturnPath: boolean;
|
|
128
168
|
noReturnPath: boolean;
|
|
129
169
|
returnCta: string;
|
|
@@ -296,8 +336,8 @@ const svc = defineService<S, {
|
|
|
296
336
|
showSignIn: (s: S) => S;
|
|
297
337
|
showRecovery: (s: S) => S;
|
|
298
338
|
resetPassword: (s: S) => Promise<S>;
|
|
299
|
-
signIn: (s: S) => Promise<S>;
|
|
300
|
-
register: (s: S) => Promise<S>;
|
|
339
|
+
signIn: (s: S) => Promise<S | undefined>;
|
|
340
|
+
register: (s: S) => Promise<S | undefined>;
|
|
301
341
|
signOut: (s: S) => Promise<S>;
|
|
302
342
|
}>("himiStoreAccount", {
|
|
303
343
|
state: derive({
|
|
@@ -381,30 +421,29 @@ const svc = defineService<S, {
|
|
|
381
421
|
},
|
|
382
422
|
signIn: async (s) => {
|
|
383
423
|
if (!s.email || !s.password) return derive({ ...s, message: copyParam("storeAccount.validationEmailPassword", "Enter your email and password.") });
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}
|
|
424
|
+
return authenticate(
|
|
425
|
+
s,
|
|
426
|
+
() => dispatch("himi.wix.signInDirect", { email: s.email, password: s.password }),
|
|
427
|
+
copyParam("storeAccount.signInCheckDetails", "Sign in failed. Check your details."),
|
|
428
|
+
copyParam("storeAccount.signInFailed", "Sign in failed. Try again."),
|
|
429
|
+
);
|
|
391
430
|
},
|
|
392
431
|
register: async (s) => {
|
|
393
432
|
if (!s.email || !s.password) return derive({ ...s, message: copyParam("storeAccount.validationEmailPassword", "Enter your email and password.") });
|
|
394
|
-
|
|
395
|
-
|
|
433
|
+
return authenticate(
|
|
434
|
+
s,
|
|
435
|
+
() => dispatch("himi.wix.registerDirect", {
|
|
396
436
|
email: s.email,
|
|
397
437
|
password: s.password,
|
|
398
438
|
firstName: s.firstName,
|
|
399
439
|
lastName: s.lastName,
|
|
400
|
-
})
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
return derive({ ...s, message: copyParam("storeAccount.registerFailed", "Account creation failed. Try another email.") });
|
|
405
|
-
}
|
|
440
|
+
}),
|
|
441
|
+
copyParam("storeAccount.registerTrySignIn", "Account creation failed. Try signing in."),
|
|
442
|
+
copyParam("storeAccount.registerFailed", "Account creation failed. Try another email."),
|
|
443
|
+
);
|
|
406
444
|
},
|
|
407
445
|
signOut: async (s) => {
|
|
446
|
+
routeInputConsumed = true;
|
|
408
447
|
try { await dispatch("himi.wix.signOut"); } catch { /* ignore */ }
|
|
409
448
|
// Do NOT re-probe via getMyData after logout — visitor-scoped my-data can
|
|
410
449
|
// still return a site nickname and resurrect isMember (BUG-3). Trust the
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// `wix-site-id` header and nothing else, so every read stayed on the baked site while the
|
|
9
9
|
// UI claimed otherwise. Managing several businesses is Owner Lite's job (account auth, a
|
|
10
10
|
// real Sites List, and per-call site scope via `site.forSite`).
|
|
11
|
-
import { defineService } from "@himalaya/state";
|
|
11
|
+
import { defineService, navigate, type NavDirective } from "@himalaya/state";
|
|
12
12
|
import { dispatch } from "@himalaya/dispatch";
|
|
13
13
|
import { searchOrders, type WixOrder } from "@himalaya/wix-ecom";
|
|
14
14
|
import { guestProfile, loadMemberProfile, strOf, fmtDate, titleCase, type AccountActivityRow } from "../_shared/himi-account";
|
|
@@ -21,6 +21,9 @@ interface AuthRouteInput {
|
|
|
21
21
|
returnId?: string;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
// The screen's boot input survives refresh; consuming it must survive too.
|
|
25
|
+
let routeInputConsumed = false;
|
|
26
|
+
|
|
24
27
|
/** Mid-flow open params land on `himalaya.input` (paramKeys on himi_account). */
|
|
25
28
|
function routeInput(): AuthRouteInput {
|
|
26
29
|
const input = (globalThis as { himalaya?: { input?: AuthRouteInput } }).himalaya?.input;
|
|
@@ -35,7 +38,7 @@ function strField(arg: unknown, key: string): string {
|
|
|
35
38
|
|
|
36
39
|
/** Mid-flow auth (UX F3): intent + return path from himi.open / himalaya.input. */
|
|
37
40
|
function authIntentFromArg(arg: unknown, prev: Pick<S, "authIntent" | "authContextName" | "returnScreenId" | "returnId">): Pick<S, "authIntent" | "authContextName" | "returnScreenId" | "returnId"> {
|
|
38
|
-
const route = routeInput();
|
|
41
|
+
const route = routeInputConsumed ? {} : routeInput();
|
|
39
42
|
return {
|
|
40
43
|
authIntent: strField(arg, "intent") || route.intent || prev.authIntent || "",
|
|
41
44
|
authContextName: strField(arg, "contextName") || route.contextName || prev.authContextName || "",
|
|
@@ -44,6 +47,42 @@ function authIntentFromArg(arg: unknown, prev: Pick<S, "authIntent" | "authConte
|
|
|
44
47
|
};
|
|
45
48
|
}
|
|
46
49
|
|
|
50
|
+
/** Where a successful sign-in returns to, or null. Never Account itself — that would
|
|
51
|
+
* only push a second copy of the screen the member is already on. */
|
|
52
|
+
function resumeTarget(s: Pick<S, "returnScreenId" | "returnId">): NavDirective | null {
|
|
53
|
+
if (!s.returnScreenId || s.returnScreenId === "himi_account") return null;
|
|
54
|
+
return s.returnId ? { screenId: s.returnScreenId, args: { id: s.returnId } } : { screenId: s.returnScreenId };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Resume the member-only screen that sent the visitor here, once. The return path is
|
|
58
|
+
* consumed so a later refresh cannot fire it again; the host opens the target only if
|
|
59
|
+
* it is linkable, the same check an authored `himi.open` gets. */
|
|
60
|
+
function resumeAfterAuth(next: S): S {
|
|
61
|
+
const target = resumeTarget(next);
|
|
62
|
+
if (!next.isMember || !target) return next;
|
|
63
|
+
routeInputConsumed = true;
|
|
64
|
+
return navigate(derive({ ...next, authIntent: "", authContextName: "", returnScreenId: "", returnId: "" }), target);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Actions are not serialized: two quick Sign in taps both run against the pre-tap
|
|
68
|
+
// state, so without this both would submit credentials and both would resume.
|
|
69
|
+
let authInFlight = false;
|
|
70
|
+
|
|
71
|
+
async function authenticate(s: S, submit: () => Promise<unknown>, notMemberMessage: string, failedMessage: string): Promise<S | undefined> {
|
|
72
|
+
if (authInFlight) return undefined;
|
|
73
|
+
authInFlight = true;
|
|
74
|
+
try {
|
|
75
|
+
await submit();
|
|
76
|
+
const next = await probe(optimisticMember(s, s.email));
|
|
77
|
+
if (!next.isMember) return derive({ ...next, message: notMemberMessage });
|
|
78
|
+
return resumeAfterAuth(derive({ ...next, message: "" }));
|
|
79
|
+
} catch {
|
|
80
|
+
return derive({ ...s, message: failedMessage });
|
|
81
|
+
} finally {
|
|
82
|
+
authInFlight = false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
47
86
|
// Next value of a focus token: monotonic, and never empty once requested. A counter
|
|
48
87
|
// rather than Date.now() — deterministic under test, and immune to two focus
|
|
49
88
|
// requests landing inside the same millisecond.
|
|
@@ -123,7 +162,8 @@ interface S {
|
|
|
123
162
|
authContextName: string;
|
|
124
163
|
returnScreenId: string;
|
|
125
164
|
returnId: string;
|
|
126
|
-
/** Show Continue
|
|
165
|
+
/** Show Continue when an already-signed-in member opens Account with a return path.
|
|
166
|
+
* A sign-in made here resumes on its own (resumeAfterAuth). */
|
|
127
167
|
hasReturnPath: boolean;
|
|
128
168
|
noReturnPath: boolean;
|
|
129
169
|
returnCta: string;
|
|
@@ -296,8 +336,8 @@ const svc = defineService<S, {
|
|
|
296
336
|
showSignIn: (s: S) => S;
|
|
297
337
|
showRecovery: (s: S) => S;
|
|
298
338
|
resetPassword: (s: S) => Promise<S>;
|
|
299
|
-
signIn: (s: S) => Promise<S>;
|
|
300
|
-
register: (s: S) => Promise<S>;
|
|
339
|
+
signIn: (s: S) => Promise<S | undefined>;
|
|
340
|
+
register: (s: S) => Promise<S | undefined>;
|
|
301
341
|
signOut: (s: S) => Promise<S>;
|
|
302
342
|
}>("himiStoreAccount", {
|
|
303
343
|
state: derive({
|
|
@@ -381,30 +421,29 @@ const svc = defineService<S, {
|
|
|
381
421
|
},
|
|
382
422
|
signIn: async (s) => {
|
|
383
423
|
if (!s.email || !s.password) return derive({ ...s, message: copyParam("storeAccount.validationEmailPassword", "Enter your email and password.") });
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}
|
|
424
|
+
return authenticate(
|
|
425
|
+
s,
|
|
426
|
+
() => dispatch("himi.wix.signInDirect", { email: s.email, password: s.password }),
|
|
427
|
+
copyParam("storeAccount.signInCheckDetails", "Sign in failed. Check your details."),
|
|
428
|
+
copyParam("storeAccount.signInFailed", "Sign in failed. Try again."),
|
|
429
|
+
);
|
|
391
430
|
},
|
|
392
431
|
register: async (s) => {
|
|
393
432
|
if (!s.email || !s.password) return derive({ ...s, message: copyParam("storeAccount.validationEmailPassword", "Enter your email and password.") });
|
|
394
|
-
|
|
395
|
-
|
|
433
|
+
return authenticate(
|
|
434
|
+
s,
|
|
435
|
+
() => dispatch("himi.wix.registerDirect", {
|
|
396
436
|
email: s.email,
|
|
397
437
|
password: s.password,
|
|
398
438
|
firstName: s.firstName,
|
|
399
439
|
lastName: s.lastName,
|
|
400
|
-
})
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
return derive({ ...s, message: copyParam("storeAccount.registerFailed", "Account creation failed. Try another email.") });
|
|
405
|
-
}
|
|
440
|
+
}),
|
|
441
|
+
copyParam("storeAccount.registerTrySignIn", "Account creation failed. Try signing in."),
|
|
442
|
+
copyParam("storeAccount.registerFailed", "Account creation failed. Try another email."),
|
|
443
|
+
);
|
|
406
444
|
},
|
|
407
445
|
signOut: async (s) => {
|
|
446
|
+
routeInputConsumed = true;
|
|
408
447
|
try { await dispatch("himi.wix.signOut"); } catch { /* ignore */ }
|
|
409
448
|
// Do NOT re-probe via getMyData after logout — visitor-scoped my-data can
|
|
410
449
|
// still return a site nickname and resurrect isMember (BUG-3). Trust the
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// `wix-site-id` header and nothing else, so every read stayed on the baked site while the
|
|
9
9
|
// UI claimed otherwise. Managing several businesses is Owner Lite's job (account auth, a
|
|
10
10
|
// real Sites List, and per-call site scope via `site.forSite`).
|
|
11
|
-
import { defineService } from "@himalaya/state";
|
|
11
|
+
import { defineService, navigate, type NavDirective } from "@himalaya/state";
|
|
12
12
|
import { dispatch } from "@himalaya/dispatch";
|
|
13
13
|
import { searchOrders, type WixOrder } from "@himalaya/wix-ecom";
|
|
14
14
|
import { guestProfile, loadMemberProfile, strOf, fmtDate, titleCase, type AccountActivityRow } from "../_shared/himi-account";
|
|
@@ -21,6 +21,9 @@ interface AuthRouteInput {
|
|
|
21
21
|
returnId?: string;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
// The screen's boot input survives refresh; consuming it must survive too.
|
|
25
|
+
let routeInputConsumed = false;
|
|
26
|
+
|
|
24
27
|
/** Mid-flow open params land on `himalaya.input` (paramKeys on himi_account). */
|
|
25
28
|
function routeInput(): AuthRouteInput {
|
|
26
29
|
const input = (globalThis as { himalaya?: { input?: AuthRouteInput } }).himalaya?.input;
|
|
@@ -35,7 +38,7 @@ function strField(arg: unknown, key: string): string {
|
|
|
35
38
|
|
|
36
39
|
/** Mid-flow auth (UX F3): intent + return path from himi.open / himalaya.input. */
|
|
37
40
|
function authIntentFromArg(arg: unknown, prev: Pick<S, "authIntent" | "authContextName" | "returnScreenId" | "returnId">): Pick<S, "authIntent" | "authContextName" | "returnScreenId" | "returnId"> {
|
|
38
|
-
const route = routeInput();
|
|
41
|
+
const route = routeInputConsumed ? {} : routeInput();
|
|
39
42
|
return {
|
|
40
43
|
authIntent: strField(arg, "intent") || route.intent || prev.authIntent || "",
|
|
41
44
|
authContextName: strField(arg, "contextName") || route.contextName || prev.authContextName || "",
|
|
@@ -44,6 +47,42 @@ function authIntentFromArg(arg: unknown, prev: Pick<S, "authIntent" | "authConte
|
|
|
44
47
|
};
|
|
45
48
|
}
|
|
46
49
|
|
|
50
|
+
/** Where a successful sign-in returns to, or null. Never Account itself — that would
|
|
51
|
+
* only push a second copy of the screen the member is already on. */
|
|
52
|
+
function resumeTarget(s: Pick<S, "returnScreenId" | "returnId">): NavDirective | null {
|
|
53
|
+
if (!s.returnScreenId || s.returnScreenId === "himi_account") return null;
|
|
54
|
+
return s.returnId ? { screenId: s.returnScreenId, args: { id: s.returnId } } : { screenId: s.returnScreenId };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Resume the member-only screen that sent the visitor here, once. The return path is
|
|
58
|
+
* consumed so a later refresh cannot fire it again; the host opens the target only if
|
|
59
|
+
* it is linkable, the same check an authored `himi.open` gets. */
|
|
60
|
+
function resumeAfterAuth(next: S): S {
|
|
61
|
+
const target = resumeTarget(next);
|
|
62
|
+
if (!next.isMember || !target) return next;
|
|
63
|
+
routeInputConsumed = true;
|
|
64
|
+
return navigate(derive({ ...next, authIntent: "", authContextName: "", returnScreenId: "", returnId: "" }), target);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Actions are not serialized: two quick Sign in taps both run against the pre-tap
|
|
68
|
+
// state, so without this both would submit credentials and both would resume.
|
|
69
|
+
let authInFlight = false;
|
|
70
|
+
|
|
71
|
+
async function authenticate(s: S, submit: () => Promise<unknown>, notMemberMessage: string, failedMessage: string): Promise<S | undefined> {
|
|
72
|
+
if (authInFlight) return undefined;
|
|
73
|
+
authInFlight = true;
|
|
74
|
+
try {
|
|
75
|
+
await submit();
|
|
76
|
+
const next = await probe(optimisticMember(s, s.email));
|
|
77
|
+
if (!next.isMember) return derive({ ...next, message: notMemberMessage });
|
|
78
|
+
return resumeAfterAuth(derive({ ...next, message: "" }));
|
|
79
|
+
} catch {
|
|
80
|
+
return derive({ ...s, message: failedMessage });
|
|
81
|
+
} finally {
|
|
82
|
+
authInFlight = false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
47
86
|
// Next value of a focus token: monotonic, and never empty once requested. A counter
|
|
48
87
|
// rather than Date.now() — deterministic under test, and immune to two focus
|
|
49
88
|
// requests landing inside the same millisecond.
|
|
@@ -123,7 +162,8 @@ interface S {
|
|
|
123
162
|
authContextName: string;
|
|
124
163
|
returnScreenId: string;
|
|
125
164
|
returnId: string;
|
|
126
|
-
/** Show Continue
|
|
165
|
+
/** Show Continue when an already-signed-in member opens Account with a return path.
|
|
166
|
+
* A sign-in made here resumes on its own (resumeAfterAuth). */
|
|
127
167
|
hasReturnPath: boolean;
|
|
128
168
|
noReturnPath: boolean;
|
|
129
169
|
returnCta: string;
|
|
@@ -296,8 +336,8 @@ const svc = defineService<S, {
|
|
|
296
336
|
showSignIn: (s: S) => S;
|
|
297
337
|
showRecovery: (s: S) => S;
|
|
298
338
|
resetPassword: (s: S) => Promise<S>;
|
|
299
|
-
signIn: (s: S) => Promise<S>;
|
|
300
|
-
register: (s: S) => Promise<S>;
|
|
339
|
+
signIn: (s: S) => Promise<S | undefined>;
|
|
340
|
+
register: (s: S) => Promise<S | undefined>;
|
|
301
341
|
signOut: (s: S) => Promise<S>;
|
|
302
342
|
}>("himiStoreAccount", {
|
|
303
343
|
state: derive({
|
|
@@ -381,30 +421,29 @@ const svc = defineService<S, {
|
|
|
381
421
|
},
|
|
382
422
|
signIn: async (s) => {
|
|
383
423
|
if (!s.email || !s.password) return derive({ ...s, message: copyParam("storeAccount.validationEmailPassword", "Enter your email and password.") });
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}
|
|
424
|
+
return authenticate(
|
|
425
|
+
s,
|
|
426
|
+
() => dispatch("himi.wix.signInDirect", { email: s.email, password: s.password }),
|
|
427
|
+
copyParam("storeAccount.signInCheckDetails", "Sign in failed. Check your details."),
|
|
428
|
+
copyParam("storeAccount.signInFailed", "Sign in failed. Try again."),
|
|
429
|
+
);
|
|
391
430
|
},
|
|
392
431
|
register: async (s) => {
|
|
393
432
|
if (!s.email || !s.password) return derive({ ...s, message: copyParam("storeAccount.validationEmailPassword", "Enter your email and password.") });
|
|
394
|
-
|
|
395
|
-
|
|
433
|
+
return authenticate(
|
|
434
|
+
s,
|
|
435
|
+
() => dispatch("himi.wix.registerDirect", {
|
|
396
436
|
email: s.email,
|
|
397
437
|
password: s.password,
|
|
398
438
|
firstName: s.firstName,
|
|
399
439
|
lastName: s.lastName,
|
|
400
|
-
})
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
return derive({ ...s, message: copyParam("storeAccount.registerFailed", "Account creation failed. Try another email.") });
|
|
405
|
-
}
|
|
440
|
+
}),
|
|
441
|
+
copyParam("storeAccount.registerTrySignIn", "Account creation failed. Try signing in."),
|
|
442
|
+
copyParam("storeAccount.registerFailed", "Account creation failed. Try another email."),
|
|
443
|
+
);
|
|
406
444
|
},
|
|
407
445
|
signOut: async (s) => {
|
|
446
|
+
routeInputConsumed = true;
|
|
408
447
|
try { await dispatch("himi.wix.signOut"); } catch { /* ignore */ }
|
|
409
448
|
// Do NOT re-probe via getMyData after logout — visitor-scoped my-data can
|
|
410
449
|
// still return a site nickname and resurrect isMember (BUG-3). Trust the
|
|
@@ -9363,7 +9363,8 @@ const SCREENS = runtimeSetting({
|
|
|
9363
9363
|
"action": {
|
|
9364
9364
|
"actionId": "himi.open",
|
|
9365
9365
|
"params": {
|
|
9366
|
-
"screenId": "himi_account"
|
|
9366
|
+
"screenId": "himi_account",
|
|
9367
|
+
"returnScreenId": "himi_orders"
|
|
9367
9368
|
}
|
|
9368
9369
|
}
|
|
9369
9370
|
}
|
|
@@ -14514,7 +14515,8 @@ const SCREENS = runtimeSetting({
|
|
|
14514
14515
|
"action": {
|
|
14515
14516
|
"actionId": "himi.open",
|
|
14516
14517
|
"params": {
|
|
14517
|
-
"screenId": "himi_account"
|
|
14518
|
+
"screenId": "himi_account",
|
|
14519
|
+
"returnScreenId": "himi_bookings"
|
|
14518
14520
|
}
|
|
14519
14521
|
}
|
|
14520
14522
|
}
|
|
@@ -27231,6 +27233,29 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
27231
27233
|
},
|
|
27232
27234
|
"hiddenPath": "$service:himiBrandedOrder.noEmpty"
|
|
27233
27235
|
}
|
|
27236
|
+
},
|
|
27237
|
+
{
|
|
27238
|
+
"id": "himi_order.signin",
|
|
27239
|
+
"body": {
|
|
27240
|
+
"package": "himalaya",
|
|
27241
|
+
"type": "Button",
|
|
27242
|
+
"props": {
|
|
27243
|
+
"title": "Sign in",
|
|
27244
|
+
"style": "primary",
|
|
27245
|
+
"fullWidth": true,
|
|
27246
|
+
"action": {
|
|
27247
|
+
"actionId": "himi.open",
|
|
27248
|
+
"params": {
|
|
27249
|
+
"screenId": "himi_account",
|
|
27250
|
+
"returnScreenId": "himi_order",
|
|
27251
|
+
"returnId": "$service:himiBrandedOrder.id"
|
|
27252
|
+
}
|
|
27253
|
+
}
|
|
27254
|
+
}
|
|
27255
|
+
},
|
|
27256
|
+
"modifiers": {
|
|
27257
|
+
"hiddenPath": "$service:himiBrandedOrder.noSignIn"
|
|
27258
|
+
}
|
|
27234
27259
|
}
|
|
27235
27260
|
]
|
|
27236
27261
|
}
|
|
@@ -63,6 +63,10 @@ interface State extends LiveSessionFields {
|
|
|
63
63
|
cannotReorder: boolean;
|
|
64
64
|
reorderMessage: string;
|
|
65
65
|
emptyText: string;
|
|
66
|
+
/** No member session is what stands between the visitor and this order — drives the
|
|
67
|
+
* sign-in CTA, which returns here with `id` once the member has signed in. */
|
|
68
|
+
needsSignIn: boolean;
|
|
69
|
+
noSignIn: boolean;
|
|
66
70
|
isLoading: boolean;
|
|
67
71
|
notLoading: boolean;
|
|
68
72
|
isErrored: boolean;
|
|
@@ -180,6 +184,7 @@ function derive(state: State): State {
|
|
|
180
184
|
isErrored: state.status === "error",
|
|
181
185
|
notErrored: state.status !== "error",
|
|
182
186
|
noEmpty: state.status !== "ready" || hasContent,
|
|
187
|
+
noSignIn: !state.needsSignIn,
|
|
183
188
|
hideContent: !hasContent,
|
|
184
189
|
hasShipping: !!state.shipping,
|
|
185
190
|
noShipping: !state.shipping,
|
|
@@ -249,7 +254,7 @@ function orderState(order: WixOrder): Partial<State> {
|
|
|
249
254
|
let loadVersion = 0;
|
|
250
255
|
let loadedMember: OrderMember | undefined;
|
|
251
256
|
function clearOrder(state: State): State {
|
|
252
|
-
return derive({ ...state, title: "", placedAt: "", fulfillmentStatus: "", paymentStatus: "", subtotal: "", shipping: "", tax: "", discount: "", total: "", rows: [], reorderRefs: [], reorderMessage: "", emptyText: "" });
|
|
257
|
+
return derive({ ...state, title: "", placedAt: "", fulfillmentStatus: "", paymentStatus: "", subtotal: "", shipping: "", tax: "", discount: "", total: "", rows: [], reorderRefs: [], reorderMessage: "", emptyText: "", needsSignIn: false });
|
|
253
258
|
}
|
|
254
259
|
async function load(state: State, arg?: unknown): Promise<State | undefined> {
|
|
255
260
|
const version = ++loadVersion;
|
|
@@ -259,9 +264,10 @@ async function load(state: State, arg?: unknown): Promise<State | undefined> {
|
|
|
259
264
|
const finish = (next: State) => version === loadVersion ? derive(next) : undefined;
|
|
260
265
|
const id = inputId();
|
|
261
266
|
const member = await orderMember();
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
267
|
+
const signInText = copyParam("order.signInEmptyText", "Sign in to view this order.");
|
|
268
|
+
if (!id) return finish({ ...state, status: "ready", emptyText: member.email ? copyParam("order.notFoundEmptyText", "Order not found.") : signInText });
|
|
269
|
+
const signInRequired = (status: State["status"]) => finish({ ...state, id, status, needsSignIn: true, emptyText: signInText });
|
|
270
|
+
if (!member.email) return signInRequired("ready");
|
|
265
271
|
try {
|
|
266
272
|
const order = await findOwnedOrder(id, member.email);
|
|
267
273
|
await assertOrderMember(member);
|
|
@@ -272,11 +278,11 @@ async function load(state: State, arg?: unknown): Promise<State | undefined> {
|
|
|
272
278
|
} catch (error) {
|
|
273
279
|
if (version !== loadVersion) return undefined;
|
|
274
280
|
try { await assertOrderMember(member); } catch {
|
|
275
|
-
return
|
|
281
|
+
return signInRequired("error");
|
|
276
282
|
}
|
|
277
283
|
const cached = orderAccessDenied(error) ? null : await readCachedOrder(id, member);
|
|
278
284
|
try { await assertOrderMember(member); } catch {
|
|
279
|
-
return
|
|
285
|
+
return signInRequired("error");
|
|
280
286
|
}
|
|
281
287
|
if (cached) {
|
|
282
288
|
return finish({ ...state, status: "ready", title: String(cached.title ?? "") || copyTemplate("order.orderTitle", "Order #{number}", { number: id }),
|
|
@@ -326,6 +332,8 @@ const svc = defineService<State, {
|
|
|
326
332
|
cannotReorder: true,
|
|
327
333
|
reorderMessage: "",
|
|
328
334
|
emptyText: "",
|
|
335
|
+
needsSignIn: false,
|
|
336
|
+
noSignIn: true,
|
|
329
337
|
isLoading: true,
|
|
330
338
|
notLoading: false,
|
|
331
339
|
isErrored: false,
|