@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
@@ -2707,8 +2707,10 @@ const SCREENS = runtimeSetting({
2707
2707
  "actionId": "himi.open",
2708
2708
  "params": {
2709
2709
  "screenId": "himi_sign_in",
2710
- "mode": "signin"
2711
- }
2710
+ "mode": "signin",
2711
+ "modal": "1"
2712
+ },
2713
+ "presentation": "sheet"
2712
2714
  }
2713
2715
  }
2714
2716
  }
@@ -2726,8 +2728,10 @@ const SCREENS = runtimeSetting({
2726
2728
  "actionId": "himi.open",
2727
2729
  "params": {
2728
2730
  "screenId": "himi_sign_in",
2729
- "mode": "register"
2730
- }
2731
+ "mode": "register",
2732
+ "modal": "1"
2733
+ },
2734
+ "presentation": "sheet"
2731
2735
  }
2732
2736
  }
2733
2737
  }
@@ -4091,6 +4095,8 @@ const SCREENS = runtimeSetting({
4091
4095
  "memberAccountLinks": "1",
4092
4096
  "restorePurchases": "1",
4093
4097
  "signInScreen": "himi_sign_in",
4098
+ "signInPresentation": "sheet",
4099
+ "signInReturnTab": "profile",
4094
4100
  "pollIdentity": "1",
4095
4101
  "cartCount": "1",
4096
4102
  "copy": "{\"account.subtitle\":\"Orders, bookings, RSVPs, memberships and saved content in one place.\",\"storeAccount.authBody\":\"Sign in to continue — cart, bookings, RSVPs and memberships stay with you.\",\"storeAccount.authBodyRegister\":\"Create a member account to continue — cart, bookings, RSVPs and memberships stay with you.\"}"
@@ -4366,8 +4372,10 @@ const SCREENS = runtimeSetting({
4366
4372
  "params": {
4367
4373
  "screenId": "himi_sign_in",
4368
4374
  "mode": "signin",
4369
- "returnScreenId": "himi_account_settings"
4370
- }
4375
+ "returnScreenId": "himi_account_settings",
4376
+ "modal": "1"
4377
+ },
4378
+ "presentation": "sheet"
4371
4379
  }
4372
4380
  }
4373
4381
  },
@@ -6277,8 +6285,7 @@ const SCREENS = runtimeSetting({
6277
6285
  "style": "primary",
6278
6286
  "fullWidth": true,
6279
6287
  "action": {
6280
- "actionId": "nav.back",
6281
- "presentation": "back"
6288
+ "actionId": "tier5.himiStoreAccount.leave"
6282
6289
  }
6283
6290
  }
6284
6291
  }
@@ -6319,7 +6326,7 @@ const SCREENS = runtimeSetting({
6319
6326
  "declaredCapabilities": [
6320
6327
  "wix_api"
6321
6328
  ],
6322
- "minBinaryVersion": "0.2.0",
6329
+ "minBinaryVersion": "1.7.0",
6323
6330
  "bundleVersion": "0.0.1"
6324
6331
  }
6325
6332
  },
@@ -7186,8 +7193,10 @@ const SCREENS = runtimeSetting({
7186
7193
  "screenId": "himi_sign_in",
7187
7194
  "mode": "signin",
7188
7195
  "intent": "profile",
7189
- "returnScreenId": "himi_view_profile"
7190
- }
7196
+ "returnScreenId": "himi_view_profile",
7197
+ "modal": "1"
7198
+ },
7199
+ "presentation": "sheet"
7191
7200
  }
7192
7201
  }
7193
7202
  }
@@ -16013,8 +16022,10 @@ const SCREENS = runtimeSetting({
16013
16022
  "screenId": "himi_sign_in",
16014
16023
  "mode": "signin",
16015
16024
  "intent": "orders",
16016
- "returnScreenId": "himi_orders"
16017
- }
16025
+ "returnScreenId": "himi_orders",
16026
+ "modal": "1"
16027
+ },
16028
+ "presentation": "sheet"
16018
16029
  }
16019
16030
  }
16020
16031
  }
@@ -16647,8 +16658,10 @@ const SCREENS = runtimeSetting({
16647
16658
  "screenId": "himi_sign_in",
16648
16659
  "mode": "signin",
16649
16660
  "intent": "addresses",
16650
- "returnScreenId": "himi_addresses"
16651
- }
16661
+ "returnScreenId": "himi_addresses",
16662
+ "modal": "1"
16663
+ },
16664
+ "presentation": "sheet"
16652
16665
  }
16653
16666
  }
16654
16667
  }
@@ -17704,8 +17717,10 @@ const SCREENS = runtimeSetting({
17704
17717
  "screenId": "himi_sign_in",
17705
17718
  "mode": "signin",
17706
17719
  "intent": "addresses",
17707
- "returnScreenId": "himi_address"
17708
- }
17720
+ "returnScreenId": "himi_address",
17721
+ "modal": "1"
17722
+ },
17723
+ "presentation": "sheet"
17709
17724
  }
17710
17725
  }
17711
17726
  }
@@ -18375,8 +18390,10 @@ const SCREENS = runtimeSetting({
18375
18390
  "screenId": "himi_sign_in",
18376
18391
  "mode": "signin",
18377
18392
  "intent": "wallet",
18378
- "returnScreenId": "himi_wallet"
18379
- }
18393
+ "returnScreenId": "himi_wallet",
18394
+ "modal": "1"
18395
+ },
18396
+ "presentation": "sheet"
18380
18397
  }
18381
18398
  }
18382
18399
  }
@@ -18844,11 +18861,7 @@ const SCREENS = runtimeSetting({
18844
18861
  "size": "sm",
18845
18862
  "fullWidth": true,
18846
18863
  "action": {
18847
- "actionId": "himi.open",
18848
- "params": {
18849
- "screenId": "$service:himiPlaceSummary.ctaScreenId",
18850
- "mode": "$service:himiPlaceSummary.ctaMode"
18851
- }
18864
+ "actionId": "tier5.himiPlaceSummary.openCta"
18852
18865
  }
18853
18866
  }
18854
18867
  },
@@ -18889,7 +18902,8 @@ const SCREENS = runtimeSetting({
18889
18902
  "onAppear": {
18890
18903
  "actionId": "tier5.himiPlaceSummary.load",
18891
18904
  "params": {
18892
- "verticals": "[\"store\",\"bookings\",\"plans\",\"events\",\"blog\"]"
18905
+ "verticals": "[\"store\",\"bookings\",\"plans\",\"events\",\"blog\"]",
18906
+ "signInPresentation": "sheet"
18893
18907
  }
18894
18908
  }
18895
18909
  }
@@ -22162,8 +22176,10 @@ const SCREENS = runtimeSetting({
22162
22176
  "params": {
22163
22177
  "screenId": "himi_sign_in",
22164
22178
  "mode": "signin",
22165
- "returnScreenId": "himi_bookings"
22166
- }
22179
+ "returnScreenId": "himi_bookings",
22180
+ "modal": "1"
22181
+ },
22182
+ "presentation": "sheet"
22167
22183
  }
22168
22184
  }
22169
22185
  }
@@ -25550,7 +25566,8 @@ const SCREENS = runtimeSetting({
25550
25566
  "onAppear": {
25551
25567
  "actionId": "tier5.himiBrandedAgenda.load",
25552
25568
  "params": {
25553
- "firstWeekday": "0"
25569
+ "firstWeekday": "0",
25570
+ "signInPresentation": "sheet"
25554
25571
  }
25555
25572
  }
25556
25573
  }
@@ -28607,8 +28624,10 @@ const SCREENS = runtimeSetting({
28607
28624
  "actionId": "himi.open",
28608
28625
  "params": {
28609
28626
  "screenId": "himi_sign_in",
28610
- "mode": "signin"
28611
- }
28627
+ "mode": "signin",
28628
+ "modal": "1"
28629
+ },
28630
+ "presentation": "sheet"
28612
28631
  }
28613
28632
  }
28614
28633
  },
@@ -28993,8 +29012,10 @@ const SCREENS = runtimeSetting({
28993
29012
  "screenId": "himi_sign_in",
28994
29013
  "mode": "signin",
28995
29014
  "intent": "post",
28996
- "returnScreenId": "himi_create_post"
28997
- }
29015
+ "returnScreenId": "himi_create_post",
29016
+ "modal": "1"
29017
+ },
29018
+ "presentation": "sheet"
28998
29019
  }
28999
29020
  }
29000
29021
  }
@@ -37579,8 +37600,10 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
37579
37600
  "params": {
37580
37601
  "screenId": "himi_sign_in",
37581
37602
  "returnScreenId": "himi_order",
37582
- "returnId": "$service:himiBrandedOrder.id"
37583
- }
37603
+ "returnId": "$service:himiBrandedOrder.id",
37604
+ "modal": "1"
37605
+ },
37606
+ "presentation": "sheet"
37584
37607
  }
37585
37608
  }
37586
37609
  },
@@ -43007,8 +43030,10 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
43007
43030
  "params": {
43008
43031
  "screenId": "himi_sign_in",
43009
43032
  "mode": "signin",
43010
- "intent": "book"
43011
- }
43033
+ "intent": "book",
43034
+ "modal": "1"
43035
+ },
43036
+ "presentation": "sheet"
43012
43037
  }
43013
43038
  }
43014
43039
  },
@@ -43033,8 +43058,10 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
43033
43058
  "params": {
43034
43059
  "screenId": "himi_sign_in",
43035
43060
  "mode": "signin",
43036
- "intent": "book"
43037
- }
43061
+ "intent": "book",
43062
+ "modal": "1"
43063
+ },
43064
+ "presentation": "sheet"
43038
43065
  }
43039
43066
  }
43040
43067
  },
@@ -43059,8 +43086,10 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
43059
43086
  "params": {
43060
43087
  "screenId": "himi_sign_in",
43061
43088
  "mode": "signin",
43062
- "intent": "book"
43063
- }
43089
+ "intent": "book",
43090
+ "modal": "1"
43091
+ },
43092
+ "presentation": "sheet"
43064
43093
  }
43065
43094
  }
43066
43095
  },
@@ -45076,8 +45105,10 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
45076
45105
  "params": {
45077
45106
  "screenId": "himi_sign_in",
45078
45107
  "mode": "signin",
45079
- "intent": "rsvp"
45080
- }
45108
+ "intent": "rsvp",
45109
+ "modal": "1"
45110
+ },
45111
+ "presentation": "sheet"
45081
45112
  }
45082
45113
  }
45083
45114
  },
@@ -598,6 +598,9 @@ function initial(): S {
598
598
  };
599
599
  }
600
600
 
601
+ /** How the app opens its sign-in screen, from the appear params ("" = push). */
602
+ let signInPresentation: "" | "sheet" | "full_screen" = "";
603
+
601
604
  const svc = defineService<S, {
602
605
  load: (s: S, p?: unknown) => Promise<S | undefined>;
603
606
  retry: (s: S) => Promise<S | undefined>;
@@ -614,6 +617,8 @@ const svc = defineService<S, {
614
617
  actions: {
615
618
  load: async (s, p) => {
616
619
  stashCopy(p);
620
+ const presentation = argOf(p, "signInPresentation");
621
+ if (presentation === "sheet" || presentation === "full_screen") signInPresentation = presentation;
617
622
  const fw = Number(argOf(p, "firstWeekday") || s.firstWeekday);
618
623
  const today = dayKey(new Date());
619
624
  // First appearance: open on this week. A re-appearance keeps the member's place and
@@ -739,7 +744,10 @@ const svc = defineService<S, {
739
744
  const before = await memberAccess().catch(() => undefined);
740
745
  if (!before?.email) {
741
746
  busy = false;
742
- return navigate(s, { screenId: "himi_sign_in", args: { mode: "signin", returnScreenId: "himi_agenda" } });
747
+ // As a modal it only closes, and this screen loads again (its re-appearance reload).
748
+ return navigate(s, signInPresentation
749
+ ? { screenId: "himi_sign_in", presentation: signInPresentation, args: { mode: "signin", modal: "1" } }
750
+ : { screenId: "himi_sign_in", args: { mode: "signin", returnScreenId: "himi_agenda" } });
743
751
  }
744
752
  const name = await memberName();
745
753
  const current = await memberAccess().catch(() => undefined);
@@ -9,7 +9,7 @@
9
9
  // `load` is both the onAppear action and what an embed controller's `refresh()` runs (arg-less:
10
10
  // the stashed appear params keep the verticals and the copy pack), so a host that hears
11
11
  // `booking.created` / `booking.cancelled` re-reads the summary without a remount.
12
- import { defineService } from "@himalaya/state";
12
+ import { defineService, navigate } from "@himalaya/state";
13
13
  import { listMyPlanOrders, type WixPlanOrder } from "@himalaya/wix-pricing-plans";
14
14
  import { fetchMemberBookings, type MemberBookingRow } from "../_shared/member-bookings.js";
15
15
  import { memberAccess, assertMemberAccess, memberAccessDenied, type MemberAccess } from "../_shared/member-access.js";
@@ -52,6 +52,7 @@ const PLANS_SCREEN = "himi_plans";
52
52
  const SIGN_IN_SCREEN = "himi_sign_in";
53
53
 
54
54
  let stashedVerticals: string[] | null = null;
55
+ let signInPresentation: "sheet" | "full_screen" | undefined;
55
56
  let generation = 0;
56
57
 
57
58
  function appearVerticals(arg: unknown): string[] {
@@ -190,6 +191,8 @@ async function read(arg: unknown, again = true): Promise<PlaceSummaryState> {
190
191
  * write back over the newer result. */
191
192
  async function load(_s: PlaceSummaryState, arg?: unknown): Promise<PlaceSummaryState | undefined> {
192
193
  stashCopy(arg);
194
+ const presentation = arg && typeof arg === "object" ? (arg as Record<string, unknown>).signInPresentation : undefined;
195
+ if (presentation === "sheet" || presentation === "full_screen") signInPresentation = presentation;
193
196
  const mine = ++generation;
194
197
  const next = await read(arg);
195
198
  return mine === generation ? next : undefined;
@@ -198,11 +201,19 @@ async function load(_s: PlaceSummaryState, arg?: unknown): Promise<PlaceSummaryS
198
201
  const svc = defineService<PlaceSummaryState, {
199
202
  load: (s: PlaceSummaryState, arg?: unknown) => Promise<PlaceSummaryState | undefined>;
200
203
  retry: (s: PlaceSummaryState) => Promise<PlaceSummaryState | undefined>;
204
+ openCta: (s: PlaceSummaryState) => PlaceSummaryState;
201
205
  }>("himiPlaceSummary", {
202
206
  state: BLANK,
203
207
  actions: {
204
208
  load,
205
209
  retry: (s) => load(s),
210
+ openCta: (s) => {
211
+ if (s.hideCta || !s.ctaScreenId) return s;
212
+ // No return path: the tile is embedded, so the form closes over its host.
213
+ return navigate(s, s.ctaScreenId === SIGN_IN_SCREEN && signInPresentation
214
+ ? { screenId: SIGN_IN_SCREEN, presentation: signInPresentation, args: { mode: "signin", modal: "1" } }
215
+ : { screenId: s.ctaScreenId, args: { mode: s.ctaMode } });
216
+ },
206
217
  },
207
218
  });
208
219
  export { svc };
@@ -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.