@tribe-nest/forge 3.59.0 → 3.61.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 (266) hide show
  1. package/package.json +2 -1
  2. package/src/_tests/testCommunity.tsx +28 -0
  3. package/src/contexts/CartContext.tsx +34 -1
  4. package/src/contexts/CommunityContext.tsx +192 -0
  5. package/src/contexts/PublicAuthContext.tsx +15 -3
  6. package/src/data/queries/_tests/metaSignals.spec.tsx +130 -0
  7. package/src/data/queries/useAccount.ts +33 -2
  8. package/src/data/queries/useAnalytics.ts +6 -6
  9. package/src/data/queries/useCallChat.ts +2 -2
  10. package/src/data/queries/useChat.ts +52 -40
  11. package/src/data/queries/useCheckouts.ts +2 -1
  12. package/src/data/queries/useCoachingAvailability.ts +10 -1
  13. package/src/data/queries/useCommunities.ts +345 -0
  14. package/src/data/queries/useCommunity.ts +450 -102
  15. package/src/data/queries/useCommunityAgent.ts +247 -0
  16. package/src/data/queries/useCommunityCalendar.ts +150 -0
  17. package/src/data/queries/useCommunityClassroom.ts +75 -0
  18. package/src/data/queries/useCommunityMembers.ts +108 -0
  19. package/src/data/queries/useCommunityModeration.ts +327 -0
  20. package/src/data/queries/useCommunityWiki.ts +137 -0
  21. package/src/data/queries/useCourses.ts +2 -0
  22. package/src/data/queries/useEmailSignIn.ts +79 -0
  23. package/src/data/queries/useEvents.ts +6 -1
  24. package/src/data/queries/useForms.ts +5 -1
  25. package/src/data/queries/useLeadMagnet.ts +6 -2
  26. package/src/data/queries/useMembership.ts +32 -7
  27. package/src/data/queries/useMembershipCalls.ts +116 -7
  28. package/src/data/queries/useMyBookings.ts +24 -1
  29. package/src/data/queries/useMyTickets.ts +24 -1
  30. package/src/data/queries/useNotifications.ts +6 -2
  31. package/src/data/queries/useOrders.ts +2 -1
  32. package/src/data/queries/usePWA.ts +111 -35
  33. package/src/data/queries/usePostCollections.ts +28 -16
  34. package/src/data/queries/usePosts.ts +16 -14
  35. package/src/data/queries/useSiteApp.ts +130 -0
  36. package/src/data/queries/useSubscriptions.ts +28 -9
  37. package/src/i18n/de.json +891 -2
  38. package/src/i18n/en.json +891 -2
  39. package/src/index.ts +36 -7
  40. package/src/runtime/pages.spec.ts +36 -0
  41. package/src/runtime/pages.ts +52 -27
  42. package/src/server/_tests/buildWebManifest.spec.ts +1 -1
  43. package/src/server/_tests/forgeServiceWorkerScript.spec.ts +4 -4
  44. package/src/types/models.ts +11 -0
  45. package/src/ui/analytics/ForgeAnalytics.tsx +64 -34
  46. package/src/ui/analytics/PageMetaPixel.tsx +23 -8
  47. package/src/ui/analytics/_tests/ForgeAnalytics.meta.spec.tsx +129 -0
  48. package/src/ui/analytics/_tests/metaCommerceEvents.spec.tsx +159 -0
  49. package/src/ui/analytics/useMetaEvents.ts +78 -0
  50. package/src/ui/format/_tests/membershipGate.spec.ts +6 -6
  51. package/src/ui/format/membershipGate.ts +19 -15
  52. package/src/ui/headless/_tests/communityAgent.spec.tsx +519 -0
  53. package/src/ui/headless/_tests/communityCalendar.spec.tsx +195 -0
  54. package/src/ui/headless/_tests/communityClassroom.spec.tsx +120 -0
  55. package/src/ui/headless/_tests/communityGate.spec.tsx +80 -0
  56. package/src/ui/headless/_tests/communityHarness.tsx +212 -0
  57. package/src/ui/headless/_tests/communityMembers.spec.tsx +316 -0
  58. package/src/ui/headless/_tests/communityPoll.spec.tsx +389 -0
  59. package/src/ui/headless/_tests/communityProvider.spec.tsx +151 -0
  60. package/src/ui/headless/_tests/memberHomeBar.spec.ts +84 -0
  61. package/src/ui/headless/_tests/siteAppBar.spec.ts +83 -0
  62. package/src/ui/headless/_tests/siteAppHarness.ts +19 -0
  63. package/src/ui/headless/_tests/siteAppHome.spec.ts +226 -0
  64. package/src/ui/headless/_tests/siteAppPrompts.spec.ts +185 -0
  65. package/src/ui/headless/_tests/useCommunityInvite.spec.tsx +104 -0
  66. package/src/ui/headless/_tests/useCommunityJoin.spec.tsx +596 -0
  67. package/src/ui/headless/_tests/useCommunityMembership.spec.tsx +179 -0
  68. package/src/ui/headless/_tests/useCommunityModeration.spec.tsx +420 -0
  69. package/src/ui/headless/_tests/useCommunityReads.spec.tsx +295 -0
  70. package/src/ui/headless/_tests/useCommunityUpgrade.spec.tsx +157 -0
  71. package/src/ui/headless/_tests/useCommunityWiki.spec.tsx +224 -0
  72. package/src/ui/headless/_tests/useSiteApp.spec.tsx +52 -0
  73. package/src/ui/headless/_tests/wikiMediaImage.spec.tsx +132 -0
  74. package/src/ui/headless/auth/useEmailCodeSignIn.ts +186 -0
  75. package/src/ui/headless/auth/useSignupForm.ts +30 -3
  76. package/src/ui/headless/chat/useChatAttachmentUpload.ts +9 -6
  77. package/src/ui/headless/chat/useChatRoom.ts +35 -1
  78. package/src/ui/headless/communities/CommunityGate.tsx +67 -0
  79. package/src/ui/headless/communities/index.ts +96 -0
  80. package/src/ui/headless/communities/paths.ts +36 -0
  81. package/src/ui/headless/communities/useCommunity.ts +140 -0
  82. package/src/ui/headless/communities/useCommunityAbout.ts +71 -0
  83. package/src/ui/headless/communities/useCommunityCalendar.ts +156 -0
  84. package/src/ui/headless/communities/useCommunityClassroom.ts +84 -0
  85. package/src/ui/headless/communities/useCommunityHome.ts +91 -0
  86. package/src/ui/headless/communities/useCommunityInvite.ts +116 -0
  87. package/src/ui/headless/communities/useCommunityJoin.ts +494 -0
  88. package/src/ui/headless/communities/useCommunityMembership.ts +182 -0
  89. package/src/ui/headless/communities/useCommunityUpgrade.ts +213 -0
  90. package/src/ui/headless/communities/useCommunityWiki.ts +266 -0
  91. package/src/ui/headless/communities/useMyMemberships.ts +59 -0
  92. package/src/ui/headless/community/communityPoll.ts +297 -0
  93. package/src/ui/headless/community/countries.ts +123 -0
  94. package/src/ui/headless/community/index.ts +81 -1
  95. package/src/ui/headless/community/linkPreview.ts +71 -0
  96. package/src/ui/headless/community/richLexical.ts +27 -2
  97. package/src/ui/headless/community/useCommunityAgent.ts +285 -0
  98. package/src/ui/headless/community/useCommunityAttachmentUpload.ts +7 -3
  99. package/src/ui/headless/community/useCommunityMembers.ts +175 -0
  100. package/src/ui/headless/community/useCommunityModeration.ts +300 -0
  101. package/src/ui/headless/community/useCommunitySpaceFeed.ts +22 -4
  102. package/src/ui/headless/community/useCommunityThread.ts +16 -3
  103. package/src/ui/headless/community/useMentionComposer.ts +17 -9
  104. package/src/ui/headless/community/useRichMentionComposer.ts +21 -5
  105. package/src/ui/headless/downloads/index.ts +8 -0
  106. package/src/ui/headless/downloads/useDownloadPage.ts +134 -0
  107. package/src/ui/headless/index.ts +21 -4
  108. package/src/ui/headless/memberHome/index.ts +11 -1
  109. package/src/ui/headless/memberHome/memberHomeBar.ts +91 -0
  110. package/src/ui/headless/memberHome/useMemberHomeNav.ts +84 -44
  111. package/src/ui/headless/membership/_tests/membershipCheckoutRefetch.spec.tsx +8 -0
  112. package/src/ui/headless/membership/_tests/useMembershipCheckoutApplication.spec.tsx +5 -4
  113. package/src/ui/headless/membership/_tests/useMembershipCheckoutQuestionnaire.spec.tsx +1 -1
  114. package/src/ui/headless/membership/_tests/useMembershipCheckoutTrial.spec.tsx +1 -1
  115. package/src/ui/headless/membership/useMembershipCheckout.ts +89 -22
  116. package/src/ui/headless/membership/useMembershipGateNotice.ts +25 -7
  117. package/src/ui/headless/portal/index.ts +57 -0
  118. package/src/ui/headless/portal/usePortal.ts +274 -0
  119. package/src/ui/headless/portal/usePortalFiles.ts +68 -0
  120. package/src/ui/headless/portal/usePortalMessages.ts +189 -0
  121. package/src/ui/headless/portal/usePortalPreview.ts +142 -0
  122. package/src/ui/headless/posts/upsell.ts +54 -8
  123. package/src/ui/headless/posts/useMemberPostsFeed.ts +6 -5
  124. package/src/ui/headless/posts/usePostCollectionDetail.ts +7 -4
  125. package/src/ui/headless/posts/usePostCollectionsList.ts +6 -5
  126. package/src/ui/headless/posts/usePostItem.ts +6 -5
  127. package/src/ui/headless/siteApp/appBar.ts +102 -0
  128. package/src/ui/headless/siteApp/appHome.ts +344 -0
  129. package/src/ui/headless/siteApp/appPrompts.ts +229 -0
  130. package/src/ui/headless/siteApp/index.ts +58 -0
  131. package/src/ui/headless/siteApp/useAppHome.ts +210 -0
  132. package/src/ui/headless/siteApp/useAppInstallRequest.ts +16 -0
  133. package/src/ui/headless/work/index.ts +2 -0
  134. package/src/ui/headless/work/useWorkPortal.ts +37 -26
  135. package/src/ui/index.ts +28 -12
  136. package/src/ui/styled/AccountDashboard.tsx +754 -602
  137. package/src/ui/styled/AiAgentWidget.tsx +13 -1
  138. package/src/ui/styled/ChatRoom.tsx +126 -16
  139. package/src/ui/styled/Checkout.tsx +8 -0
  140. package/src/ui/styled/CheckoutConfirmation.tsx +8 -0
  141. package/src/ui/styled/CoachingConfirmation.tsx +8 -0
  142. package/src/ui/styled/CoachingDetail.tsx +2 -0
  143. package/src/ui/styled/CourseConfirmation.tsx +5 -0
  144. package/src/ui/styled/CourseDetail.tsx +2 -0
  145. package/src/ui/styled/DownloadPage.tsx +263 -0
  146. package/src/ui/styled/EventConfirmation.tsx +10 -0
  147. package/src/ui/styled/EventDetail.tsx +10 -0
  148. package/src/ui/styled/FilmLibrary.tsx +22 -9
  149. package/src/ui/styled/MembershipCheckout.tsx +37 -17
  150. package/src/ui/styled/MembershipTierCallout.tsx +20 -1
  151. package/src/ui/styled/MembershipTiers.tsx +49 -11
  152. package/src/ui/styled/MusicLinkPage.tsx +5 -5
  153. package/src/ui/styled/Paywall.tsx +1 -1
  154. package/src/ui/styled/ProductDetail.tsx +12 -0
  155. package/src/ui/styled/PwaRegistration.tsx +5 -1
  156. package/src/ui/styled/SignupForm.tsx +18 -3
  157. package/src/ui/styled/_tests/AccountDashboardBookingCall.spec.tsx +6 -6
  158. package/src/ui/styled/_tests/AccountDashboardMemberships.spec.tsx +151 -0
  159. package/src/ui/styled/_tests/AccountDashboardRentals.spec.tsx +1 -1
  160. package/src/ui/styled/_tests/AccountNotifications.spec.tsx +135 -0
  161. package/src/ui/styled/_tests/CourseCallsSection.spec.tsx +7 -1
  162. package/src/ui/styled/_tests/DownloadPage.spec.tsx +68 -0
  163. package/src/ui/styled/_tests/MembershipCheckout.spec.tsx +54 -11
  164. package/src/ui/styled/_tests/membershipTierCallout.spec.tsx +1 -1
  165. package/src/ui/styled/_tests/membershipTiersCuratedAccess.spec.tsx +1 -1
  166. package/src/ui/styled/app/AppBar.tsx +298 -0
  167. package/src/ui/styled/app/AppHome.tsx +1139 -0
  168. package/src/ui/styled/app/AppHomeMark.tsx +66 -0
  169. package/src/ui/styled/app/AppPrompts.tsx +464 -0
  170. package/src/ui/styled/app/_tests/AppBar.spec.tsx +96 -0
  171. package/src/ui/styled/app/_tests/AppHome.spec.tsx +209 -0
  172. package/src/ui/styled/app/_tests/AppPrompts.spec.tsx +232 -0
  173. package/src/ui/styled/app/index.ts +7 -0
  174. package/src/ui/styled/chat/ChatThreadPanel.tsx +29 -4
  175. package/src/ui/styled/chat/MessageBubble.tsx +43 -6
  176. package/src/ui/styled/communities/CommunitiesList.tsx +88 -0
  177. package/src/ui/styled/communities/CommunityAbout.tsx +145 -0
  178. package/src/ui/styled/communities/CommunityCalendar.tsx +484 -0
  179. package/src/ui/styled/communities/CommunityClassroom.tsx +302 -0
  180. package/src/ui/styled/communities/CommunityHome.tsx +298 -0
  181. package/src/ui/styled/communities/CommunityJoinSheet.tsx +544 -0
  182. package/src/ui/styled/communities/CommunityMembershipMenu.tsx +249 -0
  183. package/src/ui/styled/communities/CommunityPending.tsx +49 -0
  184. package/src/ui/styled/communities/CommunityPublicWiki.tsx +75 -0
  185. package/src/ui/styled/communities/CommunityUpgradeSheet.tsx +137 -0
  186. package/src/ui/styled/communities/CommunityWiki.tsx +289 -0
  187. package/src/ui/styled/communities/MyMembershipsList.tsx +115 -0
  188. package/src/ui/styled/communities/_tests/CommunityAppHome.spec.tsx +92 -0
  189. package/src/ui/styled/communities/_tests/CommunityClassroomCalendar.spec.tsx +453 -0
  190. package/src/ui/styled/communities/_tests/CommunityMobile.spec.tsx +399 -0
  191. package/src/ui/styled/communities/_tests/CommunityPages.spec.tsx +102 -0
  192. package/src/ui/styled/communities/_tests/CommunityWiki.spec.tsx +223 -0
  193. package/src/ui/styled/communities/index.ts +19 -0
  194. package/src/ui/styled/communities/shared.tsx +177 -0
  195. package/src/ui/styled/community/CommunityAgent.tsx +286 -0
  196. package/src/ui/styled/community/CommunityComposer.tsx +266 -15
  197. package/src/ui/styled/community/CommunityFeed.tsx +50 -1
  198. package/src/ui/styled/community/CommunityLinkPreview.tsx +159 -0
  199. package/src/ui/styled/community/CommunityMemberGlobe.tsx +171 -0
  200. package/src/ui/styled/community/CommunityMemberProfile.tsx +137 -4
  201. package/src/ui/styled/community/CommunityMembers.tsx +387 -0
  202. package/src/ui/styled/community/CommunityModeration.tsx +587 -0
  203. package/src/ui/styled/community/CommunityModerationState.tsx +127 -0
  204. package/src/ui/styled/community/CommunityModeratorMenu.tsx +317 -0
  205. package/src/ui/styled/community/CommunityNotifications.tsx +52 -6
  206. package/src/ui/styled/community/CommunityPollCard.tsx +250 -0
  207. package/src/ui/styled/community/CommunityPostCard.tsx +110 -34
  208. package/src/ui/styled/community/CommunityPostDetail.tsx +157 -71
  209. package/src/ui/styled/community/CommunityRichText.tsx +177 -32
  210. package/src/ui/styled/community/CommunitySpaces.tsx +49 -14
  211. package/src/ui/styled/community/_tests/CommunityAgent.spec.tsx +571 -0
  212. package/src/ui/styled/community/_tests/CommunityComposer.spec.tsx +4 -1
  213. package/src/ui/styled/community/_tests/CommunityLinkPreview.spec.tsx +167 -0
  214. package/src/ui/styled/community/_tests/CommunityMembers.spec.tsx +137 -0
  215. package/src/ui/styled/community/_tests/CommunityModeration.spec.tsx +291 -0
  216. package/src/ui/styled/community/_tests/CommunityPoll.spec.tsx +386 -0
  217. package/src/ui/styled/community/_tests/CommunityPostDetail.spec.tsx +4 -1
  218. package/src/ui/styled/community/index.ts +37 -2
  219. package/src/ui/styled/forge-utilities.css +253 -0
  220. package/src/ui/styled/members/CallRecordingActions.tsx +182 -0
  221. package/src/ui/styled/members/CallTranscriptViewer.tsx +424 -0
  222. package/src/ui/styled/members/MemberCallsCalendar.tsx +41 -54
  223. package/src/ui/styled/members/MemberHomeLayout.tsx +327 -29
  224. package/src/ui/styled/members/MemberPostCard.tsx +9 -5
  225. package/src/ui/styled/members/MemberPostsFeed.tsx +3 -2
  226. package/src/ui/styled/members/PostCollectionDetail.tsx +18 -10
  227. package/src/ui/styled/members/PostCollectionsGrid.tsx +24 -10
  228. package/src/ui/styled/members/_tests/MemberCallsCalendar.spec.tsx +167 -8
  229. package/src/ui/styled/members/callTranscriptText.ts +67 -0
  230. package/src/ui/styled/members/index.ts +8 -0
  231. package/src/ui/styled/portal/PortalAccountSection.tsx +61 -0
  232. package/src/ui/styled/portal/PortalActivity.tsx +167 -0
  233. package/src/ui/styled/portal/PortalBilling.tsx +197 -0
  234. package/src/ui/styled/portal/PortalFiles.tsx +279 -0
  235. package/src/ui/styled/portal/PortalHome.tsx +401 -0
  236. package/src/ui/styled/portal/PortalMessages.tsx +300 -0
  237. package/src/ui/styled/portal/PortalPreviewBanner.tsx +75 -0
  238. package/src/ui/styled/portal/PortalProjects.tsx +407 -0
  239. package/src/ui/styled/portal/PortalShell.tsx +397 -0
  240. package/src/ui/styled/portal/_tests/PortalAppHome.spec.tsx +90 -0
  241. package/src/ui/styled/portal/_tests/PortalFiles.spec.tsx +159 -0
  242. package/src/ui/styled/portal/_tests/PortalHome.spec.tsx +148 -0
  243. package/src/ui/styled/portal/_tests/PortalMessages.spec.tsx +168 -0
  244. package/src/ui/styled/portal/_tests/PortalPreviewBanner.spec.tsx +132 -0
  245. package/src/ui/styled/portal/_tests/portalNav.spec.ts +123 -0
  246. package/src/ui/styled/portal/index.ts +13 -0
  247. package/src/ui/styled/portal/portalNav.ts +260 -0
  248. package/src/ui/styled/portal/shared.tsx +206 -0
  249. package/src/ui/styled/work/WorkInviteAccept.tsx +228 -168
  250. package/src/ui/styled/work/WorkPortalProjects.tsx +68 -120
  251. package/src/ui/styled/work/WorkProjectDocuments.tsx +99 -128
  252. package/src/ui/styled/work/WorkProjectInvoices.tsx +91 -86
  253. package/src/ui/styled/work/WorkProjectReport.tsx +11 -17
  254. package/src/ui/styled/work/WorkReportBody.tsx +223 -248
  255. package/src/ui/styled/work/WorkTaskAttachments.tsx +23 -29
  256. package/src/ui/styled/work/WorkTaskComments.tsx +75 -102
  257. package/src/ui/styled/work/WorkTaskDetail.tsx +48 -59
  258. package/src/ui/styled/work/WorkTokenReport.tsx +8 -16
  259. package/src/ui/styled/work/index.ts +1 -1
  260. package/src/utils/_tests/metaEvents.spec.ts +264 -0
  261. package/src/utils/_tests/safeRedirect.spec.ts +2 -2
  262. package/src/utils/landing.ts +19 -6
  263. package/src/utils/metaEvents.ts +302 -0
  264. package/src/ui/headless/membership/MembershipGate.tsx +0 -44
  265. package/src/ui/styled/_tests/AccountDashboardCommunity.spec.tsx +0 -149
  266. package/src/ui/styled/_tests/AccountDashboardTrial.spec.tsx +0 -105
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tribe-nest/forge",
3
- "version": "3.59.0",
3
+ "version": "3.61.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -35,6 +35,7 @@
35
35
  "@tribe-nest/media-protocol": "^1.0.0",
36
36
  "@tribe-nest/seating-core": "^1.0.0",
37
37
  "axios": "^1.7.9",
38
+ "cobe": "2.0.1",
38
39
  "libphonenumber-js": "^1.13.13",
39
40
  "lucide-react": "^0.510.0",
40
41
  "react-hook-form": "^7.60.0",
@@ -0,0 +1,28 @@
1
+ import type { ReactNode } from "react";
2
+ import { CommunityProvider } from "../contexts/CommunityContext";
3
+ import type { PublicCommunity } from "../data/queries/useCommunities";
4
+
5
+ /**
6
+ * A community for specs. Member-content hooks throw outside a
7
+ * `<CommunityProvider>`, so a spec that renders one wraps it in `InCommunity`.
8
+ * `initialData` makes the id known on the first render, with no request.
9
+ */
10
+ export const TEST_COMMUNITY: PublicCommunity = {
11
+ id: "11111111-1111-4111-8111-111111111111",
12
+ name: "Fan Club",
13
+ slug: "fan-club",
14
+ description: "Where the fans are.",
15
+ coverUrl: null,
16
+ logoUrl: null,
17
+ category: "Music",
18
+ tags: [],
19
+ memberCount: 42,
20
+ };
21
+
22
+ export function InCommunity({ children, community = TEST_COMMUNITY }: { children: ReactNode; community?: PublicCommunity }) {
23
+ return (
24
+ <CommunityProvider community={community.slug} initialData={community}>
25
+ {children}
26
+ </CommunityProvider>
27
+ );
28
+ }
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import type { IBookingFee, ProductDeliveryType } from "../types/models";
3
- import { useCallback, useContext, useMemo, useState } from "react";
3
+ import { useCallback, useContext, useMemo, useRef, useState } from "react";
4
+ import { trackMeta } from "../utils/metaEvents";
4
5
  import { createSharedContext } from "../runtime/registry";
5
6
  import type { ReactNode } from "react";
6
7
  import { useEffect } from "react";
@@ -333,6 +334,15 @@ export function CartProvider({ children }: { children: ReactNode }) {
333
334
  return false;
334
335
  }
335
336
  setCartItems((prev) => [...prev, next]);
337
+ // Meta AddToCart, only for a line that is new: a duplicate updates the
338
+ // line and adds nothing.
339
+ trackMeta("AddToCart", {
340
+ content_ids: [item.productId],
341
+ content_name: item.title,
342
+ content_type: "product",
343
+ value: Number(item.price) * (item.quantity || 1),
344
+ num_items: item.quantity || 1,
345
+ });
336
346
  return true;
337
347
  },
338
348
  [cartItems],
@@ -392,7 +402,30 @@ export function CartProvider({ children }: { children: ReactNode }) {
392
402
  }
393
403
  }, []);
394
404
 
405
+ // The latest selection, read by `setTickets` to tell an increase (an add,
406
+ // for Meta) from a decrease, outside the state updater (which must stay pure).
407
+ const ticketItemsRef = useRef(ticketItems);
408
+ ticketItemsRef.current = ticketItems;
409
+
395
410
  const setTickets = useCallback((item: TicketCartItem) => {
411
+ const before = ticketItemsRef.current.find((t) => t.eventId === item.eventId);
412
+ let added = 0;
413
+ let addedValue = 0;
414
+ for (const [ticketId, qty] of Object.entries(item.tickets)) {
415
+ const increase = qty - (before?.tickets[ticketId] ?? 0);
416
+ if (increase <= 0) continue;
417
+ added += increase;
418
+ addedValue += increase * Number(item.ticketMeta[ticketId]?.price ?? 0);
419
+ }
420
+ if (added > 0) {
421
+ trackMeta("AddToCart", {
422
+ content_ids: [item.eventId],
423
+ content_name: item.eventTitle,
424
+ content_type: "product",
425
+ value: Number.isFinite(addedValue) ? addedValue : undefined,
426
+ num_items: added,
427
+ });
428
+ }
396
429
  setTicketItems((prev) => [...prev.filter((t) => t.eventId !== item.eventId), item]);
397
430
  setCartOpen(true);
398
431
  }, []);
@@ -0,0 +1,192 @@
1
+ "use client";
2
+ import { useContext, useEffect, useMemo, type ReactNode } from "react";
3
+ import { useQueryClient } from "@tanstack/react-query";
4
+ import { createSharedContext } from "../runtime/registry";
5
+ import { useForge } from "../provider/ForgeProvider";
6
+ import {
7
+ communitiesKeys,
8
+ usePublicCommunity,
9
+ useViewerAccountId,
10
+ type CommunityDetail,
11
+ type PublicCommunity,
12
+ } from "../data/queries/useCommunities";
13
+
14
+ /**
15
+ * Which community the surrounding UI is about.
16
+ *
17
+ * A profile can run several communities, each fully isolated (members, tiers,
18
+ * feed, spaces, collections, chat, calls, identity). So every member-content
19
+ * hook has to be told WHICH one, and it is told by this provider rather than by
20
+ * an argument threaded through every component: the hosted pages fill it from
21
+ * the `$slug` in the URL, and a creator's own page wraps a section in it.
22
+ *
23
+ * <CommunityProvider community="fan-club">
24
+ * <CommunityHome />
25
+ * </CommunityProvider>
26
+ *
27
+ * A member-content hook used outside it THROWS. That is deliberate: guessing
28
+ * ("the first community", "any membership on the profile") is exactly the
29
+ * cross-community leak this model exists to prevent, and a developer error at
30
+ * build time is far cheaper than a fan reading the wrong community's feed.
31
+ *
32
+ * The context is shared across bundles (`createSharedContext`), so a provider a
33
+ * site mounts is the one the hosted pages bundle reads, and vice versa.
34
+ */
35
+
36
+ export interface CommunityContextValue {
37
+ /** The community, once loaded. */
38
+ community: PublicCommunity | undefined;
39
+ /** Its id. Known before the community loads when the provider was given an id. */
40
+ communityId: string | undefined;
41
+ /** Its slug. Known before the community loads when the provider was given a slug. */
42
+ slug: string | undefined;
43
+ isLoading: boolean;
44
+ /** True when the server answered that no such live community exists. */
45
+ notFound: boolean;
46
+ /** What the provider was given: a slug or an id. */
47
+ ref: string;
48
+ }
49
+
50
+ const CommunityContext = createSharedContext<CommunityContextValue | null>("community", null);
51
+
52
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
53
+
54
+ /** True when a community reference is an id rather than a slug. */
55
+ export function isCommunityId(ref: string): boolean {
56
+ return UUID_RE.test(ref);
57
+ }
58
+
59
+ export interface CommunityProviderProps {
60
+ /** The community's slug (as in `/i/communities/<slug>`) or its id. */
61
+ community: string;
62
+ /**
63
+ * The community when the host already has it (SSR, a list it came from).
64
+ * Seeds the cache so nothing below waits on a request. Optional.
65
+ */
66
+ initialData?: PublicCommunity;
67
+ children: ReactNode;
68
+ }
69
+
70
+ /**
71
+ * Supplies one community to every hook and component below it. See the file
72
+ * docblock for why a member-content hook refuses to run without one.
73
+ */
74
+ export function CommunityProvider({ community: ref, initialData, children }: CommunityProviderProps) {
75
+ const { profileId } = useForge();
76
+ const queryClient = useQueryClient();
77
+ const accountId = useViewerAccountId();
78
+
79
+ // Seed the detail cache from `initialData` so `community` is there on the
80
+ // first render. The seed carries no tiers or viewer, so the real read still
81
+ // runs and replaces it.
82
+ useEffect(() => {
83
+ if (!initialData || !profileId) return;
84
+ const key = communitiesKeys.detail(profileId, ref, {}, accountId);
85
+ if (!queryClient.getQueryData(key)) {
86
+ queryClient.setQueryData<CommunityDetail>(
87
+ key,
88
+ { community: initialData, tiers: [], viewer: EMPTY_VIEWER, invite: null },
89
+ { updatedAt: 0 },
90
+ );
91
+ }
92
+ }, [initialData, profileId, ref, accountId, queryClient]);
93
+
94
+ const detail = usePublicCommunity(ref);
95
+ const community = detail.data?.community ?? initialData;
96
+ const byId = isCommunityId(ref);
97
+
98
+ const value = useMemo<CommunityContextValue>(
99
+ () => ({
100
+ community,
101
+ communityId: community?.id ?? (byId ? ref : undefined),
102
+ slug: community?.slug ?? (byId ? undefined : ref),
103
+ isLoading: !community && detail.isLoading,
104
+ notFound: !community && (detail.error as { response?: { status?: number } } | null)?.response?.status === 404,
105
+ ref,
106
+ }),
107
+ [community, byId, ref, detail.isLoading, detail.error],
108
+ );
109
+
110
+ return <CommunityContext.Provider value={value}>{children}</CommunityContext.Provider>;
111
+ }
112
+
113
+ const EMPTY_VIEWER = {
114
+ state: "none" as const,
115
+ membership: null,
116
+ application: null,
117
+ upgradeTiers: [],
118
+ canInteract: false,
119
+ };
120
+
121
+ /** Thrown by any member-content hook used outside `<CommunityProvider>`. */
122
+ export class MissingCommunityProviderError extends Error {
123
+ constructor(hookName: string) {
124
+ super(
125
+ `${hookName} must be used inside <CommunityProvider community="<slug or id>">. ` +
126
+ "A creator can run several communities, so Forge will not guess which one this is. " +
127
+ "Wrap the page (or section) in <CommunityProvider>, or pass `community` to the component that accepts it.",
128
+ );
129
+ this.name = "MissingCommunityProviderError";
130
+ }
131
+ }
132
+
133
+ /**
134
+ * The surrounding community. Throws outside `<CommunityProvider>`.
135
+ *
136
+ * `communityId` can be undefined for a moment while a slug resolves, so data
137
+ * hooks gate on it rather than on the provider alone.
138
+ */
139
+ export function useCommunityContext(): CommunityContextValue {
140
+ const ctx = useContext(CommunityContext);
141
+ if (!ctx) throw new MissingCommunityProviderError("useCommunityContext()");
142
+ return ctx;
143
+ }
144
+
145
+ /** The surrounding community, or null outside a provider. For components that work either way. */
146
+ export function useOptionalCommunityContext(): CommunityContextValue | null {
147
+ return useContext(CommunityContext);
148
+ }
149
+
150
+ /**
151
+ * What a member-content data hook needs: the API client, the profile and the
152
+ * community. Throws `MissingCommunityProviderError` naming the calling hook.
153
+ */
154
+ export function useCommunityScope(hookName: string) {
155
+ const ctx = useContext(CommunityContext);
156
+ if (!ctx) throw new MissingCommunityProviderError(hookName);
157
+ const { client, profileId } = useForge();
158
+ return { client, profileId, communityId: ctx.communityId, slug: ctx.slug };
159
+ }
160
+
161
+ // ---- The upgrade sheet opener ----------------------------------------------------
162
+
163
+ /**
164
+ * Lets any locked item inside a community ("Unlock with {tier}") open the
165
+ * community's ONE upgrade sheet, wherever it is drawn. `CommunityHome` provides
166
+ * it; a custom member area can provide its own. Absent, locked items fall back
167
+ * to a link to the community's About page with the tier preselected.
168
+ */
169
+ export interface CommunityUpgradeOpener {
170
+ /** Open the upgrade sheet, optionally preselecting the tier that unlocks the item. */
171
+ openUpgrade: (tierId?: string | null) => void;
172
+ }
173
+
174
+ const CommunityUpgradeOpenerContext = createSharedContext<CommunityUpgradeOpener | null>(
175
+ "communityUpgradeOpener",
176
+ null,
177
+ );
178
+
179
+ export function CommunityUpgradeOpenerProvider({
180
+ value,
181
+ children,
182
+ }: {
183
+ value: CommunityUpgradeOpener;
184
+ children: ReactNode;
185
+ }) {
186
+ return <CommunityUpgradeOpenerContext.Provider value={value}>{children}</CommunityUpgradeOpenerContext.Provider>;
187
+ }
188
+
189
+ /** The surrounding upgrade opener, or null when no community home provides one. */
190
+ export function useCommunityUpgradeOpener(): CommunityUpgradeOpener | null {
191
+ return useContext(CommunityUpgradeOpenerContext);
192
+ }
@@ -31,7 +31,7 @@ import {
31
31
  storeTokenPair,
32
32
  } from "../client/tokenStorage";
33
33
 
34
- // Kept for compat — the canonical constants live in client/tokenStorage.ts.
34
+ // Kept for compat. The canonical constants live in client/tokenStorage.ts.
35
35
  export const ACCESS_TOKEN_KEY = PUBLIC_ACCESS_TOKEN_KEY;
36
36
  export { PUBLIC_ACCESS_TOKEN_KEY };
37
37
 
@@ -129,7 +129,7 @@ function PublicAuthProvider({ children }: AuthProviderProps) {
129
129
  }, []);
130
130
 
131
131
  const { data: currencies } = useQuery({
132
- // profileId scopes the cache — supportedCurrencies is narrowed to the tenant's
132
+ // profileId scopes the cache: supportedCurrencies is narrowed to the tenant's
133
133
  // connected account (must match useCurrencies' key so they share one fetch).
134
134
  queryKey: ["public-currencies", profileId],
135
135
  queryFn: async () => {
@@ -241,6 +241,17 @@ function PublicAuthProvider({ children }: AuthProviderProps) {
241
241
  }
242
242
  };
243
243
 
244
+ /**
245
+ * Sign in with a session another call already minted (the email-first flow's
246
+ * `signed_in` answers). The same three steps every sign-in here ends with:
247
+ * store the token pair, hand the access token to the client, record the user.
248
+ */
249
+ const adoptSession = (session: { token: string; refreshToken?: string; account: PublicAuthUser }) => {
250
+ storeTokenPair(PUBLIC_ACCESS_TOKEN_KEY, PUBLIC_REFRESH_TOKEN_KEY, session.token, session.refreshToken);
251
+ setToken(session.token);
252
+ dispatch({ type: Types.Login, payload: { user: session.account } });
253
+ };
254
+
244
255
  const resendLoginCode = async (email: string) => {
245
256
  // /public/sessions requires the profile context.
246
257
  await client.post("/public/sessions/resend-code", { email, profileId });
@@ -303,7 +314,7 @@ function PublicAuthProvider({ children }: AuthProviderProps) {
303
314
  ...data,
304
315
  });
305
316
  // No token: signup issues no session until the emailed code is redeemed
306
- // (see PublicAuthChallenge). Keep the caller's data — `verifyRegistration`
317
+ // (see PublicAuthChallenge). Keep the caller's data: `verifyRegistration`
307
318
  // needs the coupon/consent flags again to finish the job.
308
319
  const { email } = response.data as { status: "verify_email"; email: string };
309
320
  dispatch({ type: Types.Loading, payload: { isLoading: false } });
@@ -372,6 +383,7 @@ function PublicAuthProvider({ children }: AuthProviderProps) {
372
383
  validateLoginCredentials,
373
384
  verifyLoginCode,
374
385
  resendLoginCode,
386
+ adoptSession,
375
387
  logout,
376
388
  register,
377
389
  verifyRegistration,
@@ -0,0 +1,130 @@
1
+ // @vitest-environment jsdom
2
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
3
+ import { act, renderHook } from "@testing-library/react";
4
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
5
+ import type { ReactNode } from "react";
6
+
7
+ /**
8
+ * Every request that creates a visitor record carries `metaSignals`, which is
9
+ * what lets a server event sent later (from a worker, with no request) still
10
+ * match the visitor. A create response that carries `metaEvent` means the
11
+ * server queued that event: the browser fires only its pixel twin.
12
+ */
13
+
14
+ const post = vi.fn();
15
+
16
+ vi.mock("../../../provider/ForgeProvider", () => ({
17
+ useForge: () => ({ client: { post }, profileId: "profile-1" }),
18
+ }));
19
+
20
+ import { useSubmitPublicForm } from "../useForms";
21
+ import { useJoinEmailList } from "../useLeadMagnet";
22
+ import { useCreateOrder } from "../useOrders";
23
+ import { useUpdateCoachingBooking } from "../useCoachingAvailability";
24
+ import { resetMetaPolicy, setMetaPolicy } from "../../../utils/metaEvents";
25
+ import { saveCookieConsent, GRANT_ALL, DENY_ALL } from "../../../utils/cookieConsent";
26
+
27
+ const wrapper = ({ children }: { children: ReactNode }) => {
28
+ const queryClient = new QueryClient({ defaultOptions: { mutations: { retry: false } } });
29
+ return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
30
+ };
31
+
32
+ let fbq: ReturnType<typeof vi.fn>;
33
+ let fetchSpy: ReturnType<typeof vi.fn>;
34
+ const pixel = () => fbq.mock.calls.filter((c) => c[0] === "trackSingle");
35
+
36
+ beforeEach(() => {
37
+ post.mockReset();
38
+ resetMetaPolicy();
39
+ window.localStorage.clear();
40
+ document.cookie = "_fbp=fb.1.1.2; path=/";
41
+ fbq = vi.fn();
42
+ (window as unknown as { fbq: unknown }).fbq = fbq;
43
+ fetchSpy = vi.fn().mockResolvedValue({ ok: true });
44
+ vi.stubGlobal("fetch", fetchSpy);
45
+ window.history.replaceState({}, "", "/contact");
46
+ setMetaPolicy({
47
+ pixelId: "555",
48
+ enabled: true,
49
+ requireConsent: true,
50
+ apiUrl: "https://api.test",
51
+ subdomain: "artist",
52
+ currency: "USD",
53
+ resolved: true,
54
+ });
55
+ });
56
+
57
+ afterEach(() => {
58
+ vi.unstubAllGlobals();
59
+ resetMetaPolicy();
60
+ document.cookie = "_fbp=; path=/; max-age=0";
61
+ });
62
+
63
+ describe("useSubmitPublicForm", () => {
64
+ it("attaches metaSignals and fires the pixel copy of the server's Lead", async () => {
65
+ saveCookieConsent(GRANT_ALL);
66
+ post.mockResolvedValue({ data: { id: "sub-1", metaEvent: { name: "Lead", eventId: "sub-1", customData: { content_name: "Contact" } } } });
67
+ const { result } = renderHook(() => useSubmitPublicForm("form-1"), { wrapper });
68
+
69
+ await act(async () => {
70
+ await result.current.mutateAsync({ answers: [], respondentEmail: "a@b.test" });
71
+ });
72
+
73
+ expect(post).toHaveBeenCalledWith("/public/forms/form-1/submit", {
74
+ answers: [],
75
+ respondentEmail: "a@b.test",
76
+ idempotencyKey: expect.any(String),
77
+ metaSignals: { pageUrl: "http://localhost:3000/contact", marketingConsent: true, fbp: "fb.1.1.2" },
78
+ });
79
+ expect(pixel()).toEqual([["trackSingle", "555", "Lead", { content_name: "Contact" }, { eventID: "sub-1" }]]);
80
+ // The server already queued its copy.
81
+ expect(fetchSpy).not.toHaveBeenCalled();
82
+ });
83
+
84
+ it("sends consent false and no Meta ids when marketing was declined, and fires no pixel", async () => {
85
+ saveCookieConsent(DENY_ALL);
86
+ post.mockResolvedValue({ data: { id: "sub-2", metaEvent: { name: "Lead", eventId: "sub-2" } } });
87
+ const { result } = renderHook(() => useSubmitPublicForm("form-1"), { wrapper });
88
+
89
+ await act(async () => {
90
+ await result.current.mutateAsync([]);
91
+ });
92
+
93
+ expect(post.mock.calls[0][1].metaSignals).toEqual({ pageUrl: "http://localhost:3000/contact", marketingConsent: false });
94
+ expect(pixel()).toEqual([]);
95
+ });
96
+ });
97
+
98
+ describe("other create requests", () => {
99
+ beforeEach(() => saveCookieConsent(GRANT_ALL));
100
+
101
+ it("useJoinEmailList attaches metaSignals and fires CompleteRegistration from the response", async () => {
102
+ post.mockResolvedValue({ data: { metaEvent: { name: "CompleteRegistration", eventId: "subscriber-1" } } });
103
+ const { result } = renderHook(() => useJoinEmailList(), { wrapper });
104
+ await act(async () => {
105
+ await result.current.mutateAsync({ email: "a@b.test", emailListId: "list-1" });
106
+ });
107
+ expect(post.mock.calls[0][1]).toMatchObject({ email: "a@b.test", profileId: "profile-1", metaSignals: { marketingConsent: true } });
108
+ expect(pixel()).toEqual([["trackSingle", "555", "CompleteRegistration", {}, { eventID: "subscriber-1" }]]);
109
+ });
110
+
111
+ it("useUpdateCoachingBooking fires Schedule from the response", async () => {
112
+ post.mockResolvedValue({ data: { bookingId: "b-1", isConfirmed: true, metaEvent: { name: "Schedule", eventId: "b-1" } } });
113
+ const { result } = renderHook(() => useUpdateCoachingBooking("coach-1"), { wrapper });
114
+ await act(async () => {
115
+ await result.current.mutateAsync({ bookingId: "b-1", email: "a@b.test", firstName: "A", lastName: "B", confirmIfFree: true });
116
+ });
117
+ expect(post.mock.calls[0][1].metaSignals).toMatchObject({ marketingConsent: true, fbp: "fb.1.1.2" });
118
+ expect(pixel()).toEqual([["trackSingle", "555", "Schedule", {}, { eventID: "b-1" }]]);
119
+ });
120
+
121
+ it("useCreateOrder attaches metaSignals and fires nothing itself", async () => {
122
+ post.mockResolvedValue({ data: { orderId: "o-1", totalAmount: 10, subTotal: 10 } });
123
+ const { result } = renderHook(() => useCreateOrder(), { wrapper });
124
+ await act(async () => {
125
+ await result.current.mutateAsync({ amount: 10, email: "a@b.test", cartItems: [] });
126
+ });
127
+ expect(post.mock.calls[0][1]).toMatchObject({ amount: 10, profileId: "profile-1", metaSignals: { marketingConsent: true } });
128
+ expect(pixel()).toEqual([]);
129
+ });
130
+ });
@@ -1,6 +1,6 @@
1
1
  import type { IPublicOrder, IPublicPost, PaginatedData } from "../../types/models";
2
2
  import { useForge } from "../../provider/ForgeProvider";
3
- import { useMutation, useQuery } from "@tanstack/react-query";
3
+ import { useInfiniteQuery, useMutation, useQuery } from "@tanstack/react-query";
4
4
 
5
5
  /** Orders placed by the signed-in member on this profile. */
6
6
  export function useUserOrders(accountId?: string) {
@@ -40,6 +40,11 @@ export type NotificationPreferences = {
40
40
  };
41
41
 
42
42
  /** The member's notification-type preferences for this profile. */
43
+ /**
44
+ * @deprecated Reads `newPosts` / `chatMessages`, which the server never sends:
45
+ * the preferences endpoint speaks the `"<type>.<channel>"` matrix. Use
46
+ * `useNotificationMatrix` and `useUpdateNotificationMatrix`.
47
+ */
43
48
  export function useNotificationPreferences(enabled = true) {
44
49
  const { client, profileId } = useForge();
45
50
 
@@ -55,7 +60,11 @@ export function useNotificationPreferences(enabled = true) {
55
60
  });
56
61
  }
57
62
 
58
- /** Update one notification-type preference. */
63
+ /**
64
+ * @deprecated Sends `{ newPosts: false }`, which the server drops without an
65
+ * error (it accepts only `"<type>.<channel>"` keys), so nothing is ever saved.
66
+ * Use `useUpdateNotificationMatrix`.
67
+ */
59
68
  export function useUpdateNotificationPreference() {
60
69
  const { client, profileId } = useForge();
61
70
 
@@ -69,3 +78,25 @@ export function useUpdateNotificationPreference() {
69
78
  },
70
79
  });
71
80
  }
81
+
82
+ /**
83
+ * Every order, a page at a time, for a list with a "Show more" button.
84
+ *
85
+ * `useUserOrders` sends no page, so the server's default of 10 applies and a
86
+ * buyer with more orders than that never saw the older ones. `/public/orders`
87
+ * returns a bare array with no total, so a FULL page is the only sign that
88
+ * another may follow; an exact multiple costs one extra, empty request.
89
+ */
90
+ export function useUserOrdersPaged(accountId?: string, limit = 20) {
91
+ const { client, profileId } = useForge();
92
+ return useInfiniteQuery({
93
+ queryKey: ["user-orders", accountId, profileId, "paged", limit],
94
+ initialPageParam: 1,
95
+ queryFn: async ({ pageParam }) => {
96
+ const res = await client.get("/public/orders", { params: { profileId, page: pageParam, limit } });
97
+ return res.data as IPublicOrder[];
98
+ },
99
+ getNextPageParam: (last, pages) => (last.length === limit ? pages.length + 1 : undefined),
100
+ enabled: !!accountId && !!profileId && !!client,
101
+ });
102
+ }
@@ -5,7 +5,7 @@ import { useForge } from "../../provider/ForgeProvider";
5
5
  * server-side CAPI twin, and for correlating first-party events. */
6
6
  export function newEventId(): string {
7
7
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
8
- // Fallback for older runtimes — not cryptographically strong, only needs to be
8
+ // Fallback for older runtimes: not cryptographically strong, only needs to be
9
9
  // unique enough to pair a pixel + CAPI event.
10
10
  return `evt_${Date.now().toString(36)}_${Math.abs(hashString(String(performance?.now?.() ?? Date.now()))).toString(36)}`;
11
11
  }
@@ -33,7 +33,7 @@ export function getAnalyticsSessionId(subdomain: string): string | null {
33
33
  }
34
34
  return sessionId;
35
35
  } catch {
36
- // Private mode / storage disabled — fall back to an ephemeral per-load id.
36
+ // Private mode / storage disabled: fall back to an ephemeral per-load id.
37
37
  return `${subdomain}_${Date.now()}`;
38
38
  }
39
39
  }
@@ -44,7 +44,7 @@ export interface TrackEventOptions {
44
44
  /**
45
45
  * Store a session id in `sessionStorage` and send it with events. Default
46
46
  * `true`. Set `false` for cookieless, Plausible-style analytics: nothing is
47
- * written to the device and no id is sent — the backend derives a rotating
47
+ * written to the device and no id is sent; the backend derives a rotating
48
48
  * per-day visitor hash instead, so the "visits" metric still works without
49
49
  * needing storage consent.
50
50
  */
@@ -53,7 +53,7 @@ export interface TrackEventOptions {
53
53
 
54
54
  /**
55
55
  * The first-party website-analytics primitive. Returns a `track(eventType,
56
- * eventData)` function that posts to `POST /public/websites/track-event` — the
56
+ * eventData)` function that posts to `POST /public/websites/track-event`, the
57
57
  * same feed the platform's admin analytics dashboard reads (visits, page views,
58
58
  * clicks, geo). The backend enriches each event with geo-IP + user agent.
59
59
  *
@@ -68,7 +68,7 @@ export function useTrackEvent(opts?: TrackEventOptions) {
68
68
 
69
69
  const buildPayload = useCallback(
70
70
  (eventData: Record<string, unknown>) => {
71
- // Cookieless mode omits the sessionId entirely — the backend derives a
71
+ // Cookieless mode omits the sessionId entirely; the backend derives a
72
72
  // rotating per-day visitor hash so nothing touches the device.
73
73
  const sessionId = storeSession && subdomain ? getAnalyticsSessionId(subdomain) : undefined;
74
74
  const payload: Record<string, unknown> = { ...eventData };
@@ -133,5 +133,5 @@ export function useTrackEvent(opts?: TrackEventOptions) {
133
133
  [apiUrl, subdomain, buildPayload],
134
134
  );
135
135
 
136
- return { track, trackBeacon };
136
+ return { track, trackBeacon, apiUrl, subdomain };
137
137
  }
@@ -22,7 +22,7 @@ type ReactionRow = { emoji: string; count: number; reactedByMe: boolean };
22
22
  type MessageWithReactions = ChatMessage & { reactions?: ReactionRow[] };
23
23
 
24
24
  const senderName = (message: MessageWithReactions): string =>
25
- [message.senderFirstName, message.senderLastName].filter(Boolean).join(" ").trim() || message.senderAccountId;
25
+ [message.senderFirstName, message.senderLastName].filter(Boolean).join(" ").trim() || (message.senderAccountId ?? "");
26
26
 
27
27
  export function useCallChat(callId: string | undefined): CallStageChat | undefined {
28
28
  const { client, profileId } = useForge();
@@ -145,7 +145,7 @@ export function useCallChat(callId: string | undefined): CallStageChat | undefin
145
145
  id: message.id,
146
146
  content: message.content,
147
147
  senderName: senderName(message),
148
- senderKey: message.senderAccountId,
148
+ senderKey: message.senderAccountId ?? message.senderAgentId ?? "",
149
149
  at: message.createdAt,
150
150
  reactions: message.reactions ?? [],
151
151
  }));