@wix/himalaya-cli 0.1496.0 → 0.1498.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 (138) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/attendance-classifier.d.ts +37 -0
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/catalog-pricing-contract.d.ts +1 -1
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/index.d.ts +2 -1
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/wix-payments-contract.d.ts +11 -3
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/apple-iap-contract.d.ts +27 -3
  6. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/commerce-contract.d.ts +1 -0
  7. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/iap-verticals/attendance.d.ts +10 -0
  8. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/iap-verticals/index.d.ts +1 -0
  9. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/index.d.ts +1 -1
  10. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/events/src/index.d.ts +33 -2
  11. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap-checkout.mjs +346 -26
  12. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap.mjs +132 -3
  13. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/screens.mjs +366 -48
  14. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-events.mjs +5 -0
  15. package/dist/cli.mjs +817 -666
  16. package/guides/iap-products.md +7 -1
  17. package/package.json +2 -2
  18. package/templates/beauty-salon/dev/index.ts +394 -0
  19. package/templates/beauty-salon/tier5-src/_shared/iap-attendance-form.ts +106 -0
  20. package/templates/beauty-salon/tier5-src/_shared/iap-attendance-purchase.ts +211 -0
  21. package/templates/beauty-salon/tier5-src/_shared/iap-attendance.ts +38 -0
  22. package/templates/beauty-salon/tier5-src/himi-booking-time/index.ts +52 -7
  23. package/templates/beauty-salon-owner/dev/index.ts +571 -0
  24. package/templates/beauty-salon-owner/himalaya.content.json +6 -0
  25. package/templates/beauty-salon-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
  26. package/templates/beauty-salon-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
  27. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
  28. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
  29. package/templates/branded-lite/dev/index.ts +1429 -278
  30. package/templates/branded-lite/tier5-src/_shared/business-chat-presenter.ts +46 -8
  31. package/templates/branded-lite/tier5-src/_shared/iap-attendance-form.ts +106 -0
  32. package/templates/branded-lite/tier5-src/_shared/iap-attendance-purchase.ts +211 -0
  33. package/templates/branded-lite/tier5-src/_shared/iap-attendance.ts +38 -0
  34. package/templates/branded-lite/tier5-src/business-chat/index.ts +3 -2
  35. package/templates/branded-lite/tier5-src/himi-booking-time/index.ts +52 -7
  36. package/templates/branded-lite/tier5-src/himi-events-detail/index.ts +112 -21
  37. package/templates/community-club/dev/index.ts +397 -2
  38. package/templates/community-club/tier5-src/_shared/iap-attendance-form.ts +106 -0
  39. package/templates/community-club/tier5-src/_shared/iap-attendance-purchase.ts +211 -0
  40. package/templates/community-club/tier5-src/_shared/iap-attendance.ts +38 -0
  41. package/templates/community-club/tier5-src/himi-events-detail/index.ts +112 -21
  42. package/templates/community-club-owner/dev/index.ts +571 -0
  43. package/templates/community-club-owner/himalaya.content.json +6 -0
  44. package/templates/community-club-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
  45. package/templates/community-club-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
  46. package/templates/community-club-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
  47. package/templates/community-club-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
  48. package/templates/events-meetups/dev/index.ts +397 -2
  49. package/templates/events-meetups/tier5-src/_shared/iap-attendance-form.ts +106 -0
  50. package/templates/events-meetups/tier5-src/_shared/iap-attendance-purchase.ts +211 -0
  51. package/templates/events-meetups/tier5-src/_shared/iap-attendance.ts +38 -0
  52. package/templates/events-meetups/tier5-src/himi-events-detail/index.ts +112 -21
  53. package/templates/events-meetups-owner/dev/index.ts +571 -0
  54. package/templates/events-meetups-owner/himalaya.content.json +6 -0
  55. package/templates/events-meetups-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
  56. package/templates/events-meetups-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
  57. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
  58. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
  59. package/templates/gym-fitness/dev/index.ts +828 -39
  60. package/templates/gym-fitness/tier5-src/_shared/iap-attendance-form.ts +106 -0
  61. package/templates/gym-fitness/tier5-src/_shared/iap-attendance-purchase.ts +211 -0
  62. package/templates/gym-fitness/tier5-src/_shared/iap-attendance.ts +38 -0
  63. package/templates/gym-fitness/tier5-src/himi-booking-time/index.ts +52 -7
  64. package/templates/gym-fitness/tier5-src/himi-events-detail/index.ts +112 -21
  65. package/templates/gym-fitness-owner/dev/index.ts +571 -0
  66. package/templates/gym-fitness-owner/himalaya.content.json +6 -0
  67. package/templates/gym-fitness-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
  68. package/templates/gym-fitness-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
  69. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
  70. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
  71. package/templates/nonprofit-cause/dev/index.ts +397 -2
  72. package/templates/nonprofit-cause/tier5-src/_shared/iap-attendance-form.ts +106 -0
  73. package/templates/nonprofit-cause/tier5-src/_shared/iap-attendance-purchase.ts +211 -0
  74. package/templates/nonprofit-cause/tier5-src/_shared/iap-attendance.ts +38 -0
  75. package/templates/nonprofit-cause/tier5-src/himi-events-detail/index.ts +112 -21
  76. package/templates/nonprofit-cause-owner/dev/index.ts +571 -0
  77. package/templates/nonprofit-cause-owner/himalaya.content.json +6 -0
  78. package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
  79. package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
  80. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
  81. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
  82. package/templates/owner-lite/dev/index.ts +571 -0
  83. package/templates/owner-lite/himalaya.content.json +6 -0
  84. package/templates/owner-lite/tier5-src/_shared/owner-today-rollup.ts +307 -0
  85. package/templates/owner-lite/tier5-src/_shared/owner-widget-reads.ts +23 -0
  86. package/templates/owner-lite/tier5-src/himi-owner-lite-today/index.ts +8 -20
  87. package/templates/owner-lite/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
  88. package/templates/podcast-creator/dev/index.ts +397 -2
  89. package/templates/podcast-creator/tier5-src/_shared/iap-attendance-form.ts +106 -0
  90. package/templates/podcast-creator/tier5-src/_shared/iap-attendance-purchase.ts +211 -0
  91. package/templates/podcast-creator/tier5-src/_shared/iap-attendance.ts +38 -0
  92. package/templates/podcast-creator/tier5-src/himi-events-detail/index.ts +112 -21
  93. package/templates/podcast-creator-owner/dev/index.ts +571 -0
  94. package/templates/podcast-creator-owner/himalaya.content.json +6 -0
  95. package/templates/podcast-creator-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
  96. package/templates/podcast-creator-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
  97. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
  98. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
  99. package/templates/portfolio-creative/dev/index.ts +397 -2
  100. package/templates/portfolio-creative/tier5-src/_shared/iap-attendance-form.ts +106 -0
  101. package/templates/portfolio-creative/tier5-src/_shared/iap-attendance-purchase.ts +211 -0
  102. package/templates/portfolio-creative/tier5-src/_shared/iap-attendance.ts +38 -0
  103. package/templates/portfolio-creative/tier5-src/himi-events-detail/index.ts +112 -21
  104. package/templates/portfolio-creative-owner/dev/index.ts +571 -0
  105. package/templates/portfolio-creative-owner/himalaya.content.json +6 -0
  106. package/templates/portfolio-creative-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
  107. package/templates/portfolio-creative-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
  108. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
  109. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
  110. package/templates/restaurant/dev/index.ts +397 -2
  111. package/templates/restaurant/tier5-src/_shared/iap-attendance-form.ts +106 -0
  112. package/templates/restaurant/tier5-src/_shared/iap-attendance-purchase.ts +211 -0
  113. package/templates/restaurant/tier5-src/_shared/iap-attendance.ts +38 -0
  114. package/templates/restaurant/tier5-src/himi-events-detail/index.ts +112 -21
  115. package/templates/restaurant-owner/dev/index.ts +571 -0
  116. package/templates/restaurant-owner/himalaya.content.json +6 -0
  117. package/templates/restaurant-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
  118. package/templates/restaurant-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
  119. package/templates/restaurant-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
  120. package/templates/restaurant-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
  121. package/templates/retail-store-owner/dev/index.ts +571 -0
  122. package/templates/retail-store-owner/himalaya.content.json +6 -0
  123. package/templates/retail-store-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
  124. package/templates/retail-store-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
  125. package/templates/retail-store-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
  126. package/templates/retail-store-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
  127. package/templates/yoga-studio/dev/index.ts +828 -39
  128. package/templates/yoga-studio/tier5-src/_shared/iap-attendance-form.ts +106 -0
  129. package/templates/yoga-studio/tier5-src/_shared/iap-attendance-purchase.ts +211 -0
  130. package/templates/yoga-studio/tier5-src/_shared/iap-attendance.ts +38 -0
  131. package/templates/yoga-studio/tier5-src/himi-booking-time/index.ts +52 -7
  132. package/templates/yoga-studio/tier5-src/himi-events-detail/index.ts +112 -21
  133. package/templates/yoga-studio-owner/dev/index.ts +571 -0
  134. package/templates/yoga-studio-owner/himalaya.content.json +6 -0
  135. package/templates/yoga-studio-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
  136. package/templates/yoga-studio-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
  137. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
  138. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
@@ -160,6 +160,13 @@ function safeAvatar(value: unknown): string {
160
160
  return match && !/[@%]/.test(match[1]!) ? value : "";
161
161
  }
162
162
 
163
+ /** A card's picture, only a plain https URL. Wix sends a 200-point thumbnail; a card shows it about 270 wide on a
164
+ * dense screen, so a Wix media URL asks its image service for a larger copy instead of being blown up. */
165
+ function cardImage(value: unknown): string {
166
+ const url = safeAvatar(value);
167
+ return /^https:\/\/static\.wixstatic\.com\/media\//i.test(url) ? url.replace(/\/v1\/fit\/w_\d+,h_\d+,/i, "/v1/fit/w_800,h_800,") : url;
168
+ }
169
+
163
170
  function initials(value: string): string {
164
171
  const words = value.trim().split(/\s+/).filter(Boolean);
165
172
  return words.length > 1 ? `${words[0]![0]}${words[words.length - 1]![0]}`.toUpperCase() : (words[0]?.slice(0, 2) ?? "").toUpperCase();
@@ -316,8 +323,10 @@ const isSafeLink = (url: string): boolean => SAFE_LINK.test(url) && !/[\u0000-\u
316
323
 
317
324
  /** Owner tools (a product, a coupon, a scheduling link) reach a member as plain text: a title, detail
318
325
  * lines and `Label: https://…` lines. On a phone that is a wall of text with a URL nobody can tap, so a
319
- * link line becomes a button and a coupon code its own block. Only plain https links are lifted. */
320
- export function extractBusinessMessageExtras(markdown: string): { text: string; links: { label: string; url: string }[]; couponCode: string } {
326
+ * link line becomes a button and a coupon code its own block. Only plain https links are lifted. A message shown
327
+ * without a card (an AI answer) has no coupon block, so `liftCoupon: false` leaves its code in the text. */
328
+ export function extractBusinessMessageExtras(markdown: string, options: { liftCoupon?: boolean } = {}): { text: string; links: { label: string; url: string }[]; couponCode: string } {
329
+ const liftCoupon = options.liftCoupon ?? true;
321
330
  const kept: string[] = [];
322
331
  const links: { label: string; url: string }[] = [];
323
332
  let couponCode = "";
@@ -343,7 +352,7 @@ export function extractBusinessMessageExtras(markdown: string): { text: string;
343
352
  }
344
353
  }
345
354
  const code = /^(.*?)[,;]?\s*\b[Cc]ode:\s*([A-Z0-9_-]{3,32})$/.exec(trimmed);
346
- if (code && !couponCode) {
355
+ if (code && liftCoupon && !couponCode) {
347
356
  couponCode = code[2]!;
348
357
  if (code[1]!.trim()) kept.push(code[1]!.trim());
349
358
  continue;
@@ -383,6 +392,14 @@ export function presentBusinessMessages<T extends Pick<BusinessMessage, "id" | "
383
392
  noCoupon: boolean;
384
393
  productSlug: string;
385
394
  productName: string;
395
+ isCard: boolean;
396
+ notCard: boolean;
397
+ noSuggestions: boolean;
398
+ noActions: boolean;
399
+ cardTitle: string;
400
+ cardDetail: string;
401
+ /** A card with neither title nor detail (a file, a coupon with no line): its text block is hidden, not an empty gap. */
402
+ noCardText: boolean;
386
403
  })[] {
387
404
  const metadata = new Map<string, RecordValue>();
388
405
  for (const value of raw) {
@@ -410,14 +427,15 @@ export function presentBusinessMessages<T extends Pick<BusinessMessage, "id" | "
410
427
  : "";
411
428
  if (key) previousDay = key;
412
429
 
413
- const imageUrl = imagePart?.url || "";
414
430
  const template = object(message.templateData);
431
+ // A product the owner sends from the Inbox carries its picture on the card itself (`templateData.image`).
432
+ const imageUrl = imagePart?.url || cardImage(template.image);
415
433
  const standardEventText = message.messageType === "template/standard-event"
416
434
  ? [template.title, ...(Array.isArray(template.lines) ? template.lines : [])]
417
435
  .filter((value): value is string => typeof value === "string" && !!value.trim())
418
436
  .join("\n")
419
437
  : "";
420
- const bodyText = imageUrl
438
+ const bodyText = imagePart?.url
421
439
  ? row.parts.filter((part) => part.kind === "text").map((part) => [part.text, ...(part.fields ?? []).map((field) => `${field.name}: ${field.value}`)].filter(Boolean).join("\n")).filter(Boolean).join("\n")
422
440
  : standardEventText || row.plainText;
423
441
  const senderAvatar = safeAvatar(image.url);
@@ -434,7 +452,8 @@ export function presentBusinessMessages<T extends Pick<BusinessMessage, "id" | "
434
452
  const bodyRuns = htmlMessageRuns(message) ?? plainTextRuns;
435
453
  const bodyMarkdown = markdownFromRuns(bodyRuns);
436
454
  const buttons = messageButtons(message, row.id);
437
- const extras = mine ? { text: bodyMarkdown, links: [], couponCode: "" } : extractBusinessMessageExtras(bodyMarkdown);
455
+ // An AI answer is never a card, and only a card shows the coupon block, so its code stays in its text.
456
+ const extras = mine ? { text: bodyMarkdown, links: [], couponCode: "" } : extractBusinessMessageExtras(bodyMarkdown, { liftCoupon: !isAiGenerated });
438
457
  // A card is a title and detail lines: Wix sends events as one, and an owner's product, coupon or scheduling link
439
458
  // arrives as plain text of the same shape. Its title leads in bold, and a coupon's title is its code, which
440
459
  // the coupon block already shows.
@@ -447,10 +466,21 @@ export function presentBusinessMessages<T extends Pick<BusinessMessage, "id" | "
447
466
  const titled = cardLike && extras.couponCode && first === extras.couponCode ? lines.slice(1).join("\n")
448
467
  : cardLike && lines.length > 1 && !first.startsWith("**") ? `**${first}**\n${lines.slice(1).join("\n")}` : extras.text;
449
468
  // A product card links to its store page; the slug lets the worker find the product and show its picture.
450
- const productPage = cardLike ? extras.links.map((link) => /^https:\/\/[^/?#]+(?:\/[^/?#]+)*?\/product-page\/([^/?#]+)/i.exec(link.url)?.[1]).find(Boolean) : undefined;
469
+ // The link is a line of the text or, from the Inbox's product tool, a template button.
470
+ // Only a card shows the picture, so an AI answer (never a card) does not look one up.
471
+ const productPage = cardLike && !isAiGenerated ? [...extras.links, ...articleActions(message, row.id)].map((link) => /^https:\/\/[^/?#]+(?:\/[^/?#]+)*?\/product-page\/([^/?#]+)/i.exec(link.url)?.[1]).find(Boolean) : undefined;
451
472
  // A file, recording or video has no inline player here: it gets a button that opens it.
452
473
  const attachmentLinks = row.parts.filter((part) => (part.kind === "file" || part.kind === "audio" || part.kind === "video") && !!part.url)
453
474
  .map((part, at) => ({ id: `${row.id}:attachment-${at}`, label: (part.fileName || part.caption || `Open ${part.kind}`).slice(0, 40), url: part.url as string, style: "secondary" as const }));
475
+ // The card look (a framed box with a centred title) is for what an owner tool sends: a product, coupon, scheduling
476
+ // or pay link, an event, or a file. A coupon's title is its code, which the coupon block already shows.
477
+ // An AI answer is text with link pills, as on the site's Smart Chat, even when it is shaped like a card.
478
+ const isCard = !mine && !isAiGenerated && (cardLike || !!extras.couponCode || (attachmentLinks.length > 0 && !imageUrl));
479
+ const cardTitle = cardLike && !(extras.couponCode && first === extras.couponCode) ? first.replace(/^\*\*(.+)\*\*$/, "$1") : "";
480
+ // A product card shows its name and price, as on the site's Smart Chat; the "4 variants | In stock" summary the
481
+ // Inbox adds to the text is left out of it.
482
+ const cardLines = cardTitle ? lines.slice(1) : titled.split("\n");
483
+ const cardDetail = (productPage ? cardLines.filter((line) => !/^\s*\d+\s+variants?\b|\|\s*(?:in|out of)\s+stock\s*$/i.test(line)) : cardLines).join("\n");
454
484
  const articles = [...articleActions(message, row.id), ...attachmentLinks, ...extras.links.map((link, at) => ({ id: `${row.id}:link-${at}`, label: link.label, url: link.url, style: /\b(view|buy|shop|book|pay|order|checkout|reserve|schedule)\b/i.test(link.label) ? "primary" as const : "secondary" as const }))]
455
485
  .filter((action, at, all) => all.findIndex((other) => other.url === action.url) === at);
456
486
 
@@ -463,7 +493,8 @@ export function presentBusinessMessages<T extends Pick<BusinessMessage, "id" | "
463
493
  senderInitials: typeof display.initials === "string" && display.initials.trim() ? display.initials.trim() : initials(senderName),
464
494
  senderBadge: participantId && !mine ? isAiGenerated ? "AI assistant" : "Staff Response" : "",
465
495
  imageUrl,
466
- imageDescription: imagePart?.caption || "Image attachment",
496
+ // A card's own picture is named by the card (a product's name), not as an anonymous attachment.
497
+ imageDescription: imagePart?.caption || (imageUrl && !imagePart?.url && first ? first.replace(/\*/g, "") : "Image attachment"),
467
498
  noImage: !imageUrl,
468
499
  bodyText,
469
500
  bodyRuns,
@@ -472,6 +503,13 @@ export function presentBusinessMessages<T extends Pick<BusinessMessage, "id" | "
472
503
  noCoupon: !extras.couponCode,
473
504
  productSlug: productPage ?? "",
474
505
  productName: productPage ? first : "",
506
+ isCard,
507
+ notCard: !isCard,
508
+ noSuggestions: buttons.length === 0,
509
+ noActions: articles.length === 0,
510
+ cardTitle,
511
+ noCardText: !cardTitle && !cardDetail.trim(),
512
+ cardDetail,
475
513
  isAiGenerated,
476
514
  suggestions: buttons,
477
515
  articleActions: articles,
@@ -0,0 +1,106 @@
1
+ import { copyParam } from "./himi-copy.js";
2
+
3
+ export interface AttendanceField {
4
+ id: string;
5
+ label: string;
6
+ kind: string;
7
+ required: boolean;
8
+ value: string;
9
+ selectedIds: string[];
10
+ options: { id: string; label: string }[];
11
+ error: string;
12
+ maxLength?: number;
13
+ maxSize?: number;
14
+ }
15
+
16
+ const object = (v: unknown): Record<string, unknown> => v !== null && typeof v === "object" && !Array.isArray(v) ? v as Record<string, unknown> : {};
17
+ const text = (v: unknown): string => typeof v === "string" ? v : "";
18
+ const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
19
+
20
+ export function createAttendanceForm(prefix: string) {
21
+ let fields: AttendanceField[] = [];
22
+ let supported = false;
23
+ const label = (name: string) => copyParam(`${prefix}.${name}Label`, name === "firstName" ? "First name" : name === "lastName" ? "Last name" : "Email");
24
+ const field = (scope: string, name: string, input: Record<string, unknown> = {}): AttendanceField => ({
25
+ id: `${scope}:${name}`, label: text(input.label) || label(name), required: input.mandatory === true,
26
+ kind: name === "email" ? "email" : input.type === "NUMBER" ? "number" : input.type === "TEXT_ARRAY" ? "multiple" : Array.isArray(input.options) && input.options.length ? "choice" : "text",
27
+ value: "", selectedIds: [], error: "",
28
+ options: Array.isArray(input.options) ? input.options.filter((o): o is string => typeof o === "string").map((o) => ({ id: o, label: o })) : [],
29
+ ...(typeof input.maxLength === "number" ? { maxLength: input.maxLength } : {}),
30
+ ...(typeof input.maxSize === "number" ? { maxSize: input.maxSize } : {}),
31
+ });
32
+ const contact = (scope: string) => ["firstName", "lastName", "email"].map((name) => field(scope, name, { mandatory: true }));
33
+
34
+ function event(raw: unknown): void {
35
+ fields = contact("buyer");
36
+ const controls = object(raw).controls;
37
+ supported = Array.isArray(controls) && controls.length > 0;
38
+ const names = new Set<string>();
39
+ for (const control of Array.isArray(controls) ? controls : []) {
40
+ const c = object(control);
41
+ if (c.deleted === true) continue;
42
+ if (!Array.isArray(c.inputs) || !c.inputs.length) { supported = false; continue; }
43
+ for (const rawInput of c.inputs) {
44
+ const input = object(rawInput);
45
+ const name = text(input.name);
46
+ if (!name || names.has(name) || !["TEXT", "NUMBER", "TEXT_ARRAY"].includes(text(input.type)) || (input.type === "TEXT_ARRAY" && (!Array.isArray(input.options) || !input.options.length))) { supported = false; continue; }
47
+ names.add(name);
48
+ fields.push(field("guest", name, { ...input, mandatory: input.mandatory === true || ["firstName", "lastName", "email"].includes(name) }));
49
+ }
50
+ }
51
+ if (!["firstName", "lastName", "email"].every((name) => names.has(name))) supported = false;
52
+ }
53
+
54
+ function booking(service: unknown): void {
55
+ fields = contact("buyer");
56
+ const id = text(object(object(service).form).id);
57
+ // Custom intake schemas need their own reader; never silently omit their required fields.
58
+ supported = id === "00000000-0000-0000-0000-000000000000";
59
+ }
60
+
61
+ function change(arg: unknown): void {
62
+ const a = object(arg);
63
+ const f = fields.find((candidate) => candidate.id === a.id);
64
+ if (!f) return;
65
+ if (typeof a.option === "string") {
66
+ if (f.kind === "multiple") f.selectedIds = f.selectedIds.includes(a.option) ? f.selectedIds.filter((v) => v !== a.option) : [...f.selectedIds, a.option];
67
+ else f.value = a.option;
68
+ f.error = "";
69
+ return;
70
+ }
71
+ f.value = text(a.value ?? a.text ?? a.selectedId);
72
+ f.selectedIds = Array.isArray(a.selectedIds) ? a.selectedIds.filter((v): v is string => typeof v === "string") : [];
73
+ f.error = "";
74
+ }
75
+
76
+ function valid(f: AttendanceField): boolean {
77
+ const value = f.value.trim();
78
+ const values = f.kind === "multiple" ? f.selectedIds : value ? [value] : [];
79
+ if (!values.length) return !f.required;
80
+ if (f.options.length && values.some((v) => !f.options.some((o) => o.id === v))) return false;
81
+ if (f.maxSize !== undefined && values.length > f.maxSize) return false;
82
+ if (f.maxLength !== undefined && values.some((v) => v.length > f.maxLength!)) return false;
83
+ return f.kind === "email" ? EMAIL.test(value) : f.kind !== "number" || /^-?\d+(?:\.\d+)?$/.test(value);
84
+ }
85
+
86
+ function validate(): boolean {
87
+ for (const f of fields) f.error = valid(f) ? "" : copyParam(`${prefix}.formInvalidText`, "Check this field.");
88
+ return supported && fields.every((f) => !f.error);
89
+ }
90
+
91
+ function buyer(): Record<string, string> {
92
+ return Object.fromEntries(fields.filter((f) => f.id.startsWith("buyer:")).map((f) => [f.id.slice(6), f.value.trim()]));
93
+ }
94
+
95
+ function ticket(definitionId: string) {
96
+ const inputValues = fields.filter((f) => f.id.startsWith("guest:") && (f.value.trim() || f.selectedIds.length)).map((f) => ({ inputName: f.id.slice(6), ...(f.kind === "multiple" ? { values: f.selectedIds.slice() } : { value: f.value.trim() }) }));
97
+ return { buyer: buyer(), guests: [{ ticketDefinitionId: definitionId, form: { inputValues } }] };
98
+ }
99
+
100
+ return {
101
+ event, booking, change, validate, buyer, ticket,
102
+ supported: () => supported,
103
+ rows: () => fields.map((f) => ({ ...f, selectedIds: f.selectedIds.slice(), kind: f.kind === "text" && f.required ? "requiredText" : f.kind })),
104
+ clear: () => { for (const f of fields) { f.value = ""; f.selectedIds = []; f.error = ""; } },
105
+ };
106
+ }
@@ -0,0 +1,211 @@
1
+ import { attendanceCheckoutRoute, availableTicketAttendanceOf, bookingAttendanceOf, classifyForApple, hostSessionFromGlobal, iapPriceFields, type BookingAttendance, type CatalogReference, type IapPollDirective, type TicketAttendance } from "@himalaya/iap";
2
+ import { bookingOrders, createIapCommerce, eventTicketOrders, type IapOrderItem } from "@himalaya/iap-checkout";
3
+ import { copyParam, copyTemplate } from "./himi-copy.js";
4
+ import { createAttendanceForm, type AttendanceField } from "./iap-attendance-form.js";
5
+ import { attendancePlatform } from "./iap-attendance.js";
6
+ import { iapPriceCopy, iapPurchaseCopy } from "./iap-worker.js";
7
+
8
+ export interface AttendancePurchaseFields {
9
+ iapBuyHidden: boolean;
10
+ iapBuyDisabled: boolean;
11
+ iapBuyLabel: string;
12
+ iapBusy: boolean;
13
+ iapPrice: string;
14
+ iapNote: string;
15
+ iapRetryHidden: boolean;
16
+ iapFormHidden: boolean;
17
+ iapFormError: string;
18
+ iapSignInHidden: boolean;
19
+ iapFields: AttendanceField[];
20
+ _poll?: IapPollDirective;
21
+ }
22
+
23
+ export const ATTENDANCE_INITIAL: AttendancePurchaseFields = {
24
+ iapBuyHidden: true, iapBuyDisabled: true, iapBuyLabel: "", iapBusy: false,
25
+ iapPrice: "", iapNote: "", iapRetryHidden: true, iapFormHidden: true, iapFormError: "", iapSignInHidden: true, iapFields: [],
26
+ };
27
+ export const ATTENDANCE_NOT_HYDRATED = [...Object.keys(ATTENDANCE_INITIAL), "_poll"] as (keyof AttendancePurchaseFields & string)[];
28
+
29
+ export function createAttendancePurchase(options: { kind: "bookingsService" | "eventsTicket"; prefix: string; pollAction: string; onChange(): void }) {
30
+ const { prefix } = options;
31
+ let form = createAttendanceForm(prefix);
32
+ const submittedForms = new Map<string, ReturnType<typeof createAttendanceForm>>();
33
+ const session = hostSessionFromGlobal();
34
+ const commerce = createIapCommerce({ adapters: [options.kind === "bookingsService" ? bookingOrders : eventTicketOrders], onChange: options.onChange });
35
+ let generation = session.current().generation;
36
+ let reference: CatalogReference | undefined;
37
+ let metadata: BookingAttendance | TicketAttendance | undefined;
38
+ let platform = "unknown";
39
+ let started = false;
40
+ let inFlight = false;
41
+ let formError = "";
42
+ let sequence = 0;
43
+ let formRevision = 0;
44
+ const attempts = new Map<string, string>();
45
+ session.subscribe(() => { syncSession(); if (reference) options.onChange(); });
46
+
47
+ function syncSession(): void {
48
+ const next = session.current().generation;
49
+ if (next === generation) return;
50
+ generation = next;
51
+ form.clear();
52
+ submittedForms.clear();
53
+ formError = "";
54
+ attempts.clear();
55
+ }
56
+
57
+ async function refresh(): Promise<void> {
58
+ const next = await attendancePlatform(publishPlatform);
59
+ if (next !== "unknown" || platform === "unknown") platform = next;
60
+ await resume();
61
+ }
62
+
63
+ async function resume(): Promise<void> {
64
+ if (reference) await commerce.loadPrices([{ reference }]);
65
+ const first = !started;
66
+ started = true;
67
+ await (first ? commerce.start() : commerce.reconcile());
68
+ }
69
+
70
+ function publishPlatform(next: string): void {
71
+ if (next === "unknown" && platform !== "unknown") return;
72
+ platform = next;
73
+ void resume().then(options.onChange, options.onChange);
74
+ }
75
+
76
+ function show(): void {
77
+ void attendancePlatform(publishPlatform).then(publishPlatform, options.onChange);
78
+ }
79
+
80
+ async function booking(id: string, service: unknown): Promise<void> {
81
+ syncSession();
82
+ reference = { kind: options.kind, id };
83
+ metadata = bookingAttendanceOf(service);
84
+ selectForm(id, (next) => next.booking(service));
85
+ formRevision++;
86
+ formError = "";
87
+ await show();
88
+ }
89
+
90
+ async function ticket(id: string, event: unknown, available: unknown): Promise<void> {
91
+ syncSession();
92
+ reference = id ? { kind: options.kind, id } : undefined;
93
+ metadata = availableTicketAttendanceOf(event, available);
94
+ selectForm(id, (next) => next.event((event as { form?: unknown } | undefined)?.form));
95
+ formRevision++;
96
+ formError = "";
97
+ await show();
98
+ }
99
+
100
+ function selectForm(id: string, initialize: (next: ReturnType<typeof createAttendanceForm>) => void): void {
101
+ const key = `${options.kind}:${id}`;
102
+ const retained = submittedForms.get(key);
103
+ form = retained ?? createAttendanceForm(prefix);
104
+ if (!retained) initialize(form);
105
+ }
106
+
107
+ function fields(selected = true, publishPoll = true): AttendancePurchaseFields {
108
+ syncSession();
109
+ if (!reference || !metadata) return ATTENDANCE_INITIAL;
110
+ const currency = metadata.currency ?? "";
111
+ const classified = classifyForApple(metadata, currency);
112
+ const candidate = platform === "ios" && classified === "appleIap";
113
+ const price = iapPriceFields(commerce.pricing.view(reference), iapPriceCopy(prefix));
114
+ const copy = { ...iapPurchaseCopy(prefix), buy: copyParam(`${prefix}.appleBuyCta`, options.kind === "bookingsService" ? "Book with Apple" : "Buy ticket with Apple"), buyAnother: copyParam(`${prefix}.buyAnotherCta`, options.kind === "bookingsService" ? "Book again" : "Buy another ticket") };
115
+ const purchase = commerce.purchaseFields(reference, copy);
116
+ const apple = candidate || (platform === "ios" && classified !== "wixPayments" && purchase.appleRoute);
117
+ const enabled = attendanceCheckoutRoute(metadata, currency, platform) === "appleIap";
118
+ const poll = publishPoll ? commerce.pollDirective(options.pollAction) : undefined;
119
+ const state = commerce.state();
120
+ for (const [target, attempt] of attempts) {
121
+ const key = `${reference.kind}:${reference.id}#${encodeURIComponent(attempt)}`;
122
+ if ((state.phase === "accessReady" && state.item === key) || state.payments.some((p) => p.item === key && p.access === "granted")) attempts.delete(target);
123
+ }
124
+ const formKey = `${reference.kind}:${reference.id}`;
125
+ if (![...attempts.keys()].some((target) => target.startsWith(`${formKey}:`))) submittedForms.delete(formKey);
126
+ const own = state.item?.startsWith(`${reference.kind}:${reference.id}#`);
127
+ const completed = state.phase === "accessReady" && own;
128
+ const member = session.current().kind === "member";
129
+ let note = "";
130
+ if (apple && !enabled) note = copy.unavailable;
131
+ else if (apple && !member) note = copy.signInRequired;
132
+ else if (apple && purchase.buyNote) note = purchase.buyNote;
133
+ else if (apple && !form.supported()) note = copyParam(`${prefix}.formUnavailableText`, "This registration form is not supported yet.");
134
+ else if (apple && completed) note = copyParam(`${prefix}.purchaseCompleteText`, options.kind === "bookingsService" ? "Your booking is confirmed." : "Your ticket is confirmed.");
135
+ else if (apple && own && state.phase === "cancelled") note = copyParam(`${prefix}.purchaseCancelledText`, "Purchase cancelled. Your place is not confirmed.");
136
+ return {
137
+ iapBuyHidden: !apple, iapBuyDisabled: !enabled || !selected || !member || !form.supported() || purchase.buyDisabled || inFlight,
138
+ iapBuyLabel: purchase.buyLabel || copy.buy, iapBusy: purchase.buyBusy || inFlight,
139
+ iapPrice: apple ? price.priceText : "", iapRetryHidden: !apple || (price.hidePriceRetry && commerce.scopeComplete()),
140
+ iapNote: note,
141
+ iapFormHidden: !apple || !selected || !form.supported(), iapFormError: formError,
142
+ iapSignInHidden: !apple || member || options.kind === "bookingsService",
143
+ iapFields: form.rows().map((f, index, rows) => ({ ...f,
144
+ id: `${generation}:${formRevision}:${f.id}`, fieldId: f.id,
145
+ errorHidden: !f.error,
146
+ options: f.options.map((o) => ({ ...o, fieldId: f.id, displayLabel: (f.kind === "multiple" ? f.selectedIds.includes(o.id) : f.value === o.id) ? `${o.label} ✓` : o.label })),
147
+ label: f.required ? `${f.label} *` : f.label,
148
+ inputLabel: copyTemplate(`${prefix}.fieldAccessibilityLabel`, "{section}: {field}", { section: f.id.startsWith("buyer:") ? copyParam(`${prefix}.buyerDetailsLabel`, "Buyer details") : copyParam(`${prefix}.guestDetailsLabel`, "Ticket holder details"), field: f.label }),
149
+ sectionLabel: index === 0 ? copyParam(`${prefix}.buyerDetailsLabel`, "Buyer details") : f.id.startsWith("guest:") && rows[index - 1].id.startsWith("buyer:") ? copyParam(`${prefix}.guestDetailsLabel`, "Ticket holder details") : "",
150
+ })), ...(poll ? { _poll: poll } : {}),
151
+ };
152
+ }
153
+
154
+ async function buy(selection: { key: string; slot?: Record<string, unknown>; eventId?: string }): Promise<void> {
155
+ syncSession();
156
+ if (!reference || !metadata || fields(true, false).iapBuyDisabled || inFlight) return;
157
+ if (options.kind === "eventsTicket" && reference.id !== selection.key) return;
158
+ if (!form.validate()) { formError = copyParam(`${prefix}.formRequiredText`, "Complete the required details before purchasing."); return; }
159
+ if (session.current().kind !== "member") { formError = iapPurchaseCopy(prefix).signInRequired; return; }
160
+ const currentGeneration = generation;
161
+ const ref = reference;
162
+ submittedForms.set(`${ref.kind}:${ref.id}`, form);
163
+ const target = `${ref.kind}:${ref.id}:${selection.key}`;
164
+ let attemptId = attempts.get(target);
165
+ const state = commerce.state();
166
+ const attemptKey = attemptId ? `${ref.kind}:${ref.id}#${encodeURIComponent(attemptId)}` : "";
167
+ const completed = attemptId && ((state.phase === "accessReady" && state.item === attemptKey) || state.payments.some((p) => p.item === attemptKey && p.access === "granted"));
168
+ if (!attemptId || completed) { attemptId = `${Date.now().toString(36)}-${++sequence}-${Math.random().toString(36).slice(2)}`; attempts.set(target, attemptId); }
169
+ const item: IapOrderItem = { reference: ref, attemptId, quantity: 1, attendance: {
170
+ amount: metadata.amount!, currency: metadata.currency!,
171
+ ...(options.kind === "bookingsService" ? { booking: { slot: selection.slot ?? {}, contactDetails: form.buyer() } } : { ticket: { eventId: selection.eventId ?? "", ...form.ticket(ref.id) } }),
172
+ } };
173
+ inFlight = true;
174
+ formError = "";
175
+ options.onChange();
176
+ try {
177
+ await commerce.buy(item);
178
+ const after = commerce.state();
179
+ if (after.phase === "accessReady" && after.item === `${ref.kind}:${ref.id}#${encodeURIComponent(attemptId)}`) attempts.delete(target);
180
+ }
181
+ finally { inFlight = false; syncSession(); if (generation !== currentGeneration) form.clear(); options.onChange(); }
182
+ }
183
+
184
+ function change(arg: unknown): void {
185
+ syncSession();
186
+ const state = commerce.state();
187
+ if (reference) {
188
+ for (const [target, attempt] of attempts) {
189
+ const item = `${reference.kind}:${reference.id}#${encodeURIComponent(attempt)}`;
190
+ const canCorrect = state.phase === "cancelled" || (state.phase === "refused" && (state.failure === "orderRefused" || state.failure === "productChanged"));
191
+ if (canCorrect && state.item === item) { attempts.delete(target); submittedForms.delete(`${reference.kind}:${reference.id}`); }
192
+ }
193
+ const referencePrefix = `${reference.kind}:${reference.id}:`;
194
+ if (inFlight || [...attempts.keys()].some((target) => target.startsWith(referencePrefix))) {
195
+ formError = copyParam(`${prefix}.detailsLockedText`, "Finish the current purchase before changing registration details.");
196
+ return;
197
+ }
198
+ }
199
+ form.change(arg);
200
+ formError = "";
201
+ }
202
+
203
+ return {
204
+ booking, ticket, fields, buy,
205
+ usesRegularPlatform: () => platform === "android" || platform === "web",
206
+ invalidate: () => { reference = undefined; metadata = undefined; options.onChange(); },
207
+ change,
208
+ refresh,
209
+ poll: async () => { syncSession(); await commerce.poll(); },
210
+ };
211
+ }
@@ -0,0 +1,38 @@
1
+ import { get as appInfo } from "@himalaya/app-info";
2
+ import { attendanceCheckoutRoute, bookingAttendanceOf, ticketAttendanceOf } from "@himalaya/iap";
3
+
4
+ let knownPlatform: string | undefined;
5
+
6
+ export function attendancePlatform(onResolved?: (platform: string) => void): Promise<string> {
7
+ if (knownPlatform) return Promise.resolve(knownPlatform);
8
+ const info = appInfo().then((value) => value.platform, () => "unknown").then((platform) => {
9
+ if (platform !== "unknown") knownPlatform = platform;
10
+ return platform;
11
+ });
12
+ const host = globalThis as { setTimeout?: (fn: () => void, ms: number) => unknown; clearTimeout?: (timer: unknown) => void };
13
+ if (!host.setTimeout) {
14
+ void info.then((platform) => onResolved?.(platform));
15
+ return Promise.resolve("unknown");
16
+ }
17
+ return new Promise((resolve) => {
18
+ let timedOut = false;
19
+ const timer = host.setTimeout!(() => { timedOut = true; resolve("unknown"); }, 1500);
20
+ void info.then((platform) => { host.clearTimeout?.(timer); resolve(platform); if (timedOut) onResolved?.(platform); });
21
+ });
22
+ }
23
+
24
+ export async function mayUseRegularBookingCheckout(service: unknown): Promise<boolean> {
25
+ const metadata = bookingAttendanceOf(service);
26
+ if (metadata.priceType === "NO_FEE" || isZero(metadata.amount)) return true;
27
+ return attendanceCheckoutRoute(metadata, metadata.currency ?? "", await attendancePlatform()) === "regular";
28
+ }
29
+
30
+ export async function mayUseRegularTicketCheckout(event: unknown, ticket: { free?: boolean; price?: { amount?: string; currency?: string }; saleStatus?: string } | undefined): Promise<boolean> {
31
+ if (ticket?.free === true || isZero(ticket?.price?.amount)) return true;
32
+ const metadata = ticketAttendanceOf(event, { hidden: false, saleStatus: ticket?.saleStatus });
33
+ return attendanceCheckoutRoute(metadata, ticket?.price?.currency ?? "", await attendancePlatform()) === "regular";
34
+ }
35
+
36
+ function isZero(value: unknown): boolean {
37
+ return typeof value === "string" && /^0+(?:\.0+)?$/.test(value);
38
+ }
@@ -14,6 +14,7 @@ type PendingIntent = BusinessChatIntent;
14
14
  // parts, plain text or text runs: they would be re-sent each time for nothing.
15
15
  type BusinessMessageRow = Pick<MessageRow,'id'|'sender'|'when'|'createdDate'|'mine'> & {
16
16
  senderAvatar:string; senderName:string; senderInitials:string; senderBadge:string; imageUrl:string; imageDescription:string; noImage:boolean; bodyText:string; bodyMarkdown:string; suggestions:BusinessSuggestion[]; articleActions:{id:string;label:string;url:string;style:'primary'|'secondary'}[]; couponCode:string; noCoupon:boolean; notMine:boolean;
17
+ isCard:boolean; notCard:boolean; noSuggestions:boolean; noActions:boolean; cardTitle:string; cardDetail:string; noCardText:boolean;
17
18
  dayLabel:string; dayHidden:boolean; startsDay:boolean; scrollAnchor:string;
18
19
  /** Only the member's own row that the server has not returned yet. */
19
20
  pending?:boolean;
@@ -67,7 +68,7 @@ function pendingRow(intent:BusinessChatIntent):BusinessMessageRow{
67
68
  const when=intent.delivery==='sending'?'Sending…':intent.delivery==='unconfirmed'?'Not confirmed':messageTimeLabel(intent.createdDate);
68
69
  return {id:intent.id,sender:'',when,createdDate:intent.createdDate,mine:true,
69
70
  senderAvatar:'',senderName:intent.senderName??'',senderInitials:'',senderBadge:'',imageUrl:'',imageDescription:'',noImage:true,bodyText:intent.text,bodyMarkdown:intent.text,
70
- suggestions:[],articleActions:[],couponCode:'',noCoupon:true,notMine:false,dayLabel:'',dayHidden:true,startsDay:false,scrollAnchor:`himi_business_chat.message_${intent.id}`,pending:true};
71
+ suggestions:[],articleActions:[],couponCode:'',noCoupon:true,notMine:false,isCard:false,notCard:true,noSuggestions:true,noActions:true,cardTitle:'',cardDetail:'',noCardText:true,dayLabel:'',dayHidden:true,startsDay:false,scrollAnchor:`himi_business_chat.message_${intent.id}`,pending:true};
71
72
  }
72
73
  function shape(s:State):State{const used=new Set(s.usedSuggestionIds);const messages=s.messages.map(row=>({...row,suggestions:(row.suggestions||[]).map(button=>used.has(button.id)?{...button,enabled:false,disabled:true}:button)}));const quickReplies=(s.quickReplies||[]).map(button=>used.has(button.id)?{...button,enabled:false,disabled:true}:button);const quickEnabled=quickReplies.some(button=>button.enabled);return {...s,messages,quickReplies,hasQuickReplies:quickEnabled,noQuickReplies:!quickEnabled,scrollAnchor:messages[messages.length-1]?.scrollAnchor||'',liveLabel:liveDescription(s.liveState,s.liveReason),hasActiveRoom:!!s.roomId,noActiveRoom:!s.roomId,hasHostedName:!!s.hostedName,noHostedName:!s.hostedName,isGuest:s.phase==='guest',notGuest:s.phase!=='guest',realtimeDiagnostic:`state=${s.liveState}; reason=${s.liveReason||'none'}; events=${s.realtimeEventCount}; lastType=${/^(message|messagesent)$/i.test(s.lastRealtimeEventType)?s.lastRealtimeEventType:'none'}`,errorText:s.error,loadingHidden:s.phase!=='loading'||!!s.error,noError:!s.error,noNotice:!s.notice,noMessages:!messages.length,hasMessages:!!messages.length,noEmpty:s.phase!=='ready'||!!messages.length,
73
74
  notLoading:s.phase!=='loading',sendDisabled:!canSend(s,s.draft),sendEnabled:canSend(s,s.draft),noLive:!s.roomId||s.liveState==='connected'};}
@@ -131,7 +132,7 @@ function productImageFor(slug:string,name:string):Promise<string>{
131
132
  }
132
133
  function slimRow(row:Record<string,unknown>):BusinessMessageRow{
133
134
  const pick=(...keys:string[])=>Object.fromEntries(keys.map(key=>[key,row[key]]));
134
- return pick('id','sender','when','createdDate','mine','senderAvatar','senderName','senderInitials','senderBadge','imageUrl','imageDescription','noImage','bodyText','bodyMarkdown','suggestions','articleActions','couponCode','noCoupon','notMine','dayLabel','dayHidden','startsDay','scrollAnchor') as unknown as BusinessMessageRow;
135
+ return pick('id','sender','when','createdDate','mine','senderAvatar','senderName','senderInitials','senderBadge','imageUrl','imageDescription','noImage','bodyText','bodyMarkdown','suggestions','articleActions','couponCode','noCoupon','notMine','isCard','notCard','noSuggestions','noActions','cardTitle','cardDetail','noCardText','dayLabel','dayHidden','startsDay','scrollAnchor') as unknown as BusinessMessageRow;
135
136
  }
136
137
  async function presentMessages(existing:MessageRow[],raw:unknown[],memberId?:string):Promise<BusinessMessageRow[]>{
137
138
  const rows=chronologicalMessages(existing,raw,memberId) as MessageRow[];