@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/src/index.ts CHANGED
@@ -1,8 +1,8 @@
1
- // Forge — the headless React SDK for TribeNest creator sites.
1
+ // Forge: the headless React SDK for TribeNest creator sites.
2
2
  //
3
3
  // Phase 1 (the spine): client factory, root provider, core types.
4
4
  // Subsequent phases add providers (auth/cart/audio), data hooks, flow hooks,
5
- // headless blocks, and SEO — see docs/code-websites-initiative.md.
5
+ // headless blocks, and SEO: see docs/code-websites-initiative.md.
6
6
 
7
7
  export { createForgeClient, getForgeWebSocketUrl } from "./client/createForgeClient";
8
8
  export type { ForgeClientOptions } from "./client/createForgeClient";
@@ -43,12 +43,12 @@ export {
43
43
  // All domain types + enums/consts (models) and WebPage.
44
44
  export * from "./types";
45
45
 
46
- // Providers (Phase 2 — moving from frontend-shared into Forge)
46
+ // Providers (Phase 2: moving from frontend-shared into Forge)
47
47
  export { AudioPlayerProvider, useAudioPlayer } from "./contexts/AudioPlayerContext";
48
48
  export type { AudioTrack } from "./contexts/AudioPlayerContext";
49
49
  export { CartProvider, useCart } from "./contexts/CartContext";
50
50
  // Variant selection, shared with the Craft themes in frontend-shared the same
51
- // way the cart and audio player are — so a code site and a Craft site cannot
51
+ // way the cart and audio player are: so a code site and a Craft site cannot
52
52
  // disagree about which version a buyer picked.
53
53
  export { useVariantSelection } from "./ui/headless/useVariantSelection";
54
54
  // Which version a cart line is. Exported from the package root because the
@@ -66,7 +66,7 @@ export {
66
66
  export type { AuthActions } from "./contexts/PublicAuthContext";
67
67
  export { PUBLIC_REFRESH_TOKEN_KEY, APP_REFRESH_TOKEN_KEY } from "./client/tokenStorage";
68
68
  export { safeRedirectPath } from "./utils/safeRedirect";
69
- // Ticket-confirmation copy decision — shared so the Forge block and the client
69
+ // Ticket-confirmation copy decision: shared so the Forge block and the client
70
70
  // app's finalise page can never tell a refunded buyer two different stories.
71
71
  export {
72
72
  getTicketOrderOutcome,
@@ -89,7 +89,7 @@ export {
89
89
  } from "./contexts/AppAuthContext";
90
90
  export { useAppAdminGuard, type AppAdminGuardResult } from "./contexts/useAppAdminGuard";
91
91
 
92
- // Data hooks (React Query) — Phase 3
92
+ // Data hooks (React Query): Phase 3
93
93
  export * from "./data/queries/useProducts";
94
94
  export * from "./data/queries/useMembership";
95
95
  export * from "./data/queries/useCourses";
@@ -138,12 +138,16 @@ export * from "./data/queries/useEngagement";
138
138
  export * from "./data/queries/usePaymentFlow";
139
139
  export * from "./data/queries/useOrders";
140
140
  export * from "./data/queries/useAuthActions";
141
+ // Email-first sign-in (`/public/sessions/email/*`): start, verify, complete.
142
+ export * from "./data/queries/useEmailSignIn";
141
143
  export * from "./data/queries/useCoupons";
142
144
  export * from "./data/queries/useAccountSettings";
143
145
  export * from "./data/queries/useDonations";
144
146
  export * from "./data/queries/useOffers";
145
147
  export * from "./data/queries/useShipping";
146
148
  export * from "./data/queries/useWebsite";
149
+ // The site app's settings for this host, and the Home rule (`useAppHomeHref`).
150
+ export * from "./data/queries/useSiteApp";
147
151
  export * from "./data/queries/useWebsiteAgent";
148
152
  export * from "./data/queries/useCurrencies";
149
153
  export * from "./data/queries/useChat";
@@ -151,19 +155,44 @@ export * from "./data/queries/useCallChat";
151
155
  export * from "./data/queries/useCallBreakouts";
152
156
  export * from "./data/queries/useAnalytics";
153
157
  export * from "./data/queries/useCommunity";
158
+ // The community AI on the member side: its info, DM, thumbs, "catch me up".
159
+ export * from "./data/queries/useCommunityAgent";
160
+ // Moderation from the site: the queue, the log, and the moderator actions.
161
+ export * from "./data/queries/useCommunityModeration";
162
+ // The community wiki as members and visitors read it.
163
+ export * from "./data/queries/useCommunityWiki";
164
+ // The member directory and map.
165
+ export * from "./data/queries/useCommunityMembers";
166
+ // The classroom tab: the courses a community's tiers include.
167
+ export * from "./data/queries/useCommunityClassroom";
168
+ // The community calendar: calls, promoted events and going / not going.
169
+ export * from "./data/queries/useCommunityCalendar";
170
+ // Communities: the list, one community, the viewer's state in it, invites,
171
+ // upgrades and the fan's memberships across communities.
172
+ export * from "./data/queries/useCommunities";
173
+ // The provider every member-content hook reads its community from.
174
+ export {
175
+ CommunityProvider,
176
+ useCommunityContext,
177
+ useOptionalCommunityContext,
178
+ MissingCommunityProviderError,
179
+ type CommunityContextValue,
180
+ type CommunityProviderProps,
181
+ } from "./contexts/CommunityContext";
154
182
  export * from "./data/queries/useReviews";
155
183
  export * from "./data/queries/useDocuments";
156
184
  export * from "./data/queries/useNotifications";
157
185
  export * from "./data/queries/usePWA";
158
186
  export * from "./data/queries/useCertificates";
159
187
 
160
- // Content layer (Phase E) — typed/editable/localizable site content
188
+ // Content layer (Phase E): typed/editable/localizable site content
161
189
  export * from "./content";
162
190
 
163
191
  // Utils
164
192
  export * from "./utils/attribution";
165
193
  export * from "./utils/landing";
166
194
  export * from "./utils/metaPixel";
195
+ export * from "./utils/metaEvents";
167
196
  export * from "./utils/cookieConsent";
168
197
  export * from "./utils/structuredData";
169
198
  // Paystack inline checkout: the ONE implementation of the popup, exported so a
@@ -38,4 +38,40 @@ describe("internal page links", () => {
38
38
  it("carries every page exactly once", () => {
39
39
  expect(new Set(INTERNAL_PAGES).size).toBe(INTERNAL_PAGES.length);
40
40
  });
41
+
42
+ it("carries exactly the community pages, and none of the removed single-community ones", () => {
43
+ const community = INTERNAL_PAGES.filter((page) => page.startsWith("communities"));
44
+ expect(community).toEqual([
45
+ "communities",
46
+ "communities/$slug",
47
+ "communities/$slug/home",
48
+ "communities/$slug/home/calendar",
49
+ "communities/$slug/home/chat",
50
+ "communities/$slug/home/classroom",
51
+ "communities/$slug/home/collections",
52
+ "communities/$slug/home/collections/$collectionId",
53
+ "communities/$slug/home/leaderboard",
54
+ "communities/$slug/home/members",
55
+ "communities/$slug/home/moderation",
56
+ "communities/$slug/home/notifications",
57
+ "communities/$slug/home/posts",
58
+ "communities/$slug/home/profile/$accountId",
59
+ "communities/$slug/home/spaces",
60
+ "communities/$slug/home/spaces/$spaceId",
61
+ "communities/$slug/home/spaces/posts/$postId",
62
+ "communities/$slug/home/wiki",
63
+ "communities/$slug/home/wiki/$page",
64
+ ]);
65
+ const removed = INTERNAL_PAGES.filter((page) => /^(membership|members|community|chat)(\/|$)/.test(page));
66
+ expect(removed).toEqual([]);
67
+ });
68
+
69
+ it("builds community paths with the slug and recognises them", () => {
70
+ expect(internalPath("communities/$slug/home/spaces/posts/$postId", { slug: "fan-club", postId: "p1" })).toBe(
71
+ "/i/communities/fan-club/home/spaces/posts/p1",
72
+ );
73
+ expect(isInternalPage("communities/fan-club/home")).toBe(true);
74
+ expect(isInternalPage("members/spaces")).toBe(false);
75
+ expect(isInternalPage("membership/checkout")).toBe(false);
76
+ });
41
77
  });
@@ -7,9 +7,17 @@
7
7
  * for TanStack to know about. That is the reason this helper exists, and it is
8
8
  * why the build guide forbids writing an `/i/` path by hand.
9
9
  *
10
- * The page list is the CONTRACT between a site and the hosted pages, so it only
11
- * ever grows. Removing a page breaks links on sites already published, so a
12
- * retired page keeps its entry and redirects.
10
+ * The page list is the CONTRACT between a site and the hosted pages, so it
11
+ * normally only grows: removing a page breaks links on sites already published.
12
+ * The first deliberate exception is Communities (2026-09-27). A profile can now
13
+ * run several communities, so the single-community member pages (`membership`,
14
+ * `members/*`, `community/*`, `chat`) were removed with no redirects, because
15
+ * nobody used them yet and there is no one community they could redirect to.
16
+ * Every member page now lives under `communities/$slug`.
17
+ *
18
+ * The second is the client portal (2026-10-01). `work/*` and the `?tab=` form
19
+ * of `account` folded into one portal under `account/*`, with no redirects:
20
+ * every producer of those links moved in the same change.
13
21
  *
14
22
  * <Link {...internalLink("store/$slug", { slug: product.slug })}>Buy</Link>
15
23
  * navigate(internalLink("checkout"))
@@ -25,10 +33,30 @@ export const INTERNAL_ROUTE = "/i/$" as const;
25
33
  /** Every hosted page, by its path pattern. Generated from the route table. */
26
34
  export const INTERNAL_PAGES = [
27
35
  "account",
36
+ "account/activity",
37
+ "account/bookings",
38
+ "account/courses",
39
+ "account/documents",
40
+ "account/files",
41
+ "account/films",
42
+ "account/invite",
43
+ "account/invoices",
44
+ "account/memberships",
45
+ "account/messages",
46
+ "account/notifications",
47
+ "account/orders",
48
+ "account/projects",
49
+ "account/projects/$projectId",
50
+ "account/projects/$projectId/tasks/$taskId",
51
+ "account/saved",
52
+ "account/settings",
53
+ "account/tickets",
54
+ "account/time-report/$token",
55
+ "account/waitlist",
56
+ "app",
28
57
  "blog",
29
58
  "blog-category/$slug",
30
59
  "blog/$slug",
31
- "chat",
32
60
  "checkout",
33
61
  "checkout/finalise",
34
62
  "checkout/resume",
@@ -37,11 +65,25 @@ export const INTERNAL_PAGES = [
37
65
  "coaching/$slug/finalise",
38
66
  "cohorts/$cohortId",
39
67
  "cohorts/$cohortId/finalise",
40
- "community",
41
- "community/leaderboard",
42
- "community/notifications",
43
- "community/posts/$postId",
44
- "community/spaces/$spaceId",
68
+ "communities",
69
+ "communities/$slug",
70
+ "communities/$slug/home",
71
+ "communities/$slug/home/calendar",
72
+ "communities/$slug/home/chat",
73
+ "communities/$slug/home/classroom",
74
+ "communities/$slug/home/collections",
75
+ "communities/$slug/home/collections/$collectionId",
76
+ "communities/$slug/home/leaderboard",
77
+ "communities/$slug/home/members",
78
+ "communities/$slug/home/moderation",
79
+ "communities/$slug/home/notifications",
80
+ "communities/$slug/home/posts",
81
+ "communities/$slug/home/profile/$accountId",
82
+ "communities/$slug/home/spaces",
83
+ "communities/$slug/home/spaces/$spaceId",
84
+ "communities/$slug/home/spaces/posts/$postId",
85
+ "communities/$slug/home/wiki",
86
+ "communities/$slug/home/wiki/$page",
45
87
  "confirm-subscription",
46
88
  "course-access/$accessId",
47
89
  "courses",
@@ -50,6 +92,7 @@ export const INTERNAL_PAGES = [
50
92
  "courses/claim",
51
93
  "documents/$token",
52
94
  "donation",
95
+ "downloads/$token",
53
96
  "events",
54
97
  "events/$slug",
55
98
  "events/$slug/finalise",
@@ -65,19 +108,6 @@ export const INTERNAL_PAGES = [
65
108
  "live",
66
109
  "live/$broadcastId",
67
110
  "login",
68
- "members",
69
- "members/calendar",
70
- "members/chat",
71
- "members/collections",
72
- "members/collections/$slug",
73
- "members/spaces",
74
- "members/spaces/$slug",
75
- "members/spaces/leaderboard",
76
- "members/spaces/notifications",
77
- "members/spaces/profile/$accountId",
78
- "members/spaces/thread/$postId",
79
- "membership",
80
- "membership/checkout",
81
111
  "payment-link/$paymentLinkId",
82
112
  "payment-link/$paymentLinkId/finalise",
83
113
  "podcasts",
@@ -92,11 +122,6 @@ export const INTERNAL_PAGES = [
92
122
  "store/$slug",
93
123
  "tickets/claim",
94
124
  "video-calls/$callId",
95
- "work",
96
- "work/accept-invite",
97
- "work/projects/$projectId",
98
- "work/projects/$projectId/tasks/$taskId",
99
- "work/time-report/$token",
100
125
  ] as const;
101
126
 
102
127
  export type InternalPage = (typeof INTERNAL_PAGES)[number];
@@ -270,7 +270,7 @@ describe("buildWebManifest — name/short_name fallback chain", () => {
270
270
  describe("buildWebManifest — overrides (shortcuts) merge", () => {
271
271
  const overrides: BuildWebManifestOptions["overrides"] = {
272
272
  shortcuts: [
273
- { name: "Members", short_name: "Members", url: "/i/members" },
273
+ { name: "Members", short_name: "Members", url: "/i/communities" },
274
274
  { name: "Account", short_name: "Account", url: "/i/account" },
275
275
  ],
276
276
  };
@@ -168,7 +168,7 @@ describe("forgeServiceWorkerScript — push payload contract (frozen, shared wit
168
168
  title: "New message",
169
169
  body: "hey!",
170
170
  type: "chat_message",
171
- url: "/i/chat/42",
171
+ url: "/i/communities/fan-club/home/chat",
172
172
  profileId: "prof-1",
173
173
  channelId: "chan-9",
174
174
  icon: "https://cdn.tribenest.co/p/nova/icon-192.png",
@@ -181,7 +181,7 @@ describe("forgeServiceWorkerScript — push payload contract (frozen, shared wit
181
181
  body: "hey!",
182
182
  icon: "https://cdn.tribenest.co/p/nova/icon-192.png",
183
183
  tag: "chat-chan-9",
184
- data: { url: "/i/chat/42", type: "chat_message" },
184
+ data: { url: "/i/communities/fan-club/home/chat", type: "chat_message" },
185
185
  });
186
186
  });
187
187
 
@@ -229,9 +229,9 @@ describe("forgeServiceWorkerScript — notificationclick", () => {
229
229
  it("focuses an existing client matching the deep-link URL", async () => {
230
230
  const { handlers, matchAll, openWindow } = loadSw();
231
231
  const focus = vi.fn(() => ({}));
232
- matchAll.mockResolvedValueOnce([{ url: "https://nova.tribenest.co/i/chat/42", focus }]);
232
+ matchAll.mockResolvedValueOnce([{ url: "https://nova.tribenest.co/i/communities/fan-club/home/chat", focus }]);
233
233
  const close = vi.fn();
234
- const event = fakeEvent({ notification: { close, data: { url: "/i/chat/42" } } });
234
+ const event = fakeEvent({ notification: { close, data: { url: "/i/communities/fan-club/home/chat" } } });
235
235
  handlers.notificationclick!(event);
236
236
  await event.waited;
237
237
  expect(close).toHaveBeenCalledOnce();
@@ -474,6 +474,11 @@ export interface PublicAuthUser {
474
474
  updatedAt: string;
475
475
  id: string;
476
476
  membership?: Membership;
477
+ /**
478
+ * Set when this is a read-only "Preview as client" session: a creator viewing
479
+ * the portal as this person. Null (or absent on older APIs) otherwise.
480
+ */
481
+ preview?: { previewedByName: string; expiresAt: string } | null;
477
482
  }
478
483
 
479
484
  export type UpdateLocalUserPayload = Pick<PublicAuthUser, "firstName">;
@@ -550,6 +555,12 @@ export interface PublicAuthContextType extends PublicAuthState {
550
555
  validateLoginCredentials: (data: LoginInput & { profileId: string }) => Promise<string>;
551
556
  verifyLoginCode: (email: string, code: string, profileId: string) => Promise<{ smartLinkPath: string | undefined }>;
552
557
  resendLoginCode: (email: string) => Promise<void>;
558
+ /**
559
+ * Sign in with a session another call already minted, e.g. the email-first
560
+ * flow's `signed_in` answer: stores the token pair exactly as the code
561
+ * sign-ins do and sets the user.
562
+ */
563
+ adoptSession: (session: { token: string; refreshToken?: string; account: PublicAuthUser }) => void;
553
564
  logout: (persist?: boolean) => Promise<void>;
554
565
  updateLocalUser: (data: UpdateLocalUserPayload) => void;
555
566
  clearErrorMessage: () => void;
@@ -1,7 +1,8 @@
1
1
  import { useEffect, useRef } from "react";
2
2
  import { useTrackEvent, newEventId } from "../../data/queries/useAnalytics";
3
3
  import { useSiteConfig } from "../../data/queries/useWebsite";
4
- import { initMetaPixelScoped, fireMetaEventSingle } from "../../utils/metaPixel";
4
+ import { initMetaPixelScoped } from "../../utils/metaPixel";
5
+ import { setMetaPolicy, trackMeta } from "../../utils/metaEvents";
5
6
  import { setCurrentPageEventId, isProfilePixelSuppressed } from "../../utils/analyticsBus";
6
7
  import { useCookieConsent } from "../headless/consent/useCookieConsent";
7
8
  import { createEngagementTracker, scrollDepthOf, type EngagementTracker } from "../../utils/engagementTracker";
@@ -17,7 +18,7 @@ export interface ForgeAnalyticsProps {
17
18
  metaPixelId?: string;
18
19
  /** Auto-track clicks on links/buttons (and any `[data-track]` element). Default `true`. */
19
20
  trackClicks?: boolean;
20
- /** Master switch — set `false` (e.g. in the editor/preview) to record nothing. Default `true`. */
21
+ /** Master switch: set `false` (e.g. in the editor/preview) to record nothing. Default `true`. */
21
22
  enabled?: boolean;
22
23
  /**
23
24
  * Honour the visitor's cookie choice (from `<CookieConsent />`). Default
@@ -43,7 +44,7 @@ export interface ForgeAnalyticsProps {
43
44
  }
44
45
 
45
46
  /**
46
- * The full resolved path the visitor is on (e.g. `/i/store/abc-123`) — never a
47
+ * The full resolved path the visitor is on (e.g. `/i/store/abc-123`), never a
47
48
  * route template like `store/:id`. This is the value the admin analytics "top
48
49
  * pages" report groups on (`event_data->>'pathname'`), so a code site's real
49
50
  * routes show up per-page instead of collapsed into a parameterised template.
@@ -53,7 +54,7 @@ function currentPathname(): string {
53
54
  return window.location.pathname;
54
55
  }
55
56
 
56
- /** Full route including query — kept alongside `pathname` for detail. */
57
+ /** Full route including query, kept alongside `pathname` for detail. */
57
58
  function currentPage(): string {
58
59
  if (typeof window === "undefined") return "";
59
60
  return window.location.pathname + window.location.search;
@@ -77,8 +78,8 @@ function isEngaged(): boolean {
77
78
  * page view as a Meta `PageView` (event-id matched for CAPI de-dup).
78
79
  *
79
80
  * Consent-aware: with `requireConsent` (default), it reads the shared cookie
80
- * consent — first-party analytics wait for `performance`, the Meta Pixel waits
81
- * for `marketing` — so pairing it with `<CookieConsent />` is GDPR-correct out
81
+ * consent (first-party analytics wait for `performance`, the Meta Pixel waits
82
+ * for `marketing`), so pairing it with `<CookieConsent />` is GDPR-correct out
82
83
  * of the box. Router-agnostic: SPA navigations are detected via the History API,
83
84
  * so it works under any router (or none). It renders nothing.
84
85
  *
@@ -93,18 +94,28 @@ export function ForgeAnalytics({
93
94
  }: ForgeAnalyticsProps) {
94
95
  const cookieless = firstPartyMode === "cookieless";
95
96
  // Resolve the pixel from the tenant's runtime settings when not explicitly
96
- // passed (so changing it needs no rebuild). An explicit prop — including `""`
97
- // to disable — always wins.
98
- const { data: siteConfig } = useSiteConfig();
97
+ // passed (so changing it needs no rebuild). An explicit prop (including `""`
98
+ // to disable) always wins.
99
+ const siteConfigQuery = useSiteConfig();
100
+ const siteConfig = siteConfigQuery.data;
99
101
  const pixelId = metaPixelId ?? siteConfig?.metaPixelId ?? undefined;
102
+ // The pixel is known once it was passed, or the site config has answered.
103
+ // Until then Meta events fired elsewhere (a detail page mounting first) wait.
104
+ const pixelResolved = metaPixelId !== undefined || !!siteConfig || !siteConfigQuery.isLoading;
100
105
  const { performance: perfConsent, marketing: mktConsent } = useCookieConsent();
101
106
  // Session storage follows performance consent. In `consent` mode first-party
102
107
  // only runs with that consent, so it always stores; in `cookieless` mode it
103
108
  // runs regardless but only stores a device session once the visitor turns
104
- // Performance on — otherwise it stays cookieless (backend derives the id).
109
+ // Performance on; otherwise it stays cookieless (backend derives the id).
105
110
  const storeSession = cookieless ? perfConsent : true;
106
- const { track, trackBeacon } = useTrackEvent({ storeSession });
107
- const lastPage = useRef<string | null>(null);
111
+ const { track, trackBeacon, apiUrl, subdomain } = useTrackEvent({ storeSession });
112
+ // The page the current event id belongs to, and the last page each half
113
+ // recorded. Tracked separately because consent (and the pixel id) arrive
114
+ // AFTER mount: with one shared "last page", the first-party view consumed the
115
+ // page and the Meta PageView for it never fired once consent loaded.
116
+ const pageView = useRef<{ page: string; eventId: string } | null>(null);
117
+ const lastFirstPartyPage = useRef<string | null>(null);
118
+ const lastMetaPage = useRef<string | null>(null);
108
119
  // Time on page. Pings go by beacon: most are sent as the page is hidden or
109
120
  // unloaded, which cancels an ordinary request.
110
121
  const beaconRef = useRef(trackBeacon);
@@ -118,13 +129,29 @@ export function ForgeAnalytics({
118
129
  }
119
130
 
120
131
  // Consent gates. The Meta Pixel always needs `marketing`. First-party analytics
121
- // needs `performance` — UNLESS it's cookieless (stores nothing without consent),
132
+ // needs `performance`, UNLESS it's cookieless (stores nothing without consent),
122
133
  // in which case it's treated as essential and runs anyway. Flipping a gate to
123
134
  // `true` (visitor accepts) re-runs the effects, recording the current page.
124
135
  const firstPartyOn = enabled && (cookieless || !requireConsent || perfConsent);
125
136
  const metaOn = enabled && !!pixelId && (!requireConsent || mktConsent);
126
137
 
127
- // Init the Meta pixel once (scoped — no auto PageView; we fire it ourselves so
138
+ // Publish the Meta policy for code outside this component (cart, checkout,
139
+ // detail pages, create mutations). Declared before the page-view effect so
140
+ // the policy is current when that effect fires.
141
+ const currency = siteConfig?.currency;
142
+ useEffect(() => {
143
+ setMetaPolicy({
144
+ pixelId: pixelId || undefined,
145
+ enabled,
146
+ requireConsent,
147
+ apiUrl,
148
+ subdomain,
149
+ currency,
150
+ resolved: pixelResolved,
151
+ });
152
+ }, [pixelId, enabled, requireConsent, apiUrl, subdomain, currency, pixelResolved]);
153
+
154
+ // Init the Meta pixel once (scoped: no auto PageView; we fire it ourselves so
128
155
  // it doesn't leak into a per-page campaign pixel).
129
156
  useEffect(() => {
130
157
  if (!metaOn || !pixelId) return;
@@ -136,18 +163,20 @@ export function ForgeAnalytics({
136
163
  // which keeps this free of any router dependency.
137
164
  useEffect(() => {
138
165
  if (typeof window === "undefined") return;
139
- if (!firstPartyOn && !metaOn) return; // nothing consented yet — wait
166
+ if (!firstPartyOn && !metaOn) return; // nothing consented yet, wait
140
167
 
141
168
  const sendPageView = () => {
142
169
  const page = currentPage();
143
- if (page === lastPage.current) return; // de-dupe repeated fires for the same URL
144
- lastPage.current = page;
170
+ if (pageView.current?.page !== page) {
171
+ pageView.current = { page, eventId: newEventId() };
172
+ // Publish the id so a per-page <PageMetaPixel/> (and its server CAPI twin)
173
+ // can dedupe against the same page view.
174
+ setCurrentPageEventId(pageView.current.eventId);
175
+ }
176
+ const eventId = pageView.current.eventId;
145
177
 
146
- const eventId = newEventId();
147
- // Publish the id so a per-page <PageMetaPixel/> (and its server CAPI twin)
148
- // can dedupe against the same page view.
149
- setCurrentPageEventId(eventId);
150
- if (firstPartyOn) {
178
+ if (firstPartyOn && lastFirstPartyPage.current !== page) {
179
+ lastFirstPartyPage.current = page;
151
180
  // Close the previous page's time before this page view is recorded, so
152
181
  // the ping lands on the page it measured.
153
182
  engagement.current?.flush();
@@ -162,14 +191,16 @@ export function ForgeAnalytics({
162
191
  eventId,
163
192
  });
164
193
  }
165
- // Fire the profile pixel to ITSELF (trackSingle), and skip it on routes a
166
- // campaign pixel has taken over (PageMetaPixel override). Deferred a tick
167
- // so a page pixel mounting on this route can register its suppression first.
168
- if (metaOn && pixelId) {
194
+ // Fire the profile pixel to ITSELF (trackSingle) plus its server copy,
195
+ // with the page view's id, and skip both on routes a campaign pixel has
196
+ // taken over (PageMetaPixel override; it sends its own server copy).
197
+ // Deferred a tick so a page pixel mounting on this route can register its
198
+ // suppression first.
199
+ if (metaOn && pixelId && lastMetaPage.current !== page) {
200
+ lastMetaPage.current = page;
169
201
  const path = currentPathname();
170
- const pixel = pixelId;
171
202
  queueMicrotask(() => {
172
- if (!isProfilePixelSuppressed(path)) fireMetaEventSingle(pixel, "PageView", {}, eventId);
203
+ if (!isProfilePixelSuppressed(path)) trackMeta("PageView", undefined, { eventId });
173
204
  });
174
205
  }
175
206
  };
@@ -232,7 +263,7 @@ export function ForgeAnalytics({
232
263
  };
233
264
  }, [firstPartyOn]);
234
265
 
235
- // Click tracking — first-party only, one delegated listener for the document.
266
+ // Click tracking: first-party only, one delegated listener for the document.
236
267
  useEffect(() => {
237
268
  if (!firstPartyOn || !trackClicks || typeof document === "undefined") return;
238
269
 
@@ -246,11 +277,10 @@ export function ForgeAnalytics({
246
277
  //
247
278
  // This listener cannot know that a component already sent a richer event
248
279
  // for the same click, so without a way to stand down, anything that tracks
249
- // its own clicks is counted twice. Worse than the double count: this
250
- // listener's payload carries no `eventId`, so the resulting CAPI `Lead`
251
- // has nothing to dedupe against, and Meta sees two conversions for one
252
- // action. Any element that fires its own event must carry
253
- // `data-track-skip`.
280
+ // its own clicks is counted twice in first-party analytics. A click is
281
+ // not a Meta event (components that turn one into a conversion fire it
282
+ // themselves, through `trackMeta` or `usePageMetaPixel`). Any element
283
+ // that reports its own click must carry `data-track-skip`.
254
284
  if (el.closest("[data-track-skip]")) return;
255
285
 
256
286
  const anchor = el.closest("a") as HTMLAnchorElement | null;
@@ -6,6 +6,12 @@ import {
6
6
  unsuppressProfilePixel,
7
7
  } from "../../utils/analyticsBus";
8
8
  import { useCookieConsent } from "../headless/consent/useCookieConsent";
9
+ import { sendPageMetaEvent, type MetaCustomData } from "../../utils/metaEvents";
10
+ import { newEventId } from "../../data/queries/useAnalytics";
11
+
12
+ function currentPathname(): string {
13
+ return typeof window === "undefined" ? "" : window.location.pathname;
14
+ }
9
15
 
10
16
  // useLayoutEffect warns during SSR; fall back to useEffect on the server.
11
17
  const useIsoLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
@@ -57,7 +63,11 @@ export function PageMetaPixel({
57
63
  if (!allowed || !pixelId || typeof window === "undefined") return;
58
64
  initMetaPixelScoped(pixelId);
59
65
  if (trackPageView) {
60
- fireMetaEventSingle(pixelId, "PageView", {}, getCurrentPageEventId() ?? undefined);
66
+ // One id for the pixel and its server copy: the page view's own, so the
67
+ // pair dedupes on Meta.
68
+ const eventId = getCurrentPageEventId() ?? newEventId();
69
+ fireMetaEventSingle(pixelId, "PageView", {}, eventId);
70
+ sendPageMetaEvent({ eventName: "PageView", eventId, pathname: currentPathname() });
61
71
  }
62
72
  }, [allowed, pixelId, trackPageView]);
63
73
 
@@ -65,12 +75,14 @@ export function PageMetaPixel({
65
75
  }
66
76
 
67
77
  /**
68
- * Imperative companion to `<PageMetaPixel/>` — returns `fire(event, data?)` to
69
- * send conversion events (e.g. `Lead` on a CTA click) to the campaign pixel,
70
- * gated on marketing consent and reusing the current page view's event id.
78
+ * Imperative companion to `<PageMetaPixel/>`. Returns `fire(event, data?, eventId?)`
79
+ * to send a conversion event (e.g. `Lead` on a CTA click) to the campaign pixel
80
+ * AND its server copy, gated on marketing consent. Both halves carry one id:
81
+ * the one passed, else a new one per call (an action is its own event, never
82
+ * the page view). Returns that id.
71
83
  *
72
84
  * const { fire } = usePageMetaPixel("123456789");
73
- * <button onClick={() => fire("Lead", { content_name: "Pre-save" })}>…</button>
85
+ * <button onClick={() => fire("Lead", { content_name: "Pre-save" })}>...</button>
74
86
  */
75
87
  export function usePageMetaPixel(pixelId: string, opts?: { requireConsent?: boolean }) {
76
88
  const requireConsent = opts?.requireConsent ?? true;
@@ -84,9 +96,12 @@ export function usePageMetaPixel(pixelId: string, opts?: { requireConsent?: bool
84
96
 
85
97
  return {
86
98
  allowed,
87
- fire: (eventName: string, data: Record<string, unknown> = {}) => {
88
- if (!allowed || !pixelId) return;
89
- fireMetaEventSingle(pixelId, eventName, data, getCurrentPageEventId() ?? undefined);
99
+ fire: (eventName: string, data: Record<string, unknown> = {}, eventId?: string): string | undefined => {
100
+ if (!allowed || !pixelId) return undefined;
101
+ const id = eventId ?? newEventId();
102
+ fireMetaEventSingle(pixelId, eventName, data, id);
103
+ sendPageMetaEvent({ eventName, eventId: id, pathname: currentPathname(), customData: data as MetaCustomData });
104
+ return id;
90
105
  },
91
106
  };
92
107
  }