@wix/himalaya-cli 0.1567.0 → 0.1569.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 (168) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/l10n/src/index.d.ts +9 -0
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/message-format.d.ts +28 -0
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/online-programs/src/index.d.ts +7 -0
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/l10n.mjs +191 -0
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-online-programs.mjs +14 -1
  6. package/dist/authoring-runtime/node_modules/@wix/himalaya/package.json +4 -0
  7. package/dist/cli.mjs +20994 -20822
  8. package/dist/preview-assets.generated.bin +0 -0
  9. package/guides/app-config.md +10 -0
  10. package/guides/app-icon.md +20 -1
  11. package/guides/himi-json-output.md +7 -0
  12. package/guides/importable-sdks.md +1 -0
  13. package/guides/one-app-to-himi-app.md +14 -0
  14. package/guides/release-notes.md +6 -0
  15. package/guides/sdk-reference.json +41 -2
  16. package/guides/splash-screens.md +156 -5
  17. package/package.json +2 -2
  18. package/templates/beauty-salon/config.ts +12 -1
  19. package/templates/beauty-salon/dev/index.ts +265 -0
  20. package/templates/beauty-salon/himalaya.content.json +3 -0
  21. package/templates/beauty-salon/splash.json +3 -0
  22. package/templates/beauty-salon/tier5-src/_shared/himi-copy.ts +17 -6
  23. package/templates/beauty-salon/tier5-src/_shared/rich-body.ts +44 -0
  24. package/templates/beauty-salon/tier5-src/himi-booking-service/index.ts +13 -9
  25. package/templates/beauty-salon/tier5-src/himi-branded-lite-splash/index.ts +35 -0
  26. package/templates/beauty-salon/tier5-src/himi-store-account/index.ts +4 -0
  27. package/templates/beauty-salon-owner/config.ts +8 -0
  28. package/templates/beauty-salon-owner/dev/index.ts +1 -0
  29. package/templates/beauty-salon-owner/tier5-src/_shared/himi-copy.ts +17 -6
  30. package/templates/beauty-salon-owner/tier5-src/_shared/rich-body.ts +44 -0
  31. package/templates/branded-lite/config.ts +12 -1
  32. package/templates/branded-lite/dev/index.ts +299 -2
  33. package/templates/branded-lite/himalaya.content.json +3 -0
  34. package/templates/branded-lite/splash.json +3 -0
  35. package/templates/branded-lite/tier5-src/_shared/himi-copy.ts +17 -6
  36. package/templates/branded-lite/tier5-src/_shared/rich-body.ts +44 -0
  37. package/templates/branded-lite/tier5-src/himi-booking-service/index.ts +13 -9
  38. package/templates/branded-lite/tier5-src/himi-branded-lite-group/index.ts +20 -3
  39. package/templates/branded-lite/tier5-src/himi-branded-lite-groups/index.ts +18 -2
  40. package/templates/branded-lite/tier5-src/himi-branded-lite-notifsettings/index.ts +4 -2
  41. package/templates/branded-lite/tier5-src/himi-branded-lite-program/index.ts +124 -15
  42. package/templates/branded-lite/tier5-src/himi-branded-lite-splash/index.ts +35 -0
  43. package/templates/branded-lite/tier5-src/himi-store-account/index.ts +4 -0
  44. package/templates/brandy/tier5-src/_shared/himi-copy.ts +17 -6
  45. package/templates/community-club/config.ts +12 -1
  46. package/templates/community-club/dev/index.ts +267 -1
  47. package/templates/community-club/himalaya.content.json +3 -0
  48. package/templates/community-club/splash.json +3 -0
  49. package/templates/community-club/tier5-src/_shared/himi-copy.ts +17 -6
  50. package/templates/community-club/tier5-src/_shared/rich-body.ts +44 -0
  51. package/templates/community-club/tier5-src/himi-branded-lite-group/index.ts +20 -3
  52. package/templates/community-club/tier5-src/himi-branded-lite-groups/index.ts +18 -2
  53. package/templates/community-club/tier5-src/himi-branded-lite-splash/index.ts +35 -0
  54. package/templates/community-club/tier5-src/himi-store-account/index.ts +4 -0
  55. package/templates/community-club-owner/config.ts +8 -0
  56. package/templates/community-club-owner/dev/index.ts +1 -0
  57. package/templates/community-club-owner/tier5-src/_shared/himi-copy.ts +17 -6
  58. package/templates/community-club-owner/tier5-src/_shared/rich-body.ts +44 -0
  59. package/templates/events-meetups/config.ts +12 -1
  60. package/templates/events-meetups/dev/index.ts +267 -1
  61. package/templates/events-meetups/himalaya.content.json +3 -0
  62. package/templates/events-meetups/splash.json +3 -0
  63. package/templates/events-meetups/tier5-src/_shared/himi-copy.ts +17 -6
  64. package/templates/events-meetups/tier5-src/_shared/rich-body.ts +44 -0
  65. package/templates/events-meetups/tier5-src/himi-branded-lite-group/index.ts +20 -3
  66. package/templates/events-meetups/tier5-src/himi-branded-lite-groups/index.ts +18 -2
  67. package/templates/events-meetups/tier5-src/himi-branded-lite-splash/index.ts +35 -0
  68. package/templates/events-meetups/tier5-src/himi-store-account/index.ts +4 -0
  69. package/templates/events-meetups-owner/config.ts +8 -0
  70. package/templates/events-meetups-owner/dev/index.ts +1 -0
  71. package/templates/events-meetups-owner/tier5-src/_shared/himi-copy.ts +17 -6
  72. package/templates/events-meetups-owner/tier5-src/_shared/rich-body.ts +44 -0
  73. package/templates/gym-fitness/config.ts +12 -1
  74. package/templates/gym-fitness/dev/index.ts +265 -0
  75. package/templates/gym-fitness/himalaya.content.json +3 -0
  76. package/templates/gym-fitness/splash.json +3 -0
  77. package/templates/gym-fitness/tier5-src/_shared/himi-copy.ts +17 -6
  78. package/templates/gym-fitness/tier5-src/_shared/rich-body.ts +44 -0
  79. package/templates/gym-fitness/tier5-src/himi-booking-service/index.ts +13 -9
  80. package/templates/gym-fitness/tier5-src/himi-branded-lite-splash/index.ts +35 -0
  81. package/templates/gym-fitness/tier5-src/himi-store-account/index.ts +4 -0
  82. package/templates/gym-fitness-owner/config.ts +8 -0
  83. package/templates/gym-fitness-owner/dev/index.ts +1 -0
  84. package/templates/gym-fitness-owner/tier5-src/_shared/himi-copy.ts +17 -6
  85. package/templates/gym-fitness-owner/tier5-src/_shared/rich-body.ts +44 -0
  86. package/templates/himi-chat/dev/index.ts +1 -0
  87. package/templates/himi-chat/tier5-src/_shared/himi-copy.ts +17 -6
  88. package/templates/nonprofit-cause/config.ts +12 -1
  89. package/templates/nonprofit-cause/dev/index.ts +267 -1
  90. package/templates/nonprofit-cause/himalaya.content.json +3 -0
  91. package/templates/nonprofit-cause/splash.json +3 -0
  92. package/templates/nonprofit-cause/tier5-src/_shared/himi-copy.ts +17 -6
  93. package/templates/nonprofit-cause/tier5-src/_shared/rich-body.ts +44 -0
  94. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-group/index.ts +20 -3
  95. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-groups/index.ts +18 -2
  96. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-splash/index.ts +35 -0
  97. package/templates/nonprofit-cause/tier5-src/himi-store-account/index.ts +4 -0
  98. package/templates/nonprofit-cause-owner/config.ts +8 -0
  99. package/templates/nonprofit-cause-owner/dev/index.ts +1 -0
  100. package/templates/nonprofit-cause-owner/tier5-src/_shared/himi-copy.ts +17 -6
  101. package/templates/nonprofit-cause-owner/tier5-src/_shared/rich-body.ts +44 -0
  102. package/templates/owner-lite/config.ts +8 -0
  103. package/templates/owner-lite/dev/index.ts +1 -0
  104. package/templates/owner-lite/tier5-src/_shared/himi-copy.ts +17 -6
  105. package/templates/owner-lite/tier5-src/_shared/rich-body.ts +44 -0
  106. package/templates/podcast-creator/config.ts +12 -1
  107. package/templates/podcast-creator/dev/index.ts +237 -0
  108. package/templates/podcast-creator/himalaya.content.json +3 -0
  109. package/templates/podcast-creator/splash.json +3 -0
  110. package/templates/podcast-creator/tier5-src/_shared/himi-copy.ts +17 -6
  111. package/templates/podcast-creator/tier5-src/_shared/rich-body.ts +44 -0
  112. package/templates/podcast-creator/tier5-src/himi-branded-lite-splash/index.ts +35 -0
  113. package/templates/podcast-creator/tier5-src/himi-store-account/index.ts +4 -0
  114. package/templates/podcast-creator-owner/config.ts +8 -0
  115. package/templates/podcast-creator-owner/dev/index.ts +1 -0
  116. package/templates/podcast-creator-owner/tier5-src/_shared/himi-copy.ts +17 -6
  117. package/templates/podcast-creator-owner/tier5-src/_shared/rich-body.ts +44 -0
  118. package/templates/portfolio-creative/config.ts +12 -1
  119. package/templates/portfolio-creative/dev/index.ts +237 -0
  120. package/templates/portfolio-creative/himalaya.content.json +3 -0
  121. package/templates/portfolio-creative/splash.json +3 -0
  122. package/templates/portfolio-creative/tier5-src/_shared/himi-copy.ts +17 -6
  123. package/templates/portfolio-creative/tier5-src/_shared/rich-body.ts +44 -0
  124. package/templates/portfolio-creative/tier5-src/himi-branded-lite-splash/index.ts +35 -0
  125. package/templates/portfolio-creative/tier5-src/himi-store-account/index.ts +4 -0
  126. package/templates/portfolio-creative-owner/config.ts +8 -0
  127. package/templates/portfolio-creative-owner/dev/index.ts +1 -0
  128. package/templates/portfolio-creative-owner/tier5-src/_shared/himi-copy.ts +17 -6
  129. package/templates/portfolio-creative-owner/tier5-src/_shared/rich-body.ts +44 -0
  130. package/templates/restaurant/config.ts +12 -1
  131. package/templates/restaurant/dev/index.ts +237 -0
  132. package/templates/restaurant/himalaya.content.json +3 -0
  133. package/templates/restaurant/splash.json +3 -0
  134. package/templates/restaurant/tier5-src/_shared/himi-copy.ts +17 -6
  135. package/templates/restaurant/tier5-src/_shared/rich-body.ts +44 -0
  136. package/templates/restaurant/tier5-src/himi-branded-lite-splash/index.ts +35 -0
  137. package/templates/restaurant/tier5-src/himi-store-account/index.ts +4 -0
  138. package/templates/restaurant-owner/config.ts +8 -0
  139. package/templates/restaurant-owner/dev/index.ts +1 -0
  140. package/templates/restaurant-owner/tier5-src/_shared/himi-copy.ts +17 -6
  141. package/templates/restaurant-owner/tier5-src/_shared/rich-body.ts +44 -0
  142. package/templates/retail-store/config.ts +12 -1
  143. package/templates/retail-store/dev/index.ts +237 -0
  144. package/templates/retail-store/himalaya.content.json +3 -0
  145. package/templates/retail-store/splash.json +3 -0
  146. package/templates/retail-store/tier5-src/_shared/himi-copy.ts +17 -6
  147. package/templates/retail-store/tier5-src/_shared/rich-body.ts +44 -0
  148. package/templates/retail-store/tier5-src/himi-branded-lite-splash/index.ts +35 -0
  149. package/templates/retail-store/tier5-src/himi-store-account/index.ts +4 -0
  150. package/templates/retail-store-owner/config.ts +8 -0
  151. package/templates/retail-store-owner/dev/index.ts +1 -0
  152. package/templates/retail-store-owner/tier5-src/_shared/himi-copy.ts +17 -6
  153. package/templates/retail-store-owner/tier5-src/_shared/rich-body.ts +44 -0
  154. package/templates/surface-showcase/dev/index.ts +1 -0
  155. package/templates/yoga-studio/config.ts +12 -1
  156. package/templates/yoga-studio/dev/index.ts +269 -1
  157. package/templates/yoga-studio/himalaya.content.json +3 -0
  158. package/templates/yoga-studio/splash.json +3 -0
  159. package/templates/yoga-studio/tier5-src/_shared/himi-copy.ts +17 -6
  160. package/templates/yoga-studio/tier5-src/_shared/rich-body.ts +44 -0
  161. package/templates/yoga-studio/tier5-src/himi-booking-service/index.ts +13 -9
  162. package/templates/yoga-studio/tier5-src/himi-branded-lite-program/index.ts +124 -15
  163. package/templates/yoga-studio/tier5-src/himi-branded-lite-splash/index.ts +35 -0
  164. package/templates/yoga-studio/tier5-src/himi-store-account/index.ts +4 -0
  165. package/templates/yoga-studio-owner/config.ts +8 -0
  166. package/templates/yoga-studio-owner/dev/index.ts +1 -0
  167. package/templates/yoga-studio-owner/tier5-src/_shared/himi-copy.ts +17 -6
  168. package/templates/yoga-studio-owner/tier5-src/_shared/rich-body.ts +44 -0
@@ -1,4 +1,6 @@
1
- import { withOnlineWrites } from "../_shared/online-actions.js";
1
+ import { dispatch } from "@himalaya/dispatch";
2
+ import { memberAccess, assertMemberAccess } from "../_shared/member-access.js";
3
+ import { offlineWriteMessage, withOnlineWrites } from "../_shared/online-actions.js";
2
4
  // Tier-5 bundle: himi-branded-lite — single online-program detail (design §3.5
3
5
  // `programs` vertical, screen full-potential pass).
4
6
  //
@@ -20,7 +22,7 @@ import { withOnlineWrites } from "../_shared/online-actions.js";
20
22
  // reads every program as `disabled` and keeps Enrol on its own commerce. An Apple-routed program
21
23
  // offers Get access in place of Enrol and sells only through the shared checkout, never through the
22
24
  // Wix payment rail.
23
- import { defineService as registerService } from "@himalaya/state";
25
+ import { connectivity, defineService as registerService } from "@himalaya/state";
24
26
  import { programImage } from "../_shared/himi-media.js";
25
27
  import { WIX_DETAIL_IMAGE } from "@himalaya/wix-media";
26
28
  import {
@@ -28,10 +30,12 @@ import {
28
30
  joinProgram,
29
31
  payableIdOf,
30
32
  queryPrograms,
33
+ searchMemberParticipation,
31
34
  type WixProgram,
32
35
  type WixProgramFull,
33
36
  } from "@himalaya/wix-online-programs";
34
37
  import { createRedirectSession } from "@himalaya/wix-redirects";
38
+ import { getMyMember } from "@himalaya/wix-members";
35
39
  import { session as wixPlatformSession } from "@himalaya/wix-platform";
36
40
  import { cleanTitle, initials } from "../_shared/himi-text.js";
37
41
  import { copyParam, copyTemplate, stashCopy } from "../_shared/himi-copy.js";
@@ -183,16 +187,17 @@ async function fetchProgram(id: string): Promise<WixProgram | undefined> {
183
187
  /** Whether the existing commerce flow may start (before the join) and continue (before the Wix
184
188
  * payment): a price refresh can move the program onto a route this flow must not charge. */
185
189
  interface CommerceGuards {
190
+ assertMember(): Promise<void>;
186
191
  mayStart(): boolean;
187
192
  mayContinue(): boolean;
188
193
  }
189
194
 
190
- async function enrollmentMessage(name: string, guards: CommerceGuards): Promise<string> {
195
+ async function enrollmentMessage(id: string, name: string, guards: CommerceGuards): Promise<string> {
191
196
  const blocked = () => iapPriceCopy("program").purchaseUnavailable;
192
197
  if (!guards.mayStart()) return blocked();
193
198
  let preparationWithdrawn = false;
194
199
  try {
195
- const joined = await joinProgram(inputId(), { mayPreparePayment: () => {
200
+ const joined = await joinProgram(id, { beforeWrite: guards.assertMember, mayPreparePayment: () => {
196
201
  if (!guards.mayContinue()) preparationWithdrawn = true;
197
202
  return !preparationWithdrawn;
198
203
  } });
@@ -218,6 +223,7 @@ async function enrollmentMessage(name: string, guards: CommerceGuards): Promise<
218
223
  return !withdrawn;
219
224
  };
220
225
  if (!stillAllowed()) return blocked();
226
+ await guards.assertMember();
221
227
  const pay = await payCashierOrder({
222
228
  appId: WIX_PROGRAMS_APP_ID,
223
229
  snapshotId: payable,
@@ -232,6 +238,7 @@ async function enrollmentMessage(name: string, guards: CommerceGuards): Promise<
232
238
  // Join Applications returns a Wix Payments order id; the payment-request
233
239
  // redirect uses that same order handle on the hosted rail.
234
240
  cashierUrl: async () => {
241
+ await guards.assertMember();
235
242
  if (!stillAllowed()) return undefined;
236
243
  const redirect = await createRedirectSession({
237
244
  ecomOrderPaymentRequest: { paymentRequestId: payable },
@@ -247,6 +254,7 @@ async function enrollmentMessage(name: string, guards: CommerceGuards): Promise<
247
254
  if (pay.kind === "handoff") return "";
248
255
  if (pay.kind === "cancelled") return copyParam("program.joinCancelledMessage", "Payment cancelled — you haven't enrolled.");
249
256
  } catch {
257
+ if (!connectivity().online) return offlineWriteMessage;
250
258
  if (preparationWithdrawn) return blocked();
251
259
  // fall through to the site-directing message
252
260
  }
@@ -274,6 +282,17 @@ let pricedItem: IapCatalogItem | undefined;
274
282
  let actionMessage = "";
275
283
  let loadSeq = 0;
276
284
  let commerceStarted = false;
285
+ let enrollmentInFlight = false;
286
+ // Presentation dedupe only. The host remains the sole authority to resume a join.
287
+ let openedAuthToken = "";
288
+ let signInPresentation: "sheet" | "full_screen" = "sheet";
289
+ const continuationTarget = (id: string) => `program_enrol:${id}`;
290
+
291
+ async function continuation(operation: "begin" | "check" | "consume" | "cancel", id: string, token = "") {
292
+ try {
293
+ return await dispatch("himi.wix.authContinuation", { operation, target: continuationTarget(id), token }) as { supported?: boolean; confirmed?: boolean; account?: string; token?: string } | null;
294
+ } catch { return { supported: false }; } // Older binaries do not implement this host contract: fail closed.
295
+ }
277
296
 
278
297
  function contentOf(state: State): Content {
279
298
  return Object.fromEntries(CONTENT_KEYS.map((key) => [key, state[key]])) as Content;
@@ -283,13 +302,31 @@ const programReference = () => onlineProgramItem({ id: inputId() })?.reference;
283
302
 
284
303
  async function load(state: State, arg?: unknown): Promise<undefined> {
285
304
  stashCopy(arg);
305
+ const presentation = (arg as { signInPresentation?: unknown } | undefined)?.signInPresentation;
306
+ if (presentation === "sheet" || presentation === "full_screen") signInPresentation = presentation;
307
+ const id = inputId();
286
308
  const seq = ++loadSeq;
287
309
  const found = await fetchProgram(inputId()).then((program) => ({ program }), () => undefined);
288
310
  if (seq !== loadSeq) return undefined;
289
311
  content = found ? contentOf(ready(state, found.program)) : { ...content, status: "error" };
290
312
  pricedItem = found?.program ? onlineProgramItem({ id: inputId() }, priceOf(found.program)) : pricedItem;
291
313
  void svc.pricingPublish();
292
- if (found?.program) void priceProgram();
314
+ const prices = found?.program ? priceProgram() : Promise.resolve();
315
+ if (seq !== loadSeq || inputId() !== id) return undefined;
316
+ // The host owns the intent across account-scoped worker replacement. The consume is
317
+ // atomic and destructive. A background guest refresh keeps a pending request; the
318
+ // host modal boundary cancels it on dismissal. Route params and hydration never
319
+ // authorize a join. Check prices before spending the one shot.
320
+ const waiting = await continuation("check", id);
321
+ if (waiting?.confirmed && found?.program) {
322
+ await prices;
323
+ if (seq === loadSeq && inputId() === id) {
324
+ // Authority stays in the host until the last asynchronous prerequisite completes:
325
+ // expiry, cancellation or an intervening account switch can still revoke it.
326
+ const permit = await continuation("consume", id);
327
+ if (permit?.confirmed && permit.account) await enroll(permit.account);
328
+ }
329
+ }
293
330
  void resumeCommerce();
294
331
  return undefined;
295
332
  }
@@ -310,16 +347,88 @@ async function priceProgram(): Promise<void> {
310
347
  void svc.pricingPublish();
311
348
  }
312
349
 
313
- async function enroll(): Promise<undefined> {
350
+ async function enroll(afterAuthAccount = ""): Promise<undefined> {
351
+ const id = inputId();
314
352
  const reference = programReference();
315
- if (!reference) return undefined;
316
- // A tap that lands as a refresh starts is refused without a verdict: the route is unknown, not
317
- // App Store, and the screen already reads "Loading price…" with Enrol disabled.
318
- actionMessage = pricing.checkoutGuard(reference).route === "resolving" ? "" : await enrollmentMessage(content.name, {
319
- mayStart: () => pricing.checkoutGuard(reference).allowed,
320
- mayContinue: () => pricing.continueCheckoutGuard(reference).allowed,
321
- });
353
+ if (!reference || enrollmentInFlight) return undefined;
354
+ if (!connectivity().online) {
355
+ actionMessage = offlineWriteMessage;
356
+ void svc.pricingPublish();
357
+ return undefined;
358
+ }
359
+ if (!pricing.checkoutGuard(reference).allowed) {
360
+ actionMessage = pricing.checkoutGuard(reference).route === "resolving" ? "" : iapPriceCopy("program").purchaseUnavailable;
361
+ void svc.pricingPublish();
362
+ return undefined;
363
+ }
364
+ enrollmentInFlight = true;
322
365
  void svc.pricingPublish();
366
+ try {
367
+ const member = await memberAccess();
368
+ if (id !== inputId()) return undefined;
369
+ if (afterAuthAccount && member.email !== afterAuthAccount) return undefined;
370
+ if (!member.email) {
371
+ const request = await continuation("begin", id);
372
+ if (request?.supported !== true || !request.token) {
373
+ actionMessage = copyParam("program.signInUpdateRequired", "Update the app to sign in and continue enrollment.");
374
+ return undefined;
375
+ }
376
+ if (openedAuthToken === request.token) return undefined;
377
+ openedAuthToken = request.token;
378
+ actionMessage = "";
379
+ try {
380
+ await dispatch("himi.open", { screenId: "himi_sign_in", mode: "signin", presentation: signInPresentation, modal: "1", authContinuation: request.token });
381
+ } catch (error) {
382
+ openedAuthToken = "";
383
+ await continuation("cancel", id, request.token);
384
+ throw error;
385
+ }
386
+ return undefined;
387
+ }
388
+ await assertMemberAccess(member);
389
+ if (afterAuthAccount) {
390
+ // Branded's initPhase refetches participation after NOW_LOGGED_IN, before
391
+ // joining. A settled participant need not have a current Join Application.
392
+ const memberId = (await getMyMember()).member?.id;
393
+ if (!memberId) throw new Error("Current member unavailable");
394
+ const response = await searchMemberParticipation(id, memberId);
395
+ await assertMemberAccess(member);
396
+ if (id !== inputId()) return undefined;
397
+ if (!Array.isArray(response.participants)) throw new Error("Participation unavailable");
398
+ const participant = response.participants[0];
399
+ if (participant) {
400
+ if (participant.memberId !== memberId || participant.programId !== id) throw new Error("Participation identity changed");
401
+ const status = (participant.enrollmentStatus ?? "").toUpperCase();
402
+ const progress = (participant.progressStatus ?? "").toUpperCase();
403
+ if (status === "JOINED" || ["COMPLETED", "FAILED"].includes(progress)) {
404
+ actionMessage = ["COMPLETED", "FAILED"].includes(progress)
405
+ ? copyTemplate("program.participationRecordedMessage", "Your participation in {name} is already recorded.", { name: content.name })
406
+ : copyTemplate("program.joinedMessage", "You're enrolled in {name}.", { name: content.name });
407
+ return undefined;
408
+ }
409
+ if (!["LEFT", "REMOVED"].includes(status)) {
410
+ actionMessage = copyParam("program.participationUnavailable", "Your current participation cannot be enrolled again.");
411
+ return undefined;
412
+ }
413
+ }
414
+ }
415
+ // joinProgram refreshes the current Join Application before creating anything. Its
416
+ // existing-participant/private/paid and pending-payment guards remain authoritative.
417
+ const message = await enrollmentMessage(id, content.name, {
418
+ assertMember: async () => {
419
+ await assertMemberAccess(member);
420
+ if (!connectivity().online) throw new Error(offlineWriteMessage);
421
+ },
422
+ mayStart: () => id === inputId() && pricing.checkoutGuard(reference).allowed,
423
+ mayContinue: () => id === inputId() && pricing.continueCheckoutGuard(reference).allowed,
424
+ });
425
+ if (id === inputId()) actionMessage = message;
426
+ } catch {
427
+ if (id === inputId()) actionMessage = copyTemplate("program.joinOnSiteMessage", "Enrolment happens on the website — visit the site to join {name}.", { name: content.name });
428
+ } finally {
429
+ enrollmentInFlight = false;
430
+ void svc.pricingPublish();
431
+ }
323
432
  return undefined;
324
433
  }
325
434
 
@@ -343,7 +452,7 @@ function pricedState(state: State): State {
343
452
  actionMessage: purchase.accessReady ? copyTemplate("program.joinedMessage", "You're enrolled in {name}.", { name: content.name }) : actionMessage,
344
453
  // `disabled`: this app sells nothing through the App Store, so the program shows its own price.
345
454
  priceLine: view?.route === "disabled" ? pricedItem?.commerceDisplayPrice ?? "" : price.priceText, priceNote: price.priceNote, hidePriceRetry: view?.route === "disabled" || (price.hidePriceRetry && commerce.scopeComplete()),
346
- enrollDisabled: price.actionDisabled, enrollNote: price.actionNote, enrollHidden: purchase.appleRoute,
455
+ enrollDisabled: price.actionDisabled || enrollmentInFlight, enrollNote: price.actionNote, enrollHidden: purchase.appleRoute,
347
456
  buyHidden: !purchase.appleRoute || purchase.accessReady, buyLabel: purchase.buyLabel, buyDisabled: purchase.buyDisabled, buyNote: purchase.buyNote,
348
457
  ...(poll ? { _poll: poll } : {}),
349
458
  });
@@ -394,7 +503,7 @@ const svc = defineService<State, Actions>("himiBrandedProgram", {
394
503
  notHydrated: [...PRICING_NOT_HYDRATED],
395
504
  actions: {
396
505
  load,
397
- enroll,
506
+ enroll: () => enroll(),
398
507
  buy,
399
508
  pricingPublish: pricedState,
400
509
  pricingStart: pricedState,
@@ -0,0 +1,35 @@
1
+ import { defineService } from "@himalaya/state";
2
+
3
+ // Native renderers animate; this worker only publishes the requested visibility.
4
+ // The host waits for presentation, bounded by the original startup deadline.
5
+ const input = (globalThis as unknown as { himalaya?: { input?: Record<string, unknown> } }).himalaya?.input;
6
+ interface State {
7
+ title: string;
8
+ caption: string;
9
+ iconHidden: boolean;
10
+ presentationPending: boolean;
11
+ errorText?: string;
12
+ customLogoHidden?: boolean;
13
+ backgroundImageUrl?: string;
14
+ }
15
+ export const splash = defineService<State, { present: (state: State) => State }>("splash", {
16
+ state: {
17
+ title: typeof input?.title === "string" ? input.title : "",
18
+ caption: typeof input?.caption === "string" ? input.caption : "",
19
+ iconHidden: false,
20
+ presentationPending: true,
21
+ // A malformed OTA toggle must not release the native cover as a successful
22
+ // presentation. The host's existing deadline remains the failure fallback.
23
+ ...(input?.showIcon !== undefined && typeof input.showIcon !== "boolean"
24
+ ? { errorText: "Splash showIcon must be a boolean" } : {}),
25
+ ...(typeof input?.backgroundImageUrl === "string" ? { backgroundImageUrl: input.backgroundImageUrl } : {}),
26
+ ...(input?.customLogo === true ? { customLogoHidden: true } : {}),
27
+ },
28
+ actions: {
29
+ present: (state) => state.errorText ? state : ({ ...state,
30
+ iconHidden: input?.showIcon === false || input?.customLogo === true,
31
+ ...(input?.customLogo === true ? { customLogoHidden: input?.showIcon === false } : {}),
32
+ presentationPending: false,
33
+ }),
34
+ },
35
+ });
@@ -19,6 +19,7 @@ import { createIapCommerce, onlineProgramOrders, pricingPlanOrders, storesAppleS
19
19
  import { IAP_RESTORE_KEYS, NO_IAP_RESTORE, createIapRestore, resetUnhydratedKeysOnOlderRuntime, type IapRestoreFields } from "../_shared/iap-worker.js";
20
20
 
21
21
  interface AuthRouteInput {
22
+ authContinuation?: string;
22
23
  /** Standalone sign-in only: `register` opens the form on Create account. */
23
24
  mode?: string;
24
25
  intent?: string;
@@ -356,6 +357,9 @@ async function authenticate(s: S, operation: "signIn" | "register"): Promise<S |
356
357
  await svc.beginAuth(); // publish busy before waiting for the network
357
358
  const raw = await dispatch(operation === "signIn" ? "himi.wix.signInDirect" : "himi.wix.registerDirect", {
358
359
  email: s.email.trim(), password,
360
+ // An opaque host-issued token, never a return route. Native commits it before the
361
+ // account change can destroy this worker; the replacement does not need this promise.
362
+ ...(routeInput().authContinuation ? { authContinuation: routeInput().authContinuation } : {}),
359
363
  ...(operation === "register" ? { firstName: s.firstName, lastName: s.lastName } : {}),
360
364
  });
361
365
  rejectResult(raw, operation);
@@ -39,6 +39,14 @@ export default defineApp({
39
39
  },
40
40
  capabilities: ["live_activity","push","share","secure_storage","open_url","local_database","clipboard","ui","speech","camera","photos","file_picker","files","presigned_upload","app_info","biometrics","wix_platform","animation"],
41
41
  designTokensPath: "./tokens.json",
42
+ // Native fallback; the T5 splash descriptor owns the editable launch appearance.
43
+ splash: {
44
+ "enabled": true,
45
+ "backgroundColor": "accentPrimary",
46
+ "caption": "Himalaya Owner",
47
+ "minVisibleMs": 400,
48
+ "maxHoldMs": 3000
49
+ },
42
50
  navigation: {
43
51
  "navKind": "responsive",
44
52
  "surfaces": {
@@ -3,6 +3,7 @@
3
3
  // fill __HIMI_ID__ sentinels per request.
4
4
  import config from "../config.js";
5
5
 
6
+
6
7
  const runtimeSetting = (value: unknown): unknown => {
7
8
  if (value === "__HIMI_PODCAST_FEED_URL__") return process.env.PODCAST_FEED_URL?.trim() || "";
8
9
  if (Array.isArray(value)) return value.map(runtimeSetting);
@@ -1,8 +1,8 @@
1
1
  // Copy-as-configuration for WORKER-emitted strings (`himi docs designing-himi-screens`,
2
2
  // §"Copy is configuration"): a worker never hardcodes user-facing copy — the
3
3
  // descriptor bakes the provisioned copy-pack overrides relevant to this worker
4
- // into its onAppear action params as a `copy` JSON object (key → override
5
- // string, computed from the app contract at descriptor-build time), and the
4
+ // into its onAppear action params as a `copy` JSON object (customer strings or
5
+ // source-matched stock translations, computed at descriptor-build time), and the
6
6
  // worker reads every user-facing string through `copyParam(key, fallback)`.
7
7
  //
8
8
  // STASHED (the stashed appear-params idiom, himi-branded-lite-home): the
@@ -16,9 +16,13 @@
16
16
  // every worker that emits user-facing copy, and copied verbatim into the
17
17
  // generated templates' tier5-src/_shared/.
18
18
 
19
- let stashedCopy: Record<string, string> = {};
19
+ /** Customer strings override every read; translated stock is valid only for its
20
+ * original English fallback. Some existing keys serve more than one message. */
21
+ export type CopyPackValue = string | { source: string; translation: string };
20
22
 
21
- /** Absorb the `copy` onAppear param (a JSON object of key → override string)
23
+ let stashedCopy: Record<string, CopyPackValue> = Object.create(null);
24
+
25
+ /** Absorb the `copy` onAppear param (customer strings or stock translation records)
22
26
  * from an action arg. Call first in every action that can receive onAppear
23
27
  * params (load/refresh). Non-copy args and malformed packs are ignored — the
24
28
  * stock fallbacks keep rendering. */
@@ -29,9 +33,15 @@ export function stashCopy(arg: unknown): void {
29
33
  try {
30
34
  const parsed = JSON.parse(raw) as unknown;
31
35
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return;
32
- const next: Record<string, string> = {};
36
+ const next: Record<string, CopyPackValue> = Object.create(null);
33
37
  for (const [key, value] of Object.entries(parsed as Record<string, unknown>)) {
34
38
  if (typeof value === "string") next[key] = value;
39
+ else if (value && typeof value === "object" && !Array.isArray(value)) {
40
+ const row = value as Record<string, unknown>;
41
+ if (typeof row.source === "string" && typeof row.translation === "string") {
42
+ next[key] = { source: row.source, translation: row.translation };
43
+ }
44
+ }
35
45
  }
36
46
  stashedCopy = next;
37
47
  } catch {
@@ -43,7 +53,8 @@ export function stashCopy(arg: unknown): void {
43
53
  * pack / no such key reached this worker. */
44
54
  export function copyParam(key: string, fallback: string): string {
45
55
  const value = stashedCopy[key];
46
- return typeof value === "string" && value !== "" ? value : fallback;
56
+ if (typeof value === "string") return value !== "" ? value : fallback;
57
+ return value?.source === fallback && value.translation !== "" ? value.translation : fallback;
47
58
  }
48
59
 
49
60
  /** Interpolate `{name}` placeholders in a (possibly overridden) template:
@@ -32,3 +32,47 @@ export function richBody(richContent: unknown, fallback = "", imageWidth = 1400)
32
32
  const text = rejected === "too_deep" ? "" : ricosToPlainText(doc);
33
33
  return { bodyJson: json, bodyText: text || fallback };
34
34
  }
35
+
36
+ /** A Wix description may be Ricos (including serialized JSON), HTML or ordinary text.
37
+ * Keep rich documents on the existing bounded encoder path. Non-document JSON and
38
+ * clearly malformed serialization use an independent fallback. Ambiguous bracketed
39
+ * prose stays readable; this does not attempt to repair arbitrary JSON syntax. */
40
+ export function descriptionBody(value: unknown, fallback = "", imageWidth = 1400): RichBody {
41
+ if (typeof value !== "string") return richBody(value, fallback, imageWidth);
42
+ const text = value.trim();
43
+ if (/^[{[]/.test(text)) {
44
+ try {
45
+ const parsed: unknown = JSON.parse(text);
46
+ // Passing the parsed value avoids parsing a valid Ricos document twice; a
47
+ // different JSON structure is rejected by the same existing document check.
48
+ if (parsed && typeof parsed === "object") return richBody(parsed, fallback, imageWidth);
49
+ } catch {
50
+ const objectLike = /^\{\s*(?:["'}]|nodes\b)/.test(text);
51
+ // Only reject scalar-looking malformed arrays when their syntax is clearly
52
+ // unfinished (or contains JS's non-JSON `undefined`). Balanced bracketed
53
+ // prose such as "[1,000]" and "[12:30, 13:30]" is not JSON and stays readable.
54
+ const arrayTokens = text.replace(/"(?:\\.|[^"\\])*"/g, "")
55
+ .replace(/\b(?:true|false|null|undefined)\b/g, "")
56
+ .replace(/-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/g, "");
57
+ const arrayLike = /^\[\s*[{"]/.test(text)
58
+ || (text.startsWith("[") && /^[\s[\]{},:]*$/.test(arrayTokens)
59
+ && (/,\s*]?$/.test(text) || /\bundefined\b/.test(text) || !text.endsWith("]")));
60
+ if (objectLike || arrayLike) return { bodyJson: "", bodyText: fallback };
61
+ }
62
+ }
63
+ // Legacy HTML descriptions stay text. Preserve paragraph breaks and ordinary prose's
64
+ // bare '<' (e.g. "ages < 8"), without relying on browser-only DOM APIs in workers.
65
+ const plain = text
66
+ .replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1\s*>/gi, "")
67
+ .replace(/<br\s*\/?\s*>/gi, "\n")
68
+ .replace(/<\/(?:p|div|h[1-6]|li)\s*>/gi, "\n\n")
69
+ .replace(/<\/?[a-z][^>]*>/gi, "")
70
+ .replace(/&(?:nbsp|amp|lt|gt|quot|apos|#39);/gi, (entity) => ({
71
+ "&nbsp;": " ", "&amp;": "&", "&lt;": "<", "&gt;": ">", "&quot;": '"', "&apos;": "'", "&#39;": "'",
72
+ })[entity.toLowerCase()] ?? entity)
73
+ .replace(/[\t\r ]+/g, " ")
74
+ .replace(/ *\n */g, "\n")
75
+ .replace(/\n{3,}/g, "\n\n")
76
+ .trim();
77
+ return { bodyJson: "", bodyText: plain || fallback };
78
+ }