@wix/himalaya-cli 0.1524.0 → 0.1526.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.
Files changed (55) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/dev-server/src/builder.d.ts +14 -0
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/schema.d.ts +22 -0
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/worker-subtree-safe-types.generated.d.ts +4 -0
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/apple-iap-contract.d.ts +5 -1
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/cashier-charges.d.ts +3 -0
  6. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/core.mjs +7 -0
  7. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap-checkout.mjs +35 -9
  8. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap.mjs +14 -1
  9. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/screens.mjs +34 -8
  10. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/sdui.mjs +13 -0
  11. package/dist/cli.mjs +136 -39
  12. package/dist/preview-assets.generated.bin +0 -0
  13. package/guides/designing-himi-screens.md +43 -18
  14. package/guides/recipe-dynamic-list.md +7 -7
  15. package/guides/recipe-four-state.md +47 -41
  16. package/guides/recipe-search.md +1 -1
  17. package/package.json +2 -2
  18. package/templates/beauty-salon/dev/index.ts +69 -40
  19. package/templates/beauty-salon/tier5-src/himi-branded-lite-agenda/index.ts +9 -1
  20. package/templates/beauty-salon/tier5-src/himi-branded-lite-place-summary/index.ts +12 -1
  21. package/templates/beauty-salon/tier5-src/himi-store-account/index.ts +69 -10
  22. package/templates/beauty-salon/tier5-src/himi-store-product/index.ts +153 -132
  23. package/templates/branded-lite/dev/index.ts +165 -88
  24. package/templates/branded-lite/tier5-src/business-chat/index.ts +4 -0
  25. package/templates/branded-lite/tier5-src/himi-branded-lite-agenda/index.ts +9 -1
  26. package/templates/branded-lite/tier5-src/himi-branded-lite-place-summary/index.ts +12 -1
  27. package/templates/branded-lite/tier5-src/himi-store-account/index.ts +69 -10
  28. package/templates/branded-lite/tier5-src/himi-store-product/index.ts +153 -132
  29. package/templates/community-club/dev/index.ts +40 -21
  30. package/templates/community-club/tier5-src/himi-store-account/index.ts +69 -10
  31. package/templates/events-meetups/dev/index.ts +36 -19
  32. package/templates/events-meetups/tier5-src/himi-store-account/index.ts +69 -10
  33. package/templates/gym-fitness/dev/index.ts +73 -42
  34. package/templates/gym-fitness/tier5-src/himi-branded-lite-agenda/index.ts +9 -1
  35. package/templates/gym-fitness/tier5-src/himi-branded-lite-place-summary/index.ts +12 -1
  36. package/templates/gym-fitness/tier5-src/himi-store-account/index.ts +69 -10
  37. package/templates/gym-fitness/tier5-src/himi-store-product/index.ts +153 -132
  38. package/templates/nonprofit-cause/dev/index.ts +47 -29
  39. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-place-summary/index.ts +12 -1
  40. package/templates/nonprofit-cause/tier5-src/himi-store-account/index.ts +69 -10
  41. package/templates/podcast-creator/dev/index.ts +47 -29
  42. package/templates/podcast-creator/tier5-src/himi-branded-lite-place-summary/index.ts +12 -1
  43. package/templates/podcast-creator/tier5-src/himi-store-account/index.ts +69 -10
  44. package/templates/portfolio-creative/dev/index.ts +52 -27
  45. package/templates/portfolio-creative/tier5-src/himi-store-account/index.ts +69 -10
  46. package/templates/portfolio-creative/tier5-src/himi-store-product/index.ts +153 -132
  47. package/templates/restaurant/dev/index.ts +48 -25
  48. package/templates/restaurant/tier5-src/himi-store-account/index.ts +69 -10
  49. package/templates/retail-store/dev/index.ts +44 -23
  50. package/templates/retail-store/tier5-src/himi-store-account/index.ts +69 -10
  51. package/templates/retail-store/tier5-src/himi-store-product/index.ts +153 -132
  52. package/templates/yoga-studio/dev/index.ts +57 -34
  53. package/templates/yoga-studio/tier5-src/himi-branded-lite-agenda/index.ts +9 -1
  54. package/templates/yoga-studio/tier5-src/himi-branded-lite-place-summary/index.ts +12 -1
  55. package/templates/yoga-studio/tier5-src/himi-store-account/index.ts +69 -10
@@ -26,6 +26,10 @@ interface AuthRouteInput {
26
26
  returnScreenId?: string;
27
27
  returnId?: string;
28
28
  returnChannelId?: string;
29
+ /** Standalone sign-in only: "1" when it was opened as a modal (sheet / cover). */
30
+ modal?: string;
31
+ /** Modal only: the tab whose stack the return screen opens on once the modal closes. */
32
+ returnTab?: string;
29
33
  }
30
34
 
31
35
  // The screen's boot input survives refresh; consuming it must survive too.
@@ -61,6 +65,12 @@ function authIntentFromArg(arg: unknown, prev: Pick<S, "authIntent" | "authConte
61
65
  };
62
66
  }
63
67
 
68
+ /** How the standalone form leaves. A modal is closed: on web `back` pops the tab's stack,
69
+ * not the modal. A pushed form goes back. */
70
+ function leaveNav(): "dismiss" | "back" {
71
+ return routeInput().modal === "1" ? "dismiss" : "back";
72
+ }
73
+
64
74
  /** Where a successful sign-in returns to, or null. Never Account itself — that would
65
75
  * only push a second copy of the screen the member is already on. */
66
76
  function resumeTarget(s: Pick<S, "returnScreenId" | "returnId" | "returnChannelId">): { screenId: string; args?: Record<string, string> } | null {
@@ -78,11 +88,26 @@ function resumeTarget(s: Pick<S, "returnScreenId" | "returnId" | "returnChannelI
78
88
  * On the standalone sign-in screen the target is pushed, not a `replace`: on iOS and
79
89
  * Android `replace` resets the stack to its root, which would drop the screens the
80
90
  * visitor came through. Back from the target shows the form again, and the form sends a
81
- * member back on appear (settle). With no target the form goes back. */
91
+ * member back on appear (settle). With no target the form goes back.
92
+ *
93
+ * A modal form closes. A target pushed from here would open inside the modal (iOS, Android)
94
+ * or under it (web), so a modal that names a `returnTab` hands the target to that tab: the
95
+ * host closes the modal, then pushes it on that tab's stack (Account's member rows). A host
96
+ * showing no such tab (a drawer, a narrow Mac window) drops that request whole, modal and
97
+ * all, so the modal is also dismissed directly: it always closes, and the row opens where
98
+ * the tab exists. With no `returnTab` it only closes, and the screen under it loads again —
99
+ * the screen that opened the modal is the return target. */
82
100
  function resumeAfterAuth(next: S): S {
83
101
  const target = resumeTarget(next);
84
102
  if (!next.isMember) return next;
85
- if (!target) return next.standalone ? navigate(next, { presentation: "back" }) : next;
103
+ const tab = routeInput().modal === "1" ? routeInput().returnTab ?? "" : "";
104
+ if (target && tab && next.standalone) {
105
+ routeInputConsumed = true;
106
+ void dispatch("himi.open", { presentation: "dismiss" });
107
+ const cleared = derive({ ...next, authIntent: "", authContextName: "", returnScreenId: "", returnId: "", returnChannelId: "" });
108
+ return navigate(cleared, target.args ? { tab, screenId: target.screenId, args: target.args } : { tab, screenId: target.screenId });
109
+ }
110
+ if (!target || routeInput().modal === "1") return next.standalone ? navigate(next, { presentation: leaveNav() }) : next;
86
111
  routeInputConsumed = true;
87
112
  const cleared = derive({ ...next, authIntent: "", authContextName: "", returnScreenId: "", returnId: "", returnChannelId: "" });
88
113
  return navigate(cleared, target);
@@ -96,11 +121,31 @@ function forwardToSignIn(next: S): S {
96
121
  if (!next.authIntent && !next.returnScreenId) return next;
97
122
  routeInputConsumed = true;
98
123
  const args: Record<string, string> = { mode: "signin" };
99
- for (const key of ["authIntent", "authContextName", "returnScreenId", "returnId", "returnChannelId"] as const) {
100
- if (next[key]) args[key === "authIntent" ? "intent" : key === "authContextName" ? "contextName" : key] = next[key];
124
+ for (const key of ["authIntent", "authContextName"] as const) {
125
+ if (next[key]) args[key === "authIntent" ? "intent" : "contextName"] = next[key];
126
+ }
127
+ return openSignIn(derive({ ...next, authIntent: "", authContextName: "", returnScreenId: "", returnId: "", returnChannelId: "" }),
128
+ args, resumeTarget(next));
129
+ }
130
+
131
+ /** Account sends a guest to the standalone sign-in screen, with `after`, the member screen to
132
+ * open once signed in. With a `signInPresentation` appear param it opens as that modal, and
133
+ * `after` goes on Account's own tab (`signInReturnTab`) when the modal closes; pushed, the form
134
+ * pushes `after` itself (resumeAfterAuth). */
135
+ function openSignIn(next: S, args: Record<string, string>, after: { screenId: string; args?: Record<string, string> } | null): S {
136
+ const screenId = next.signInScreen || "himi_sign_in";
137
+ const presentation = signInPresentationRequested;
138
+ const back: Record<string, string> = {};
139
+ if (after) {
140
+ back.returnScreenId = after.screenId;
141
+ if (after.args?.id) back.returnId = after.args.id;
142
+ if (after.args?.channelId) back.returnChannelId = after.args.channelId;
101
143
  }
102
- return navigate(derive({ ...next, authIntent: "", authContextName: "", returnScreenId: "", returnId: "", returnChannelId: "" }),
103
- { screenId: next.signInScreen, args });
144
+ if (!presentation) return navigate(next, { screenId, args: { ...args, ...back } });
145
+ const modalArgs: Record<string, string> = { ...args, modal: "1" };
146
+ // With no tab to open it on, a modal has no way to the member screen: it only closes.
147
+ if (after && signInReturnTabRequested) Object.assign(modalArgs, back, { returnTab: signInReturnTabRequested });
148
+ return navigate(next, { screenId, presentation, args: modalArgs });
104
149
  }
105
150
 
106
151
  /** After a load: the standalone sign-in screen sends a member back (a stale link, or a
@@ -128,8 +173,11 @@ function settle(next: S, arg: unknown, latest = true): S {
128
173
  // The appear refresh returns `_nav`, which every host applies — binaries from before
129
174
  // worker nav dispatch included. Only an arg-less load, whose result iOS
130
175
  // discards, dispatches.
131
- if (strField(arg, "standalone") === "1") return navigate(cleared, { presentation: "back" });
132
- void dispatch("himi.open", { presentation: "back" });
176
+ if (strField(arg, "standalone") === "1") return navigate(cleared, { presentation: leaveNav() });
177
+ // Not from a modal: nothing pushed over it comes back to it, and on iOS the closing modal's
178
+ // own screen reloads here once, after it has asked to close — a late dismiss would close
179
+ // whatever modal is open by then.
180
+ if (routeInput().modal !== "1") void dispatch("himi.open", { presentation: leaveNav() });
133
181
  return cleared;
134
182
  }
135
183
  // Account forwards only on an appear refresh: the boot `load()` has no params and its
@@ -180,6 +228,10 @@ let appliedSeq = 0;
180
228
  // The appear params belong to the worker session too, like the two flags above: a probe
181
229
  // dropped because the visitor typed must not take them with it.
182
230
  let signInScreenRequested = "";
231
+ /** Account only: how it opens the standalone sign-in screen ("" = push). */
232
+ let signInPresentationRequested: "" | "sheet" | "full_screen" = "";
233
+ /** Account only: its tab, where a member screen opens after a modal sign-in. */
234
+ let signInReturnTabRequested = "";
183
235
  let standaloneRequested = false;
184
236
  let pollIdentityRequested = false;
185
237
  // Bumped by every mode switch, so a dropped probe never puts back a mode the visitor left.
@@ -210,6 +262,9 @@ function appearConfig(s: S, arg: unknown): AppearConfig {
210
262
  passwordDraft = "";
211
263
  }
212
264
  signInScreenRequested ||= strField(arg, "signInScreen") || s.signInScreen;
265
+ const presentation = strField(arg, "signInPresentation");
266
+ if (presentation === "sheet" || presentation === "full_screen") signInPresentationRequested = presentation;
267
+ signInReturnTabRequested ||= strField(arg, "signInReturnTab");
213
268
  // Only the sign-in screen's route carries `mode`, so it marks the form from the boot load on.
214
269
  standaloneRequested ||= s.standalone || appearing || (!routeInputConsumed && !!routeInput().mode);
215
270
  pollIdentityRequested ||= s.pollIdentity || strField(arg, "pollIdentity") === "1";
@@ -676,6 +731,7 @@ const svc = defineService<S, {
676
731
  signOut: (s: S) => Promise<S>;
677
732
  sync: (s: S) => Promise<S | undefined>;
678
733
  openMember: (s: S, arg?: unknown) => Promise<S>;
734
+ leave: (s: S) => S;
679
735
  restorePublish: (s: S) => S;
680
736
  iapRestore: (s: S) => Promise<undefined>;
681
737
  }>("himiStoreAccount", {
@@ -756,7 +812,8 @@ const svc = defineService<S, {
756
812
  return "next" in r ? r.next : undefined;
757
813
  },
758
814
  // A row to a member-only screen shows to a guest too. A member opens it; a guest signs
759
- // in first and the sign-in screen replaces itself with it, so Back returns here.
815
+ // in first, and the sign-in screen then opens the row's screen — as a modal, by closing
816
+ // and pushing it on this tab (openSignIn) — so Back returns here either way.
760
817
  openMember: async (s, arg): Promise<S> => {
761
818
  const screenId = strField(arg, "screenId");
762
819
  if (!screenId) return s;
@@ -764,8 +821,10 @@ const svc = defineService<S, {
764
821
  // From the state as it is now: a probe may have landed during the identity read.
765
822
  const now = svc.getState();
766
823
  if (member) return navigate(now, { screenId });
767
- return navigate(now, { screenId: now.signInScreen || "himi_sign_in", args: { mode: "signin", returnScreenId: screenId } });
824
+ return openSignIn(now, { mode: "signin" }, { screenId });
768
825
  },
826
+ // The standalone form's Continue: leaves the form the way it was opened.
827
+ leave: (s) => navigate(s, { presentation: leaveNav() }),
769
828
  // Typing means any focus request was delivered: clear the tokens (safe while mounted, since
770
829
  // bumpToken never repeats), so a visit rarely ends — and a pooled form rarely reopens —
771
830
  // holding one.
@@ -2284,8 +2284,10 @@ const SCREENS = runtimeSetting({
2284
2284
  "actionId": "himi.open",
2285
2285
  "params": {
2286
2286
  "screenId": "himi_sign_in",
2287
- "mode": "signin"
2288
- }
2287
+ "mode": "signin",
2288
+ "modal": "1"
2289
+ },
2290
+ "presentation": "sheet"
2289
2291
  }
2290
2292
  }
2291
2293
  }
@@ -2303,8 +2305,10 @@ const SCREENS = runtimeSetting({
2303
2305
  "actionId": "himi.open",
2304
2306
  "params": {
2305
2307
  "screenId": "himi_sign_in",
2306
- "mode": "register"
2307
- }
2308
+ "mode": "register",
2309
+ "modal": "1"
2310
+ },
2311
+ "presentation": "sheet"
2308
2312
  }
2309
2313
  }
2310
2314
  }
@@ -3261,6 +3265,8 @@ const SCREENS = runtimeSetting({
3261
3265
  "memberAccountLinks": "1",
3262
3266
  "restorePurchases": "1",
3263
3267
  "signInScreen": "himi_sign_in",
3268
+ "signInPresentation": "sheet",
3269
+ "signInReturnTab": "profile",
3264
3270
  "pollIdentity": "1",
3265
3271
  "copy": "{\"account.subtitle\":\"RSVPs, groups and saved content in one place.\",\"storeAccount.authBody\":\"Sign in to continue — RSVPs and groups stay with you.\",\"storeAccount.authBodyRegister\":\"Create a member account to continue — RSVPs and groups stay with you.\"}"
3266
3272
  }
@@ -3516,8 +3522,10 @@ const SCREENS = runtimeSetting({
3516
3522
  "params": {
3517
3523
  "screenId": "himi_sign_in",
3518
3524
  "mode": "signin",
3519
- "returnScreenId": "himi_account_settings"
3520
- }
3525
+ "returnScreenId": "himi_account_settings",
3526
+ "modal": "1"
3527
+ },
3528
+ "presentation": "sheet"
3521
3529
  }
3522
3530
  }
3523
3531
  },
@@ -5427,8 +5435,7 @@ const SCREENS = runtimeSetting({
5427
5435
  "style": "primary",
5428
5436
  "fullWidth": true,
5429
5437
  "action": {
5430
- "actionId": "nav.back",
5431
- "presentation": "back"
5438
+ "actionId": "tier5.himiStoreAccount.leave"
5432
5439
  }
5433
5440
  }
5434
5441
  }
@@ -5469,7 +5476,7 @@ const SCREENS = runtimeSetting({
5469
5476
  "declaredCapabilities": [
5470
5477
  "wix_api"
5471
5478
  ],
5472
- "minBinaryVersion": "0.2.0",
5479
+ "minBinaryVersion": "1.7.0",
5473
5480
  "bundleVersion": "0.0.1"
5474
5481
  }
5475
5482
  },
@@ -6336,8 +6343,10 @@ const SCREENS = runtimeSetting({
6336
6343
  "screenId": "himi_sign_in",
6337
6344
  "mode": "signin",
6338
6345
  "intent": "profile",
6339
- "returnScreenId": "himi_view_profile"
6340
- }
6346
+ "returnScreenId": "himi_view_profile",
6347
+ "modal": "1"
6348
+ },
6349
+ "presentation": "sheet"
6341
6350
  }
6342
6351
  }
6343
6352
  }
@@ -7068,8 +7077,10 @@ const SCREENS = runtimeSetting({
7068
7077
  "screenId": "himi_sign_in",
7069
7078
  "mode": "signin",
7070
7079
  "intent": "post",
7071
- "returnScreenId": "himi_feed"
7072
- }
7080
+ "returnScreenId": "himi_feed",
7081
+ "modal": "1"
7082
+ },
7083
+ "presentation": "sheet"
7073
7084
  }
7074
7085
  }
7075
7086
  }
@@ -7686,8 +7697,10 @@ const SCREENS = runtimeSetting({
7686
7697
  "screenId": "himi_sign_in",
7687
7698
  "mode": "signin",
7688
7699
  "intent": "post",
7689
- "returnScreenId": "himi_create_post"
7690
- }
7700
+ "returnScreenId": "himi_create_post",
7701
+ "modal": "1"
7702
+ },
7703
+ "presentation": "sheet"
7691
7704
  }
7692
7705
  }
7693
7706
  }
@@ -18813,8 +18826,10 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
18813
18826
  "params": {
18814
18827
  "screenId": "himi_sign_in",
18815
18828
  "mode": "signin",
18816
- "intent": "rsvp"
18817
- }
18829
+ "intent": "rsvp",
18830
+ "modal": "1"
18831
+ },
18832
+ "presentation": "sheet"
18818
18833
  }
18819
18834
  }
18820
18835
  },
@@ -20146,8 +20161,10 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
20146
20161
  "intent": "join_group",
20147
20162
  "contextName": "$service:himiBrandedGroup.name",
20148
20163
  "returnScreenId": "himi_group",
20149
- "returnId": "$service:himiBrandedGroup.id"
20150
- }
20164
+ "returnId": "$service:himiBrandedGroup.id",
20165
+ "modal": "1"
20166
+ },
20167
+ "presentation": "sheet"
20151
20168
  }
20152
20169
  }
20153
20170
  },
@@ -26,6 +26,10 @@ interface AuthRouteInput {
26
26
  returnScreenId?: string;
27
27
  returnId?: string;
28
28
  returnChannelId?: string;
29
+ /** Standalone sign-in only: "1" when it was opened as a modal (sheet / cover). */
30
+ modal?: string;
31
+ /** Modal only: the tab whose stack the return screen opens on once the modal closes. */
32
+ returnTab?: string;
29
33
  }
30
34
 
31
35
  // The screen's boot input survives refresh; consuming it must survive too.
@@ -61,6 +65,12 @@ function authIntentFromArg(arg: unknown, prev: Pick<S, "authIntent" | "authConte
61
65
  };
62
66
  }
63
67
 
68
+ /** How the standalone form leaves. A modal is closed: on web `back` pops the tab's stack,
69
+ * not the modal. A pushed form goes back. */
70
+ function leaveNav(): "dismiss" | "back" {
71
+ return routeInput().modal === "1" ? "dismiss" : "back";
72
+ }
73
+
64
74
  /** Where a successful sign-in returns to, or null. Never Account itself — that would
65
75
  * only push a second copy of the screen the member is already on. */
66
76
  function resumeTarget(s: Pick<S, "returnScreenId" | "returnId" | "returnChannelId">): { screenId: string; args?: Record<string, string> } | null {
@@ -78,11 +88,26 @@ function resumeTarget(s: Pick<S, "returnScreenId" | "returnId" | "returnChannelI
78
88
  * On the standalone sign-in screen the target is pushed, not a `replace`: on iOS and
79
89
  * Android `replace` resets the stack to its root, which would drop the screens the
80
90
  * visitor came through. Back from the target shows the form again, and the form sends a
81
- * member back on appear (settle). With no target the form goes back. */
91
+ * member back on appear (settle). With no target the form goes back.
92
+ *
93
+ * A modal form closes. A target pushed from here would open inside the modal (iOS, Android)
94
+ * or under it (web), so a modal that names a `returnTab` hands the target to that tab: the
95
+ * host closes the modal, then pushes it on that tab's stack (Account's member rows). A host
96
+ * showing no such tab (a drawer, a narrow Mac window) drops that request whole, modal and
97
+ * all, so the modal is also dismissed directly: it always closes, and the row opens where
98
+ * the tab exists. With no `returnTab` it only closes, and the screen under it loads again —
99
+ * the screen that opened the modal is the return target. */
82
100
  function resumeAfterAuth(next: S): S {
83
101
  const target = resumeTarget(next);
84
102
  if (!next.isMember) return next;
85
- if (!target) return next.standalone ? navigate(next, { presentation: "back" }) : next;
103
+ const tab = routeInput().modal === "1" ? routeInput().returnTab ?? "" : "";
104
+ if (target && tab && next.standalone) {
105
+ routeInputConsumed = true;
106
+ void dispatch("himi.open", { presentation: "dismiss" });
107
+ const cleared = derive({ ...next, authIntent: "", authContextName: "", returnScreenId: "", returnId: "", returnChannelId: "" });
108
+ return navigate(cleared, target.args ? { tab, screenId: target.screenId, args: target.args } : { tab, screenId: target.screenId });
109
+ }
110
+ if (!target || routeInput().modal === "1") return next.standalone ? navigate(next, { presentation: leaveNav() }) : next;
86
111
  routeInputConsumed = true;
87
112
  const cleared = derive({ ...next, authIntent: "", authContextName: "", returnScreenId: "", returnId: "", returnChannelId: "" });
88
113
  return navigate(cleared, target);
@@ -96,11 +121,31 @@ function forwardToSignIn(next: S): S {
96
121
  if (!next.authIntent && !next.returnScreenId) return next;
97
122
  routeInputConsumed = true;
98
123
  const args: Record<string, string> = { mode: "signin" };
99
- for (const key of ["authIntent", "authContextName", "returnScreenId", "returnId", "returnChannelId"] as const) {
100
- if (next[key]) args[key === "authIntent" ? "intent" : key === "authContextName" ? "contextName" : key] = next[key];
124
+ for (const key of ["authIntent", "authContextName"] as const) {
125
+ if (next[key]) args[key === "authIntent" ? "intent" : "contextName"] = next[key];
126
+ }
127
+ return openSignIn(derive({ ...next, authIntent: "", authContextName: "", returnScreenId: "", returnId: "", returnChannelId: "" }),
128
+ args, resumeTarget(next));
129
+ }
130
+
131
+ /** Account sends a guest to the standalone sign-in screen, with `after`, the member screen to
132
+ * open once signed in. With a `signInPresentation` appear param it opens as that modal, and
133
+ * `after` goes on Account's own tab (`signInReturnTab`) when the modal closes; pushed, the form
134
+ * pushes `after` itself (resumeAfterAuth). */
135
+ function openSignIn(next: S, args: Record<string, string>, after: { screenId: string; args?: Record<string, string> } | null): S {
136
+ const screenId = next.signInScreen || "himi_sign_in";
137
+ const presentation = signInPresentationRequested;
138
+ const back: Record<string, string> = {};
139
+ if (after) {
140
+ back.returnScreenId = after.screenId;
141
+ if (after.args?.id) back.returnId = after.args.id;
142
+ if (after.args?.channelId) back.returnChannelId = after.args.channelId;
101
143
  }
102
- return navigate(derive({ ...next, authIntent: "", authContextName: "", returnScreenId: "", returnId: "", returnChannelId: "" }),
103
- { screenId: next.signInScreen, args });
144
+ if (!presentation) return navigate(next, { screenId, args: { ...args, ...back } });
145
+ const modalArgs: Record<string, string> = { ...args, modal: "1" };
146
+ // With no tab to open it on, a modal has no way to the member screen: it only closes.
147
+ if (after && signInReturnTabRequested) Object.assign(modalArgs, back, { returnTab: signInReturnTabRequested });
148
+ return navigate(next, { screenId, presentation, args: modalArgs });
104
149
  }
105
150
 
106
151
  /** After a load: the standalone sign-in screen sends a member back (a stale link, or a
@@ -128,8 +173,11 @@ function settle(next: S, arg: unknown, latest = true): S {
128
173
  // The appear refresh returns `_nav`, which every host applies — binaries from before
129
174
  // worker nav dispatch included. Only an arg-less load, whose result iOS
130
175
  // discards, dispatches.
131
- if (strField(arg, "standalone") === "1") return navigate(cleared, { presentation: "back" });
132
- void dispatch("himi.open", { presentation: "back" });
176
+ if (strField(arg, "standalone") === "1") return navigate(cleared, { presentation: leaveNav() });
177
+ // Not from a modal: nothing pushed over it comes back to it, and on iOS the closing modal's
178
+ // own screen reloads here once, after it has asked to close — a late dismiss would close
179
+ // whatever modal is open by then.
180
+ if (routeInput().modal !== "1") void dispatch("himi.open", { presentation: leaveNav() });
133
181
  return cleared;
134
182
  }
135
183
  // Account forwards only on an appear refresh: the boot `load()` has no params and its
@@ -180,6 +228,10 @@ let appliedSeq = 0;
180
228
  // The appear params belong to the worker session too, like the two flags above: a probe
181
229
  // dropped because the visitor typed must not take them with it.
182
230
  let signInScreenRequested = "";
231
+ /** Account only: how it opens the standalone sign-in screen ("" = push). */
232
+ let signInPresentationRequested: "" | "sheet" | "full_screen" = "";
233
+ /** Account only: its tab, where a member screen opens after a modal sign-in. */
234
+ let signInReturnTabRequested = "";
183
235
  let standaloneRequested = false;
184
236
  let pollIdentityRequested = false;
185
237
  // Bumped by every mode switch, so a dropped probe never puts back a mode the visitor left.
@@ -210,6 +262,9 @@ function appearConfig(s: S, arg: unknown): AppearConfig {
210
262
  passwordDraft = "";
211
263
  }
212
264
  signInScreenRequested ||= strField(arg, "signInScreen") || s.signInScreen;
265
+ const presentation = strField(arg, "signInPresentation");
266
+ if (presentation === "sheet" || presentation === "full_screen") signInPresentationRequested = presentation;
267
+ signInReturnTabRequested ||= strField(arg, "signInReturnTab");
213
268
  // Only the sign-in screen's route carries `mode`, so it marks the form from the boot load on.
214
269
  standaloneRequested ||= s.standalone || appearing || (!routeInputConsumed && !!routeInput().mode);
215
270
  pollIdentityRequested ||= s.pollIdentity || strField(arg, "pollIdentity") === "1";
@@ -676,6 +731,7 @@ const svc = defineService<S, {
676
731
  signOut: (s: S) => Promise<S>;
677
732
  sync: (s: S) => Promise<S | undefined>;
678
733
  openMember: (s: S, arg?: unknown) => Promise<S>;
734
+ leave: (s: S) => S;
679
735
  restorePublish: (s: S) => S;
680
736
  iapRestore: (s: S) => Promise<undefined>;
681
737
  }>("himiStoreAccount", {
@@ -756,7 +812,8 @@ const svc = defineService<S, {
756
812
  return "next" in r ? r.next : undefined;
757
813
  },
758
814
  // A row to a member-only screen shows to a guest too. A member opens it; a guest signs
759
- // in first and the sign-in screen replaces itself with it, so Back returns here.
815
+ // in first, and the sign-in screen then opens the row's screen — as a modal, by closing
816
+ // and pushing it on this tab (openSignIn) — so Back returns here either way.
760
817
  openMember: async (s, arg): Promise<S> => {
761
818
  const screenId = strField(arg, "screenId");
762
819
  if (!screenId) return s;
@@ -764,8 +821,10 @@ const svc = defineService<S, {
764
821
  // From the state as it is now: a probe may have landed during the identity read.
765
822
  const now = svc.getState();
766
823
  if (member) return navigate(now, { screenId });
767
- return navigate(now, { screenId: now.signInScreen || "himi_sign_in", args: { mode: "signin", returnScreenId: screenId } });
824
+ return openSignIn(now, { mode: "signin" }, { screenId });
768
825
  },
826
+ // The standalone form's Continue: leaves the form the way it was opened.
827
+ leave: (s) => navigate(s, { presentation: leaveNav() }),
769
828
  // Typing means any focus request was delivered: clear the tokens (safe while mounted, since
770
829
  // bumpToken never repeats), so a visit rarely ends — and a pooled form rarely reopens —
771
830
  // holding one.