@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
@@ -1,5 +1,5 @@
1
1
  import type { IPublicPost, PaginatedData } from "../../types/models";
2
- import { useForge } from "../../provider/ForgeProvider";
2
+ import { useCommunityScope } from "../../contexts/CommunityContext";
3
3
  import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
4
4
 
5
5
  export interface GetPostsParams {
@@ -10,14 +10,15 @@ export interface GetPostsParams {
10
10
  }
11
11
 
12
12
  export function useGetPosts(params?: GetPostsParams) {
13
- const { client, profileId } = useForge();
13
+ const { client, profileId, communityId } = useCommunityScope("useGetPosts()");
14
14
 
15
15
  return useQuery<PaginatedData<IPublicPost>>({
16
- queryKey: ["posts", profileId, params],
16
+ queryKey: ["posts", communityId, params],
17
17
  queryFn: async () => {
18
18
  const res = await client.get("/public/posts", {
19
19
  params: {
20
- profileId: profileId,
20
+ profileId,
21
+ communityId,
21
22
  page: params?.page || 1,
22
23
  limit: 10,
23
24
  filter: {
@@ -29,7 +30,7 @@ export function useGetPosts(params?: GetPostsParams) {
29
30
  });
30
31
  return res.data;
31
32
  },
32
- enabled: !!profileId && !!client,
33
+ enabled: !!profileId && !!communityId && !!client,
33
34
  });
34
35
  }
35
36
 
@@ -46,19 +47,20 @@ export interface GetPostsInfiniteParams {
46
47
  * ships media for locked posts, so paging is safe for any auth state.
47
48
  */
48
49
  export function useGetPostsInfinite(params?: GetPostsInfiniteParams) {
49
- const { client, profileId } = useForge();
50
+ const { client, profileId, communityId } = useCommunityScope("useGetPostsInfinite()");
50
51
  const limit = params?.pageSize ?? 10;
51
52
 
52
53
  return useInfiniteQuery<PaginatedData<IPublicPost>>({
53
54
  queryKey: [
54
55
  "posts-infinite",
55
- profileId,
56
+ communityId,
56
57
  { query: params?.query, type: params?.type, membershipTierId: params?.membershipTierId, limit },
57
58
  ],
58
59
  queryFn: async ({ pageParam = 1 }) => {
59
60
  const res = await client.get("/public/posts", {
60
61
  params: {
61
- profileId: profileId,
62
+ profileId,
63
+ communityId,
62
64
  page: pageParam,
63
65
  limit,
64
66
  filter: {
@@ -72,23 +74,23 @@ export function useGetPostsInfinite(params?: GetPostsInfiniteParams) {
72
74
  },
73
75
  initialPageParam: 1,
74
76
  getNextPageParam: (lastPage) => (lastPage.hasNextPage ? (lastPage.nextPage ?? lastPage.page + 1) : undefined),
75
- enabled: !!profileId && !!client,
77
+ enabled: !!profileId && !!communityId && !!client,
76
78
  });
77
79
  }
78
80
 
79
81
  /**
80
- * The member's saved posts (GET /public/posts/saved — requires auth). The
82
+ * The member's saved posts (GET /public/posts/saved: requires auth). The
81
83
  * endpoint returns a single `{ data, total }` page.
82
84
  */
83
85
  export function useGetSavedPosts(enabled = true) {
84
- const { client, profileId } = useForge();
86
+ const { client, profileId, communityId } = useCommunityScope("useGetSavedPosts()");
85
87
 
86
88
  return useQuery<{ data: IPublicPost[]; total: number }>({
87
- queryKey: ["posts-saved", profileId],
89
+ queryKey: ["posts-saved", communityId],
88
90
  queryFn: async () => {
89
- const res = await client.get("/public/posts/saved", { params: { profileId } });
91
+ const res = await client.get("/public/posts/saved", { params: { profileId, communityId } });
90
92
  return res.data;
91
93
  },
92
- enabled: !!profileId && !!client && enabled,
94
+ enabled: !!profileId && !!communityId && !!client && enabled,
93
95
  });
94
96
  }
@@ -0,0 +1,130 @@
1
+ import { useEffect, useState } from "react";
2
+ import { useQuery } from "@tanstack/react-query";
3
+ import { useForge } from "../../provider/ForgeProvider";
4
+ import { internalPath } from "../../runtime/pages";
5
+
6
+ /**
7
+ * The site app: the installable app every website is (one per origin).
8
+ *
9
+ * Speaks `GET /public/websites/app?host=<host>`, which answers for the website
10
+ * that owns the host the visitor is on. Contract:
11
+ * docs/proposals/site-app-and-website-menu.md, sections 2 and 3.
12
+ */
13
+
14
+ /** The built-in sections of the app home, in their default order. */
15
+ export const SITE_APP_SECTION_KEYS = [
16
+ "needs_you",
17
+ "portal",
18
+ "communities",
19
+ "coming_up",
20
+ "learning",
21
+ "explore_communities",
22
+ "events",
23
+ "courses",
24
+ "shop",
25
+ ] as const;
26
+
27
+ export type SiteAppSectionKey = (typeof SITE_APP_SECTION_KEYS)[number];
28
+
29
+ /** What may sit in the app bar between Home and More. */
30
+ export const SITE_APP_BAR_KEYS = ["communities", "portal", "inbox", "events", "courses", "shop"] as const;
31
+
32
+ export type SiteAppBarKey = (typeof SITE_APP_BAR_KEYS)[number];
33
+
34
+ /** How many configured items fit between Home and More. */
35
+ export const SITE_APP_BAR_MAX_ITEMS = 3;
36
+
37
+ export type SiteAppHomeSection =
38
+ | { type: "builtin"; key: SiteAppSectionKey; visible: boolean }
39
+ | { type: "link"; id: string; label: string; href: string; visible: boolean };
40
+
41
+ /** The public answer, exactly as section 3 of the contract shapes it. */
42
+ export interface SiteApp {
43
+ websiteId: string;
44
+ profileId: string;
45
+ appName: string;
46
+ shortName: string;
47
+ iconUrl: string | null;
48
+ themeColor: string | null;
49
+ backgroundColor: string | null;
50
+ /** `/i/app`, or the creator's own start page. Home everywhere. */
51
+ startUrl: string;
52
+ /** Only the VISIBLE sections, in the configured order. */
53
+ homeSections: SiteAppHomeSection[];
54
+ barItems: SiteAppBarKey[];
55
+ installPrompt: boolean;
56
+ pushPrompt: boolean;
57
+ activeVersionId: string | null;
58
+ }
59
+
60
+ /** Where the app starts when the creator has not chosen a page of their own. */
61
+ export const SITE_APP_DEFAULT_START = internalPath("app");
62
+
63
+ /** Visible by default; `events` and `courses` are off until the creator turns them on. */
64
+ const DEFAULT_HIDDEN: ReadonlySet<SiteAppSectionKey> = new Set(["events", "courses"]);
65
+
66
+ /** The visible home sections when no settings answer: the contract's defaults. */
67
+ export const SITE_APP_DEFAULT_SECTIONS: readonly SiteAppHomeSection[] = SITE_APP_SECTION_KEYS.filter(
68
+ (key) => !DEFAULT_HIDDEN.has(key),
69
+ ).map((key) => ({ type: "builtin", key, visible: true }) as const);
70
+
71
+ export const SITE_APP_DEFAULT_BAR: readonly SiteAppBarKey[] = ["communities", "portal", "inbox"];
72
+
73
+ export const siteAppKeys = {
74
+ byHost: (host: string | null) => ["site-app", host] as const,
75
+ };
76
+
77
+ /**
78
+ * The host the visitor is on, read after mount.
79
+ *
80
+ * Not during render: the server has no `window`, and reading it there would
81
+ * render one thing on the server and another on the first client pass.
82
+ */
83
+ export function useCurrentHost(): string | null {
84
+ const [host, setHost] = useState<string | null>(null);
85
+ useEffect(() => {
86
+ if (typeof window !== "undefined") setHost(window.location.host || null);
87
+ }, []);
88
+ return host;
89
+ }
90
+
91
+ /**
92
+ * This website's app settings, for the host the visitor is on.
93
+ *
94
+ * An unknown host (a preview, the edit sandbox) is a 404, which is an answer
95
+ * rather than a fault: it is not retried, and every reader falls back to the
96
+ * defaults (`useAppHomeHref` to `/i/app`).
97
+ */
98
+ export function useSiteApp(opts: { host?: string | null; enabled?: boolean } = {}) {
99
+ const { client } = useForge();
100
+ const currentHost = useCurrentHost();
101
+ const host = opts.host ?? currentHost;
102
+ return useQuery<SiteApp>({
103
+ queryKey: siteAppKeys.byHost(host),
104
+ queryFn: async () => (await client.get("/public/websites/app", { params: { host } })).data as SiteApp,
105
+ enabled: !!client && !!host && opts.enabled !== false,
106
+ retry: false,
107
+ // Settings change rarely, and every page with an app bar reads them.
108
+ staleTime: 5 * 60_000,
109
+ });
110
+ }
111
+
112
+ /**
113
+ * Where "Home" is: the creator's start page when they chose one, else `/i/app`.
114
+ *
115
+ * The Home rule (contract section 4): the app bar's Home, the community
116
+ * header's logo, and where sign-in lands by default all read this, so a creator
117
+ * who moves Home moves it everywhere at once.
118
+ */
119
+ export function useAppHomeHref(): string {
120
+ const { data } = useSiteApp();
121
+ return resolveAppHomeHref(data);
122
+ }
123
+
124
+ /** The pure half of `useAppHomeHref`, for a caller that already holds the settings. */
125
+ export function resolveAppHomeHref(app: Pick<SiteApp, "startUrl"> | null | undefined): string {
126
+ const start = app?.startUrl?.trim();
127
+ // Only a root-relative path is a Home: anything else would send the member off-site.
128
+ if (!start || !start.startsWith("/") || start.startsWith("//")) return SITE_APP_DEFAULT_START;
129
+ return start;
130
+ }
@@ -1,4 +1,6 @@
1
1
  import { useForge } from "../../provider/ForgeProvider";
2
+ import { readMetaSignals } from "../../utils/metaEvents";
3
+ import { useOptionalCommunityContext } from "../../contexts/CommunityContext";
2
4
  import { useMutation } from "@tanstack/react-query";
3
5
  import { usePublicAuth } from "../../contexts/PublicAuthContext";
4
6
  import type { QuestionnaireAnswer } from "../../types/models";
@@ -20,6 +22,8 @@ export type CreateSubscriptionInput = {
20
22
  * invite must be for the signed-in email); ignored on an open tier.
21
23
  */
22
24
  inviteToken?: string;
25
+ /** A shareable invite link token (`?join=`); opens an invite-only tier, still charges a paid one. */
26
+ joinToken?: string;
23
27
  /** Answers to the tier's questions. Required by the server when the tier has any. */
24
28
  questionnaire?: QuestionnaireAnswer[];
25
29
  };
@@ -55,6 +59,8 @@ export type CreateSubscriptionResult = {
55
59
  * charged today, and the first charge is on `endsAt`.
56
60
  */
57
61
  trial?: { days: number; endsAt: string } | null;
62
+ /** The slug of the community the tier belongs to, so the caller can land the fan inside it. */
63
+ communitySlug?: string;
58
64
  };
59
65
 
60
66
  /** Subscribe to a paid tier, or move an existing subscription onto another one. */
@@ -63,7 +69,11 @@ export function useCreateSubscription() {
63
69
 
64
70
  return useMutation<CreateSubscriptionResult, unknown, CreateSubscriptionInput>({
65
71
  mutationFn: async (body) => {
66
- const res = await client.post("/public/payments/subscriptions", { ...body, profileId });
72
+ const res = await client.post("/public/payments/subscriptions", {
73
+ ...body,
74
+ profileId,
75
+ metaSignals: readMetaSignals(),
76
+ });
67
77
  return res.data;
68
78
  },
69
79
  });
@@ -73,10 +83,12 @@ export function useCreateSubscription() {
73
83
  export function useCreateFreeSubscription() {
74
84
  const { client, profileId } = useForge();
75
85
 
76
- return useMutation<unknown, unknown, {
86
+ return useMutation<{ communitySlug?: string } | undefined, unknown, {
77
87
  membershipTierId: string;
78
88
  attributionRefId?: string;
79
89
  inviteToken?: string;
90
+ /** A shareable invite link token (`?join=`). */
91
+ joinToken?: string;
80
92
  /** Answers to the tier's questions. Required by the server when the tier has any. */
81
93
  questionnaire?: QuestionnaireAnswer[];
82
94
  }>({
@@ -89,7 +101,7 @@ export function useCreateFreeSubscription() {
89
101
 
90
102
  export type CancelMembershipResult = {
91
103
  membershipId: string;
92
- /** The status AFTER cancelling — still `active` while a paid period runs out. */
104
+ /** The status AFTER cancelling: still `active` while a paid period runs out. */
93
105
  status: string;
94
106
  /** True when the cancellation was scheduled rather than applied immediately. */
95
107
  cancelAtPeriodEnd: boolean;
@@ -104,7 +116,7 @@ export type CancelMembershipResult = {
104
116
  * schedules it for the end of the period they have already paid for and the
105
117
  * member keeps their benefits until `accessUntil`. Free tiers (and any
106
118
  * membership with nothing paid up) end immediately. Works for every membership
107
- * the member can still use — including one whose card is failing, which used to
119
+ * the member can still use: including one whose card is failing, which used to
108
120
  * be rejected with an error the UI never showed.
109
121
  */
110
122
  export function useCancelMembership() {
@@ -126,7 +138,7 @@ export function useCancelMembership() {
126
138
  *
127
139
  * Every membership-aware surface should use this instead of reading
128
140
  * `user.membership.status`. `access` answers "can they see it", `message`
129
- * answers "what do we tell them" — two different questions, and conflating
141
+ * answers "what do we tell them": two different questions, and conflating
130
142
  * them is what locked past_due members out while calling them "Cancelled".
131
143
  */
132
144
  export function useMembershipAccess(): {
@@ -143,12 +155,12 @@ export function useMembershipAccess(): {
143
155
  *
144
156
  * This is the other half of keeping access through `past_due`: the grace window
145
157
  * is only a recovery window if the member can actually fix their card inside
146
- * it. Nothing about the caller is sent — the server derives the provider
158
+ * it. Nothing about the caller is sent: the server derives the provider
147
159
  * customer from the session, because a customer id in a request body is a
148
160
  * cross-tenant leak waiting to happen.
149
161
  *
150
162
  * `returnUrl` defaults to the current page so the member lands back where they
151
- * were. The returned `url` is a one-time link — redirect, never render it.
163
+ * were. The returned `url` is a one-time link: redirect, never render it.
152
164
  */
153
165
  export function useOpenBillingPortal() {
154
166
  const { client, profileId } = useForge();
@@ -170,14 +182,21 @@ export function useOpenBillingPortal() {
170
182
  /**
171
183
  * Reconcile the most recent subscription after a checkout return.
172
184
  * Returns the latest subscription status (e.g. "active").
185
+ *
186
+ * A fan can subscribe in several communities of one creator, so "the latest"
187
+ * is the latest IN A COMMUNITY: pass `communityId`, or it is read from the
188
+ * surrounding `<CommunityProvider>`.
173
189
  */
174
190
  export function useConfirmLatestSubscription() {
175
191
  const { client, profileId } = useForge();
192
+ const contextCommunityId = useOptionalCommunityContext()?.communityId;
176
193
 
177
- return useMutation<{ status: string }, unknown, void>({
178
- mutationFn: async () => {
194
+ return useMutation<{ status: string }, unknown, { communityId?: string } | void>({
195
+ mutationFn: async (input) => {
196
+ const communityId = (input && input.communityId) || contextCommunityId;
179
197
  const res = await client.post("/public/payments/subscriptions/confirm-latest", {
180
198
  profileId,
199
+ ...(communityId ? { communityId } : {}),
181
200
  });
182
201
  return res.data;
183
202
  },