@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
@@ -0,0 +1,327 @@
1
+ import { keepPreviousData, useMutation, useQuery, useQueryClient, type QueryClient } from "@tanstack/react-query";
2
+ import { useCommunityScope } from "../../contexts/CommunityContext";
3
+ import { communityKeys, type CommunityPost, type CommunityReply } from "./useCommunity";
4
+
5
+ /**
6
+ * Moderation from the site (community parity §4), on the member session.
7
+ *
8
+ * Base path `/public/community-moderation`. The server authorises every call by
9
+ * the caller's role in the community of the ROW acted on (a moderator there, or
10
+ * an owner/admin of the profile) and answers 404 otherwise, so these hooks
11
+ * never decide who may moderate. `useCommunityCanModerate` decides only whether
12
+ * the tools are shown. There is no ban here: only an admin bans, from the
13
+ * dashboard.
14
+ */
15
+
16
+ // ── Types (mirror apps/backend/src/routes/public/communityModeration) ────────
17
+
18
+ /** One open report (ModerationReport.getQueue). */
19
+ export interface CommunityModerationReport {
20
+ id: string;
21
+ profileId: string;
22
+ communityId: string | null;
23
+ entityType: "community_post" | "community_reply" | string;
24
+ entityId: string;
25
+ /** The post to open: the post itself for a post report, the reply's post for a reply report. */
26
+ postId: string | null;
27
+ reason: string | null;
28
+ reporterAccountId: string;
29
+ status: "pending" | "resolved" | "dismissed" | string;
30
+ resolvedAt: string | null;
31
+ resolvedBy: string | null;
32
+ createdAt: string;
33
+ updatedAt: string;
34
+ reporterFirstName: string | null;
35
+ reporterLastName: string | null;
36
+ /** The first 240 characters of the reported post or reply. */
37
+ contentPreview: string | null;
38
+ /** The reported content is already hidden (removed). */
39
+ contentHidden: boolean;
40
+ }
41
+
42
+ /** A post held for approval. */
43
+ export interface CommunityPendingPost {
44
+ id: string;
45
+ spaceId: string;
46
+ spaceName: string | null;
47
+ title: string | null;
48
+ bodyText: string | null;
49
+ authorAccountId: string;
50
+ authorName: string | null;
51
+ /** `approval_new_member`, `approval_all` or `keyword:<word>`. See `describeHoldReason`. */
52
+ moderationReason: string | null;
53
+ createdAt: string;
54
+ }
55
+
56
+ /** A reply held for approval. */
57
+ export interface CommunityPendingReply {
58
+ id: string;
59
+ postId: string;
60
+ postTitle: string | null;
61
+ bodyText: string | null;
62
+ authorAccountId: string;
63
+ authorName: string | null;
64
+ moderationReason: string | null;
65
+ createdAt: string;
66
+ }
67
+
68
+ export interface CommunityModerationQueue {
69
+ reports: { data: CommunityModerationReport[]; total: number };
70
+ pending: { posts: CommunityPendingPost[]; replies: CommunityPendingReply[] };
71
+ }
72
+
73
+ export type CommunityModerationLogAction =
74
+ | "hold"
75
+ | "approve"
76
+ | "reject"
77
+ | "pin"
78
+ | "unpin"
79
+ | "lock"
80
+ | "unlock"
81
+ | "remove"
82
+ | "restore"
83
+ | "mute"
84
+ | "unmute"
85
+ | "ban"
86
+ | "unban"
87
+ | "role_member"
88
+ | "role_moderator"
89
+ | "adjust_points"
90
+ | "report_resolved"
91
+ | "report_resolved_hidden"
92
+ | "report_dismissed";
93
+
94
+ export interface CommunityModerationLogEntry {
95
+ id: string;
96
+ action: CommunityModerationLogAction | string;
97
+ entityType: "community_post" | "community_reply" | string | null;
98
+ entityId: string | null;
99
+ /** Null: the system held it (approval mode or a blocked word). */
100
+ actorAccountId: string | null;
101
+ targetAccountId: string | null;
102
+ reason: string | null;
103
+ createdAt: string;
104
+ actorName: string | null;
105
+ targetName: string | null;
106
+ }
107
+
108
+ export interface CommunityModerationLogPage {
109
+ data: CommunityModerationLogEntry[];
110
+ total: number;
111
+ page: number;
112
+ pageSize: number;
113
+ hasNextPage: boolean;
114
+ }
115
+
116
+ export type CommunityPostModerationAction =
117
+ | "pin"
118
+ | "unpin"
119
+ | "lock"
120
+ | "unlock"
121
+ | "remove"
122
+ | "restore"
123
+ | "approve"
124
+ | "reject";
125
+
126
+ export type CommunityReplyModerationAction = "remove" | "restore" | "approve" | "reject";
127
+
128
+ const BASE = "/public/community-moderation";
129
+
130
+ // ── Reads ────────────────────────────────────────────────────────────────────
131
+
132
+ /** Open reports and held posts and replies for this community (GET /queue). */
133
+ export function useCommunityModerationQueue(enabled = true) {
134
+ const { client, profileId, communityId } = useCommunityScope("useCommunityModerationQueue()");
135
+ return useQuery<CommunityModerationQueue>({
136
+ queryKey: communityKeys.moderationQueue(communityId),
137
+ queryFn: async () => {
138
+ const res = await client.get(`${BASE}/queue`, { params: { profileId, communityId } });
139
+ return res.data;
140
+ },
141
+ enabled: enabled && !!profileId && !!communityId && !!client,
142
+ });
143
+ }
144
+
145
+ /** One page of this community's moderation log, newest first (GET /log). */
146
+ export function useCommunityModerationLog(page = 1, limit = 20, enabled = true) {
147
+ const { client, profileId, communityId } = useCommunityScope("useCommunityModerationLog()");
148
+ return useQuery<CommunityModerationLogPage>({
149
+ queryKey: [...communityKeys.moderationLog(communityId, page), limit],
150
+ queryFn: async () => {
151
+ const res = await client.get(`${BASE}/log`, { params: { profileId, communityId, page, limit } });
152
+ return res.data;
153
+ },
154
+ // Paging keeps the current page on screen until the next one lands.
155
+ placeholderData: keepPreviousData,
156
+ enabled: enabled && !!profileId && !!communityId && !!client,
157
+ });
158
+ }
159
+
160
+ // ── Cache helpers ────────────────────────────────────────────────────────────
161
+
162
+ type Patchable = { id: string };
163
+
164
+ /** Patch one post wherever it is cached: every space feed and the post itself. */
165
+ export function patchCommunityPost(queryClient: QueryClient, postId: string, patch: Partial<CommunityPost>) {
166
+ const apply = <T extends Patchable>(item: T): T => (item.id === postId ? { ...item, ...patch } : item);
167
+ queryClient.setQueriesData({ queryKey: ["public-community-posts"] }, (old: any) => {
168
+ if (!old?.pages?.length) return old;
169
+ return { ...old, pages: old.pages.map((page: any) => ({ ...page, data: page.data.map(apply) })) };
170
+ });
171
+ queryClient.setQueryData(communityKeys.post(postId), (old: CommunityPost | undefined) => (old ? apply(old) : old));
172
+ }
173
+
174
+ /** Patch one reply wherever it is cached (every loaded thread). */
175
+ export function patchCommunityReply(queryClient: QueryClient, replyId: string, patch: Partial<CommunityReply>) {
176
+ const apply = <T extends Patchable>(item: T): T => (item.id === replyId ? { ...item, ...patch } : item);
177
+ queryClient.setQueriesData({ queryKey: ["public-community-replies"] }, (old: any) => {
178
+ if (!old?.pages?.length) return old;
179
+ return { ...old, pages: old.pages.map((page: any) => ({ ...page, data: page.data.map(apply) })) };
180
+ });
181
+ }
182
+
183
+ function invalidateModeration(queryClient: QueryClient, communityId: string | undefined) {
184
+ queryClient.invalidateQueries({ queryKey: communityKeys.moderationQueue(communityId) });
185
+ queryClient.invalidateQueries({ queryKey: ["public-community-moderation-log", communityId] });
186
+ }
187
+
188
+ // ── Actions ──────────────────────────────────────────────────────────────────
189
+
190
+ /**
191
+ * Pin, unpin, lock, unlock, remove, restore, approve or reject a post
192
+ * (POST /posts/:id/<action>). `reason` is read by remove and reject (max 500).
193
+ * Approving or rejecting something that is not pending answers 409.
194
+ *
195
+ * A removal is marked in the cache (`archivedAt`) rather than dropped, so the
196
+ * card can offer an undo in place. A reload drops it; the moderation log keeps
197
+ * the way back.
198
+ */
199
+ export function useModerateCommunityPost() {
200
+ const { client, profileId, communityId } = useCommunityScope("useModerateCommunityPost()");
201
+ const queryClient = useQueryClient();
202
+ return useMutation({
203
+ mutationFn: async (input: { postId: string; action: CommunityPostModerationAction; reason?: string }) => {
204
+ const reason = input.reason?.trim() || undefined;
205
+ const res = await client.post(`${BASE}/posts/${input.postId}/${input.action}`, { profileId, reason });
206
+ return { result: res.data as unknown, input };
207
+ },
208
+ onSuccess: ({ input }) => {
209
+ const { postId, action } = input;
210
+ switch (action) {
211
+ case "pin":
212
+ case "unpin":
213
+ patchCommunityPost(queryClient, postId, { isPinned: action === "pin" });
214
+ // Pinned posts sort first, so the feed order changes.
215
+ queryClient.invalidateQueries({ queryKey: ["public-community-posts"] });
216
+ break;
217
+ case "lock":
218
+ case "unlock":
219
+ patchCommunityPost(queryClient, postId, { isLocked: action === "lock" });
220
+ break;
221
+ case "remove":
222
+ patchCommunityPost(queryClient, postId, { archivedAt: new Date().toISOString() });
223
+ break;
224
+ case "restore":
225
+ patchCommunityPost(queryClient, postId, { archivedAt: null });
226
+ // Restored from the log, the post is in no cache yet: fetch it back into its feed.
227
+ queryClient.invalidateQueries({ queryKey: ["public-community-posts"] });
228
+ break;
229
+ case "approve":
230
+ patchCommunityPost(queryClient, postId, { status: "published", moderationReason: null });
231
+ queryClient.invalidateQueries({ queryKey: ["public-community-posts"] });
232
+ break;
233
+ case "reject":
234
+ patchCommunityPost(queryClient, postId, { status: "rejected" });
235
+ break;
236
+ }
237
+ invalidateModeration(queryClient, communityId);
238
+ },
239
+ });
240
+ }
241
+
242
+ /** Remove, restore, approve or reject a reply (POST /replies/:id/<action>). */
243
+ export function useModerateCommunityReply() {
244
+ const { client, profileId, communityId } = useCommunityScope("useModerateCommunityReply()");
245
+ const queryClient = useQueryClient();
246
+ return useMutation({
247
+ mutationFn: async (input: {
248
+ replyId: string;
249
+ action: CommunityReplyModerationAction;
250
+ reason?: string;
251
+ /** The reply's post, when known, so its thread and reply count refresh. */
252
+ postId?: string;
253
+ }) => {
254
+ const reason = input.reason?.trim() || undefined;
255
+ const res = await client.post(`${BASE}/replies/${input.replyId}/${input.action}`, { profileId, reason });
256
+ return { result: res.data as unknown, input };
257
+ },
258
+ onSuccess: ({ input }) => {
259
+ const { replyId, action, postId } = input;
260
+ if (action === "remove") patchCommunityReply(queryClient, replyId, { archivedAt: new Date().toISOString() });
261
+ if (action === "restore") {
262
+ patchCommunityReply(queryClient, replyId, { archivedAt: null });
263
+ // Restored from the log, the post is unknown: refresh every loaded thread.
264
+ if (!postId) queryClient.invalidateQueries({ queryKey: ["public-community-replies"] });
265
+ }
266
+ if (action === "approve") {
267
+ patchCommunityReply(queryClient, replyId, { status: "published", moderationReason: null });
268
+ if (postId) queryClient.invalidateQueries({ queryKey: communityKeys.replies(postId) });
269
+ }
270
+ if (action === "reject") patchCommunityReply(queryClient, replyId, { status: "rejected" });
271
+ // The post's reply count moves with remove, restore and approve.
272
+ if (postId && action !== "reject") queryClient.invalidateQueries({ queryKey: communityKeys.post(postId) });
273
+ invalidateModeration(queryClient, communityId);
274
+ },
275
+ });
276
+ }
277
+
278
+ /**
279
+ * Close a report (POST /reports/:id/resolve): `resolved` (optionally hiding the
280
+ * reported content) or `dismissed`.
281
+ */
282
+ export function useResolveCommunityReport() {
283
+ const { client, profileId, communityId } = useCommunityScope("useResolveCommunityReport()");
284
+ const queryClient = useQueryClient();
285
+ return useMutation({
286
+ mutationFn: async (input: { reportId: string; action: "resolved" | "dismissed"; hideContent?: boolean }) => {
287
+ const res = await client.post(`${BASE}/reports/${input.reportId}/resolve`, {
288
+ profileId,
289
+ action: input.action,
290
+ hideContent: input.hideContent ?? false,
291
+ });
292
+ return { result: res.data as unknown, input };
293
+ },
294
+ onSuccess: ({ input }) => {
295
+ invalidateModeration(queryClient, communityId);
296
+ if (input.hideContent) {
297
+ // The hidden post or reply leaves every feed and thread it sits in.
298
+ queryClient.invalidateQueries({ queryKey: ["public-community-posts"] });
299
+ queryClient.invalidateQueries({ queryKey: ["public-community-post"] });
300
+ queryClient.invalidateQueries({ queryKey: ["public-community-replies"] });
301
+ }
302
+ },
303
+ });
304
+ }
305
+
306
+ /**
307
+ * Mute a member in THIS community until a time, or unmute them with
308
+ * `mutedUntil: null` (POST /members/:accountId/mute).
309
+ */
310
+ export function useMuteCommunityMember() {
311
+ const { client, profileId, communityId } = useCommunityScope("useMuteCommunityMember()");
312
+ const queryClient = useQueryClient();
313
+ return useMutation({
314
+ mutationFn: async (input: { accountId: string; mutedUntil: string | null }) => {
315
+ const res = await client.post(`${BASE}/members/${input.accountId}/mute`, {
316
+ profileId,
317
+ communityId,
318
+ mutedUntil: input.mutedUntil,
319
+ });
320
+ return { result: res.data as unknown, input };
321
+ },
322
+ onSuccess: ({ input }) => {
323
+ queryClient.invalidateQueries({ queryKey: communityKeys.member(communityId, input.accountId) });
324
+ invalidateModeration(queryClient, communityId);
325
+ },
326
+ });
327
+ }
@@ -0,0 +1,137 @@
1
+ import { useQuery } from "@tanstack/react-query";
2
+ import { useCommunityScope } from "../../contexts/CommunityContext";
3
+ import { isNotFound, useViewerAccountId } from "./useCommunities";
4
+
5
+ /**
6
+ * A community's wiki (community parity §5.1), read on the site.
7
+ *
8
+ * `GET /public/communities/:slug/wiki` lists the pages this viewer may see, in
9
+ * order: public pages for everyone, plus members-only pages for a member. A
10
+ * members-only page their tier does not open comes back `locked`, with the
11
+ * tiers that open it and no content. `GET /public/communities/:slug/wiki/:page`
12
+ * returns one page's content, and answers 404 for a page the viewer may not
13
+ * read. Both take an optional session, so both are keyed per account.
14
+ */
15
+
16
+ export interface CommunityWikiTier {
17
+ id: string;
18
+ name: string;
19
+ }
20
+
21
+ /** One row of the page list. */
22
+ export interface CommunityWikiPageSummary {
23
+ id: string;
24
+ title: string;
25
+ slug: string;
26
+ isPublic: boolean;
27
+ /** Listed for this member, but their tier does not open it. */
28
+ locked: boolean;
29
+ /** The tiers that open a locked page. Empty when it is not locked. */
30
+ unlockTiers: CommunityWikiTier[];
31
+ /**
32
+ * The level that opens a locked page, when the member's level is what is
33
+ * missing. Null otherwise (and on older servers, absent).
34
+ */
35
+ unlockLevel?: number | null;
36
+ updatedAt: string;
37
+ }
38
+
39
+ /** One readable page. `content` is Lexical editor JSON from the Knowledge Base editor. */
40
+ export interface CommunityWikiPage {
41
+ id: string;
42
+ title: string;
43
+ slug: string;
44
+ isPublic: boolean;
45
+ content: unknown;
46
+ updatedAt: string;
47
+ }
48
+
49
+ export const communityWikiKeys = {
50
+ list: (profileId: string | undefined, slug: string | undefined, accountId: string | null) =>
51
+ ["public-community-wiki", profileId, slug, accountId] as const,
52
+ page: (
53
+ profileId: string | undefined,
54
+ slug: string | undefined,
55
+ pageSlug: string | undefined,
56
+ accountId: string | null,
57
+ ) => ["public-community-wiki-page", profileId, slug, pageSlug, accountId] as const,
58
+ media: (
59
+ profileId: string | undefined,
60
+ slug: string | undefined,
61
+ pageSlug: string | undefined,
62
+ mediaId: string | undefined,
63
+ accountId: string | null,
64
+ ) => ["public-community-wiki-media", profileId, slug, pageSlug, mediaId, accountId] as const,
65
+ };
66
+
67
+ /** A short-lived signed read for a private wiki image. */
68
+ export interface CommunityWikiMediaUrl {
69
+ url: string;
70
+ expiresInSeconds: number;
71
+ }
72
+
73
+ /**
74
+ * How long a signed image address is trusted before it is asked for again:
75
+ * a minute short of its expiry, and never under 30 seconds.
76
+ */
77
+ export function wikiMediaRefreshMs(expiresInSeconds: number | undefined): number {
78
+ const seconds = typeof expiresInSeconds === "number" && expiresInSeconds > 0 ? expiresInSeconds : 600;
79
+ return Math.max(30, seconds - 60) * 1000;
80
+ }
81
+
82
+ const enc = (value: string) => encodeURIComponent(value);
83
+
84
+ /** The wiki pages this viewer may see listed, in order. */
85
+ export function useCommunityWikiPages(opts: { enabled?: boolean } = {}) {
86
+ const { client, profileId, slug } = useCommunityScope("useCommunityWikiPages()");
87
+ const accountId = useViewerAccountId();
88
+ return useQuery<CommunityWikiPageSummary[]>({
89
+ queryKey: communityWikiKeys.list(profileId, slug, accountId),
90
+ queryFn: async () => (await client.get(`/public/communities/${enc(slug!)}/wiki`, { params: { profileId } })).data,
91
+ enabled: opts.enabled !== false && !!slug && !!profileId && !!client,
92
+ retry: (count, error) => !isNotFound(error) && count < 2,
93
+ });
94
+ }
95
+
96
+ /** One wiki page's content. A 404 means this viewer may not read it (locked, hidden or gone). */
97
+ export function useCommunityWikiPageContent(pageSlug: string | undefined, opts: { enabled?: boolean } = {}) {
98
+ const { client, profileId, slug } = useCommunityScope("useCommunityWikiPageContent()");
99
+ const accountId = useViewerAccountId();
100
+ return useQuery<CommunityWikiPage>({
101
+ queryKey: communityWikiKeys.page(profileId, slug, pageSlug, accountId),
102
+ queryFn: async () =>
103
+ (await client.get(`/public/communities/${enc(slug!)}/wiki/${enc(pageSlug!)}`, { params: { profileId } })).data,
104
+ enabled: opts.enabled !== false && !!slug && !!pageSlug && !!profileId && !!client,
105
+ retry: (count, error) => !isNotFound(error) && count < 2,
106
+ });
107
+ }
108
+
109
+ /**
110
+ * A signed address for a PRIVATE wiki image (`kbmedia:<id>` in a page body),
111
+ * from `GET /public/communities/:slug/wiki/:page/media/:mediaId`. The server
112
+ * signs only for a viewer who may read that page, only for media the page
113
+ * embeds, and answers 404 otherwise. The address lives 10 minutes, so it is
114
+ * fetched again shortly before it expires. Never cached beyond this viewer.
115
+ */
116
+ export function useCommunityWikiMediaUrl(
117
+ pageSlug: string | undefined,
118
+ mediaId: string | undefined,
119
+ opts: { enabled?: boolean } = {},
120
+ ) {
121
+ const { client, profileId, slug } = useCommunityScope("useCommunityWikiMediaUrl()");
122
+ const accountId = useViewerAccountId();
123
+ return useQuery<CommunityWikiMediaUrl>({
124
+ queryKey: communityWikiKeys.media(profileId, slug, pageSlug, mediaId, accountId),
125
+ queryFn: async () =>
126
+ (
127
+ await client.get(`/public/communities/${enc(slug!)}/wiki/${enc(pageSlug!)}/media/${enc(mediaId!)}`, {
128
+ params: { profileId },
129
+ })
130
+ ).data,
131
+ enabled: opts.enabled !== false && !!slug && !!pageSlug && !!mediaId && !!profileId && !!client,
132
+ retry: (count, error) => !isNotFound(error) && count < 2,
133
+ staleTime: (query) => wikiMediaRefreshMs(query.state.data?.expiresInSeconds),
134
+ refetchInterval: (query) => (query.state.data ? wikiMediaRefreshMs(query.state.data.expiresInSeconds) : false),
135
+ refetchOnWindowFocus: true,
136
+ });
137
+ }
@@ -1,4 +1,5 @@
1
1
  import type { PaginatedData, PillarDiscountQuote, PublicCourse } from "../../types/models";
2
+ import { readMetaSignals } from "../../utils/metaEvents";
2
3
  import { useForge } from "../../provider/ForgeProvider";
3
4
  import { useMutation, useQuery } from "@tanstack/react-query";
4
5
 
@@ -79,6 +80,7 @@ export function useCreateCourseBooking(courseId?: string) {
79
80
  courseId,
80
81
  profileId,
81
82
  ...body,
83
+ metaSignals: readMetaSignals(),
82
84
  });
83
85
  return res.data;
84
86
  },
@@ -0,0 +1,79 @@
1
+ import { useMutation } from "@tanstack/react-query";
2
+ import { useForge } from "../../provider/ForgeProvider";
3
+ import type { PublicAuthUser } from "../../types/models";
4
+
5
+ /**
6
+ * Email-first sign-in (`/public/sessions/email/*`): the fan types an email, we
7
+ * email a 6-digit code, and the code decides the rest. Someone with an account
8
+ * on this site is signed in by the code alone; someone new is asked their name
9
+ * and then signed in. The fan never chooses between "Create account" and
10
+ * "Sign in", and the start answer is the same whether or not the address has an
11
+ * account, so it cannot be used to find out who has one.
12
+ *
13
+ * These are the raw calls. A `signed_in` answer carries a session, which only
14
+ * `PublicAuthContext.adoptSession` may store; `useEmailCodeSignIn` wires the
15
+ * two together.
16
+ */
17
+
18
+ /** A session minted by the email flow: the same account shape `/public/sessions/verify` returns. */
19
+ export interface EmailSignInSession {
20
+ status: "signed_in";
21
+ account: PublicAuthUser;
22
+ token: string;
23
+ refreshToken: string;
24
+ associationId: string;
25
+ }
26
+
27
+ /** `POST /public/sessions/email/start`. */
28
+ export interface EmailSignInStarted {
29
+ status: "code_sent";
30
+ email: string;
31
+ }
32
+
33
+ /** `POST /public/sessions/email/verify`: either a session, or a ticket for the name step. Both are HTTP 200. */
34
+ export type EmailSignInVerified = EmailSignInSession | { status: "needs_profile"; ticket: string };
35
+
36
+ export interface CompleteEmailSignUpInput {
37
+ email: string;
38
+ /** The 64-character ticket `verify` answered with. */
39
+ ticket: string;
40
+ firstName: string;
41
+ lastName: string;
42
+ acceptedPrivacy?: boolean;
43
+ }
44
+
45
+ /** Email a code to `email`. Throttled server-side to one per 60 seconds. */
46
+ export function useStartEmailSignIn() {
47
+ const { client, profileId } = useForge();
48
+ return useMutation<EmailSignInStarted, unknown, { email: string }>({
49
+ mutationFn: async ({ email }) => (await client.post("/public/sessions/email/start", { profileId, email })).data,
50
+ });
51
+ }
52
+
53
+ /** Redeem the code. Branch on `status`, never on the HTTP code. */
54
+ export function useVerifyEmailSignIn() {
55
+ const { client, profileId } = useForge();
56
+ return useMutation<EmailSignInVerified, unknown, { email: string; code: string }>({
57
+ mutationFn: async ({ email, code }) =>
58
+ (await client.post("/public/sessions/email/verify", { profileId, email, code })).data,
59
+ });
60
+ }
61
+
62
+ /** Create the account for a verified address with no account yet, and sign in. */
63
+ export function useCompleteEmailSignUp() {
64
+ const { client, profileId } = useForge();
65
+ return useMutation<EmailSignInSession, unknown, CompleteEmailSignUpInput>({
66
+ mutationFn: async ({ email, ticket, firstName, lastName, acceptedPrivacy }) =>
67
+ (
68
+ await client.post("/public/sessions/email/complete", {
69
+ profileId,
70
+ email,
71
+ ticket,
72
+ firstName,
73
+ lastName,
74
+ acceptedTerms: true,
75
+ ...(acceptedPrivacy === undefined ? {} : { acceptedPrivacy }),
76
+ })
77
+ ).data,
78
+ });
79
+ }
@@ -1,4 +1,5 @@
1
1
  import type { AppliedDiscountCoupon, IEvent } from "../../types/models";
2
+ import { readMetaSignals } from "../../utils/metaEvents";
2
3
  import type { SeatAttributes, SeatMapDocument } from "@tribe-nest/seating-core";
3
4
  import { useForge } from "../../provider/ForgeProvider";
4
5
  import { useMutation, useQuery } from "@tanstack/react-query";
@@ -262,7 +263,11 @@ export function useCreateEventOrder(eventId?: string) {
262
263
 
263
264
  return useMutation<CreateEventOrderResult, unknown, CreateEventOrderInput>({
264
265
  mutationFn: async (body) => {
265
- const res = await client.post(`/public/events/${eventId}/orders`, { ...body, profileId });
266
+ const res = await client.post(`/public/events/${eventId}/orders`, {
267
+ ...body,
268
+ profileId,
269
+ metaSignals: readMetaSignals(),
270
+ });
266
271
  return res.data;
267
272
  },
268
273
  });
@@ -1,4 +1,5 @@
1
1
  import { useRef } from "react";
2
+ import { fireServerMetaEvent, readMetaSignals } from "../../utils/metaEvents";
2
3
  import type { FormData, FormAnswerEntry } from "../../types/models";
3
4
  import { useForge } from "../../provider/ForgeProvider";
4
5
  import { useMutation, useQuery } from "@tanstack/react-query";
@@ -63,14 +64,17 @@ export function useSubmitPublicForm(formId?: string) {
63
64
  const res = await client.post(`/public/forms/${formId}/submit`, {
64
65
  ...payload,
65
66
  idempotencyKey: keyRef.current,
67
+ metaSignals: readMetaSignals(),
66
68
  });
67
69
  return res.data;
68
70
  },
69
71
  // Rotate only once the write has landed. A failed submit keeps the key so
70
72
  // the retry is recognised as the same act; a successful one starts a fresh
71
73
  // act, so a form left mounted for corrections records each as its own.
72
- onSuccess: () => {
74
+ onSuccess: (data) => {
73
75
  keyRef.current = newIdempotencyKey();
76
+ // The server queued Lead for this submission; fire its pixel twin.
77
+ fireServerMetaEvent(data);
74
78
  },
75
79
  });
76
80
  }
@@ -1,4 +1,5 @@
1
1
  import { useForge } from "../../provider/ForgeProvider";
2
+ import { fireServerMetaEvent, readMetaSignals, type ServerMetaEvent } from "../../utils/metaEvents";
2
3
  import { useMutation, useQuery } from "@tanstack/react-query";
3
4
 
4
5
  export type LeadMagnetMedia = {
@@ -71,11 +72,14 @@ export type JoinEmailListInput = {
71
72
  export function useJoinEmailList() {
72
73
  const { client, profileId } = useForge();
73
74
 
74
- return useMutation<{ requiresConfirmation?: boolean }, unknown, JoinEmailListInput>({
75
+ return useMutation<{ requiresConfirmation?: boolean; metaEvent?: ServerMetaEvent }, unknown, JoinEmailListInput>({
75
76
  mutationFn: async (body) => {
76
- const res = await client.post(`/public/email-lists/join`, { ...body, profileId });
77
+ const res = await client.post(`/public/email-lists/join`, { ...body, profileId, metaSignals: readMetaSignals() });
77
78
  return res.data;
78
79
  },
80
+ // Active immediately (no double opt-in): the server queued
81
+ // CompleteRegistration; fire its pixel twin.
82
+ onSuccess: (data) => fireServerMetaEvent(data),
79
83
  });
80
84
  }
81
85