@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
@@ -13,6 +13,7 @@ import { EventCountdown } from "./EventCountdown";
13
13
  import { EventWaitlist } from "./EventWaitlist";
14
14
  import { AddToCalendar } from "./AddToCalendar";
15
15
  import { PresaleCodeField } from "./PresaleCode";
16
+ import { useMetaViewContent } from "../analytics/useMetaEvents";
16
17
 
17
18
  export interface EventDetailProps {
18
19
  /** Event slug (resolves the event). */
@@ -67,6 +68,15 @@ export function EventDetail({
67
68
  // contains the tiers that code unlocked.
68
69
  const presale = usePresaleCode(slug, { initialEvent });
69
70
  const { event, isLoading } = presale;
71
+ useMetaViewContent(
72
+ event
73
+ ? {
74
+ id: event.id,
75
+ name: event.title,
76
+ value: event.tickets?.length ? Math.min(...event.tickets.map((ticket) => Number(ticket.price))) : undefined,
77
+ }
78
+ : null,
79
+ );
70
80
 
71
81
  if (isLoading) return <Loading fullPage />;
72
82
  if (!event) return <p>{tr("forge.event_detail.not_found")}</p>;
@@ -16,6 +16,11 @@ export interface FilmRentalListProps {
16
16
  basePath?: string;
17
17
  /** SPA navigation adapter. Defaults to a full-page visit. */
18
18
  navigate?: (href: string) => void;
19
+ /**
20
+ * `cards` (the default) boxes each rental. `rows` divides them with hairlines
21
+ * instead, the client portal's list style.
22
+ */
23
+ layout?: "cards" | "rows";
19
24
  className?: string;
20
25
  style?: CSSProperties;
21
26
  }
@@ -36,7 +41,7 @@ export type FilmLibraryProps = FilmRentalListProps;
36
41
  * that already read it (the dashboard does, to decide whether to draw the tab at
37
42
  * all) shares one request through react-query rather than making a second.
38
43
  */
39
- export function FilmRentalList({ basePath = "/i/films", navigate, className, style }: FilmRentalListProps) {
44
+ export function FilmRentalList({ basePath = "/i/films", navigate, layout = "cards", className, style }: FilmRentalListProps) {
40
45
  const t = useForgeT();
41
46
  const tokens = useThemeTokens();
42
47
  const { user, isInitialized } = usePublicAuth();
@@ -47,11 +52,12 @@ export function FilmRentalList({ basePath = "/i/films", navigate, className, sty
47
52
 
48
53
  const rentals = data?.data ?? [];
49
54
 
55
+ const rows = layout === "rows";
50
56
  const notice: CSSProperties = {
51
57
  padding: "28px 20px",
52
58
  borderRadius: tokens.cornerRadius,
53
- border: `1px solid ${tokens.border}`,
54
- background: tokens.surface,
59
+ border: `1px ${rows ? "dashed" : "solid"} ${tokens.border}`,
60
+ background: rows ? "transparent" : tokens.surface,
55
61
  color: tokens.muted,
56
62
  textAlign: "center",
57
63
  fontSize: 14,
@@ -63,7 +69,8 @@ export function FilmRentalList({ basePath = "/i/films", navigate, className, sty
63
69
  style={{
64
70
  display: "flex",
65
71
  flexDirection: "column",
66
- gap: 16,
72
+ gap: rows ? 0 : 16,
73
+ ...(rows && rentals.length > 0 ? { borderTop: `1px solid ${tokens.border}` } : {}),
67
74
  color: tokens.text,
68
75
  fontFamily: tokens.fontFamily,
69
76
  ...style,
@@ -92,7 +99,7 @@ export function FilmRentalList({ basePath = "/i/films", navigate, className, sty
92
99
  )}
93
100
 
94
101
  {rentals.map((rental) => (
95
- <RentalRow key={rental.id} rental={rental} t={t} basePath={basePath} go={go} />
102
+ <RentalRow key={rental.id} rental={rental} t={t} basePath={basePath} go={go} rows={rows} />
96
103
  ))}
97
104
  </div>
98
105
  );
@@ -161,11 +168,13 @@ function RentalRow({
161
168
  t,
162
169
  basePath,
163
170
  go,
171
+ rows,
164
172
  }: {
165
173
  rental: MyFilmRental;
166
174
  t: ForgeT;
167
175
  basePath: string;
168
176
  go: (href: string) => void;
177
+ rows?: boolean;
169
178
  }) {
170
179
  const tokens = useThemeTokens();
171
180
  const remaining = splitRemainingMs(rental.msRemaining);
@@ -195,10 +204,14 @@ function RentalRow({
195
204
  gap: 14,
196
205
  alignItems: "center",
197
206
  flexWrap: "wrap",
198
- padding: 14,
199
- borderRadius: tokens.cornerRadius,
200
- border: `1px solid ${tokens.border}`,
201
- background: tokens.surface,
207
+ ...(rows
208
+ ? { padding: "16px 0", borderBottom: `1px solid ${tokens.border}` }
209
+ : {
210
+ padding: 14,
211
+ borderRadius: tokens.cornerRadius,
212
+ border: `1px solid ${tokens.border}`,
213
+ background: tokens.surface,
214
+ }),
202
215
  opacity: rental.isLive ? 1 : 0.85,
203
216
  }}
204
217
  >
@@ -16,6 +16,9 @@ import { formatTrialDate, tierTrialDays } from "../headless/membership/membershi
16
16
  import { readableTextOn } from "../theme/contrast";
17
17
  import { cycleFloor, cycleIsFree, offeredCycles, type BillingCycle } from "../format/membershipPwyw";
18
18
  import type { MembershipTier } from "../../types/models";
19
+ import { useOptionalCommunityContext } from "../../contexts/CommunityContext";
20
+ import { useCommunityViewerState } from "../../data/queries/useCommunities";
21
+ import { communitiesListPath, communityAboutPath, communityHomePath } from "../headless/communities/paths";
19
22
  import { Loading } from "./Loading";
20
23
  import { PaystackPayButton } from "./PaystackPayButton";
21
24
 
@@ -25,23 +28,24 @@ export interface MembershipCheckoutProps {
25
28
  /**
26
29
  * Called when the subscription is finished in-app: a free activation, a tier
27
30
  * change settled by proration, or a paid card payment that succeeded. The
28
- * host navigates however it wants (e.g. to the account page). Omit to fall
29
- * back to Forge's default `window.location` redirect.
31
+ * host navigates however it wants. Omit to fall back to Forge's default
32
+ * `window.location` redirect, which lands inside the community.
30
33
  *
31
34
  * `requiresConfirmation` is true only for a real provider payment, which is
32
35
  * the only case the account page has anything to reconcile.
33
36
  */
34
37
  onComplete?: (result: { requiresConfirmation: boolean }) => void;
35
38
  /** The URL Stripe returns to for a PAID subscription (its `return_url`, and the
36
- * free/paid fallback redirect). Default `/i/account?tab=membership&confirmSubscription=true`. */
39
+ * free/paid fallback redirect). Default: the community home with
40
+ * `confirmSubscription=1` (the community comes from `<CommunityProvider>`). */
37
41
  successPath?: string;
38
- /** Where the "View memberships" link points when the artist sells no tiers. Default `/i/membership`. */
42
+ /** Where the "View memberships" link points when there are no tiers. Default: the community's About page. */
39
43
  membershipsPath?: string;
40
44
  /**
41
- * The fan's account page, where their applications are listed. The link
42
- * after applying points here. Default `/i/account`.
45
+ * Where the link after applying points: the community home, which shows the
46
+ * pending application. Default: the surrounding community's home.
43
47
  */
44
- accountPath?: string;
48
+ homePath?: string;
45
49
  /** Format a numeric amount for display. Defaults to the runtime currency formatter. */
46
50
  formatAmount?: (amount: number) => string;
47
51
  /**
@@ -83,9 +87,9 @@ export interface MembershipCheckoutProps {
83
87
  export function MembershipCheckout({
84
88
  initialTierId,
85
89
  onComplete,
86
- successPath = "/i/account?tab=membership&confirmSubscription=true",
87
- membershipsPath = "/i/membership",
88
- accountPath = "/i/account",
90
+ successPath,
91
+ membershipsPath: membershipsPathProp,
92
+ homePath: homePathProp,
89
93
  formatAmount,
90
94
  inviteToken,
91
95
  className,
@@ -140,12 +144,28 @@ export function MembershipCheckout({
140
144
  const taxCaption = priceTaxCaption({ pricesIncludeTax: usePricesIncludeTax() });
141
145
  const renderPayment = usePaymentRenderer();
142
146
 
147
+ const communityContext = useOptionalCommunityContext();
148
+ const slug = communityContext?.slug;
149
+ // Inside a community, the fan's membership IN IT (not `user.membership`, the
150
+ // newest one on the profile) decides "current plan" and join vs change.
151
+ const viewer = useCommunityViewerState(communityContext?.communityId ?? slug, {
152
+ enabled: !!(communityContext?.communityId ?? slug),
153
+ });
154
+ const current =
155
+ communityContext && viewer.data
156
+ ? viewer.data.membership
157
+ ? { tierId: viewer.data.membership.tierId, status: viewer.data.membership.status }
158
+ : null
159
+ : undefined;
160
+ const membershipsPath = membershipsPathProp ?? (slug ? communityAboutPath(slug) : communitiesListPath());
161
+ const homePath = homePathProp ?? (slug ? communityHomePath(slug) : communitiesListPath());
143
162
  const checkout = useMembershipCheckout({
144
163
  initialTierId,
145
164
  returnPath: successPath,
146
165
  freeReturnPath: successPath,
147
166
  onComplete,
148
167
  inviteToken,
168
+ current,
149
169
  });
150
170
  const inviteInfo = useMembershipInviteInfo(checkout.inviteToken);
151
171
 
@@ -269,16 +289,16 @@ export function MembershipCheckout({
269
289
  : t(APPLICATION_OUTCOME_BODY[checkout.applicationOutcome], { tier_name: tier.name })}
270
290
  </p>
271
291
  </div>
272
- {/* A lodged or approved application is followed on the account page.
273
- One that closed leaves the fan choosing again, so that goes back
274
- to the tiers. */}
292
+ {/* A lodged or approved application is followed inside the community,
293
+ which shows it pending. One that closed leaves the fan choosing
294
+ again, so that goes back to the tiers. */}
275
295
  {checkout.applicationOutcome === "received" || checkout.applicationOutcome === "approved" ? (
276
296
  <a
277
- href={accountPath}
278
- data-testid="membership-application-account-link"
297
+ href={homePath}
298
+ data-testid="membership-application-home-link"
279
299
  style={{ ...ghostButton, display: "block", textAlign: "center", textDecoration: "none", boxSizing: "border-box" }}
280
300
  >
281
- {t("forge.membership_checkout.go_to_account")}
301
+ {t("forge.membership_checkout.go_to_community")}
282
302
  </a>
283
303
  ) : (
284
304
  <a
@@ -445,7 +465,7 @@ export function MembershipCheckout({
445
465
  returnUrl:
446
466
  checkout.setupPurpose === "application" && checkout.applicationReturnUrl
447
467
  ? checkout.applicationReturnUrl
448
- : `${typeof window !== "undefined" ? window.location.origin : ""}${successPath}`,
468
+ : `${typeof window !== "undefined" ? window.location.origin : ""}${checkout.returnPath}`,
449
469
  amount: checkout.amount,
450
470
  currency: settlementCurrency,
451
471
  mode: "manual",
@@ -4,6 +4,8 @@ import { useForgeT } from "../../i18n";
4
4
  import { useThemeTokens } from "../theme/ForgeThemeProvider";
5
5
  import { useAmountFormatter } from "../format/useFormatCurrency";
6
6
  import { membershipPriceLabel } from "../format/membershipPriceLabel";
7
+ import { usePublicCommunities } from "../../data/queries/useCommunities";
8
+ import { communityAboutPath } from "../headless/communities/paths";
7
9
 
8
10
  export interface MembershipTierCalloutProps {
9
11
  /** The tier the visitor is on their way to join. Nothing is drawn without it. */
@@ -30,6 +32,9 @@ export interface MembershipTierCalloutProps {
30
32
  * site remembers too. The client app has always shown this; code websites did
31
33
  * not, so this is the same reassurance in a Forge component.
32
34
  *
35
+ * A tier belongs to exactly one community, so the callout names it and links
36
+ * to its About page with the tier preselected, where the join happens in place.
37
+ *
33
38
  * Renders NOTHING when there is no tier id, when the tiers have not loaded, or
34
39
  * when the id matches nothing the artist sells. A form that pushes itself down
35
40
  * the page a beat after it appeared is worse than one that never moved, and an
@@ -50,6 +55,9 @@ export function MembershipTierCallout({
50
55
  const { data: tiers } = useGetMembershipTiers(inviteToken ? { inviteToken } : {});
51
56
  const { data: invite } = useMembershipInviteInfo(inviteToken);
52
57
  const tier = membershipTierId ? tiers?.find((candidate) => candidate.id === membershipTierId) : undefined;
58
+ const tierCommunityId = (tier as { communityId?: string } | undefined)?.communityId;
59
+ const { data: communities } = usePublicCommunities({ enabled: !!tierCommunityId });
60
+ const community = tierCommunityId ? communities?.find((candidate) => candidate.id === tierCommunityId) : undefined;
53
61
 
54
62
  if (!tier) return null;
55
63
 
@@ -76,7 +84,18 @@ export function MembershipTierCallout({
76
84
  <h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 8, fontFamily: tokens.headingFontFamily }}>
77
85
  {t("forge.membership_tier_callout.join_title", { tier_name: tier.name })}
78
86
  </h2>
79
- <p style={{ fontSize: 14, marginBottom: shown.length || invite ? 12 : 0 }}>{membershipPriceLabel(tier, t, fmt)}</p>
87
+ <p style={{ fontSize: 14, marginBottom: shown.length || invite || community ? 12 : 0 }}>{membershipPriceLabel(tier, t, fmt)}</p>
88
+ {community && (
89
+ <p style={{ fontSize: 14, marginBottom: shown.length || invite ? 12 : 0 }}>
90
+ <a
91
+ href={communityAboutPath(community.slug, { tier: tier.id })}
92
+ data-testid="membership-tier-callout-community"
93
+ style={{ color: tokens.primary, fontWeight: 600 }}
94
+ >
95
+ {t("forge.membership_tier_callout.view_community", { community_name: community.name })}
96
+ </a>
97
+ </p>
98
+ )}
80
99
  {invite && invite.membershipTierId === tier.id && (
81
100
  <p style={{ fontSize: 14, fontWeight: 600, marginBottom: shown.length ? 12 : 0 }}>
82
101
  {t("forge.membership_tier_callout.invite_for_email", { email: invite.email })}
@@ -14,6 +14,9 @@ import { usePricesIncludeTax } from "../../data/queries/useWebsite";
14
14
  import { useForgeT } from "../../i18n";
15
15
  import { PublicAuthContext } from "../../contexts/PublicAuthContext";
16
16
  import { membershipPriceLabel } from "../format/membershipPriceLabel";
17
+ import { CommunityProvider, useOptionalCommunityContext } from "../../contexts/CommunityContext";
18
+ import { usePublicCommunities } from "../../data/queries/useCommunities";
19
+ import { communityAboutPath, communitiesListPath } from "../headless/communities/paths";
17
20
  import { Button } from "./Button";
18
21
  import { Loading } from "./Loading";
19
22
 
@@ -21,8 +24,16 @@ export interface MembershipTiersProps {
21
24
  /** Format a numeric amount for display. Defaults to `$X.XX`. */
22
25
  formatAmount?: (amount: number) => string;
23
26
  /**
24
- * Where "Select" sends the buyer. The chosen tier id is appended as the
25
- * `membershipTierId` query param. Default `/i/membership/checkout`.
27
+ * The community whose tiers to show (slug or id). Omitted, the surrounding
28
+ * `<CommunityProvider>` is used; outside one, every tier on the profile is
29
+ * listed and each links to its own community.
30
+ */
31
+ community?: string;
32
+ /**
33
+ * Override where "Select" sends the buyer; the tier id is appended as the
34
+ * `membershipTierId` query param. Default: the tier's community About page
35
+ * with the tier preselected (`/i/communities/<slug>?tier=<id>`), which opens
36
+ * the join sheet in place.
26
37
  */
27
38
  checkoutPath?: string;
28
39
  /**
@@ -39,7 +50,11 @@ export interface MembershipTiersProps {
39
50
  }
40
51
 
41
52
  /**
42
- * Drop-in membership page. Renders every membership tier as a centered,
53
+ * Drop-in tier cards. `community` scopes them to one community:
54
+ *
55
+ * <MembershipTiers community="fan-club" />
56
+ *
57
+ * Renders every membership tier as a centered,
43
58
  * responsive card row (a single tier sits at a natural card width rather than
44
59
  * stretching full-screen). "Select" routes to `checkoutPath` with the tier id,
45
60
  * or calls `onSelect` when provided.
@@ -53,14 +68,25 @@ export interface MembershipTiersProps {
53
68
  * "{days}-day free trial" badge, and an open tier's button reads "Start free
54
69
  * trial". An application tier keeps "Apply to join" (approval starts the trial).
55
70
  */
56
- export function MembershipTiers({
71
+ export function MembershipTiers({ community, ...props }: MembershipTiersProps) {
72
+ if (community) {
73
+ return (
74
+ <CommunityProvider community={community}>
75
+ <MembershipTierCards {...props} />
76
+ </CommunityProvider>
77
+ );
78
+ }
79
+ return <MembershipTierCards {...props} />;
80
+ }
81
+
82
+ function MembershipTierCards({
57
83
  formatAmount,
58
- checkoutPath = "/i/membership/checkout",
84
+ checkoutPath,
59
85
  onSelect,
60
86
  heading,
61
87
  className,
62
88
  style,
63
- }: MembershipTiersProps) {
89
+ }: Omit<MembershipTiersProps, "community">) {
64
90
  const tokens = useThemeTokens();
65
91
  const t = useForgeT();
66
92
  const fmt = useAmountFormatter(formatAmount);
@@ -68,6 +94,9 @@ export function MembershipTiers({
68
94
  const pricesIncludeTax = usePricesIncludeTax();
69
95
  const taxCaption = priceTaxCaption({ pricesIncludeTax });
70
96
  const { data: tiers, isLoading } = useGetMembershipTiers();
97
+ const context = useOptionalCommunityContext();
98
+ // Only needed to link a tier to its community when there is no provider.
99
+ const { data: communities } = usePublicCommunities({ enabled: !context && !checkoutPath && !onSelect });
71
100
  const withdraw = useWithdrawMembershipApplication();
72
101
  const [withdrawError, setWithdrawError] = useState<string | null>(null);
73
102
  // `null` still means "no heading"; anything else falls back to the default.
@@ -82,15 +111,24 @@ export function MembershipTiers({
82
111
  // the same sentence this page does.
83
112
  const priceLabel = (tier: MembershipTier) => membershipPriceLabel(tier, t, fmt);
84
113
 
85
- const select = (tier: MembershipTier) => {
86
- if (onSelect) return onSelect(tier);
87
- if (typeof window !== "undefined") {
114
+ /** A tier belongs to exactly one community; its About page is where it is joined. */
115
+ const tierHref = (tier: MembershipTier): string => {
116
+ const invite = readMembershipInviteFromUrl();
117
+ if (checkoutPath) {
88
118
  const sep = checkoutPath.includes("?") ? "&" : "?";
89
- const invite = readMembershipInviteFromUrl();
90
- window.location.href = `${checkoutPath}${sep}membershipTierId=${encodeURIComponent(tier.id)}${
119
+ return `${checkoutPath}${sep}membershipTierId=${encodeURIComponent(tier.id)}${
91
120
  invite ? `&invite=${encodeURIComponent(invite)}` : ""
92
121
  }`;
93
122
  }
123
+ const tierCommunityId = (tier as MembershipTier & { communityId?: string }).communityId;
124
+ const slug =
125
+ context?.slug ?? communities?.find((candidate) => candidate.id === tierCommunityId)?.slug ?? undefined;
126
+ return slug ? communityAboutPath(slug, { tier: tier.id, ...(invite ? { invite } : {}) }) : communitiesListPath();
127
+ };
128
+
129
+ const select = (tier: MembershipTier) => {
130
+ if (onSelect) return onSelect(tier);
131
+ if (typeof window !== "undefined") window.location.href = tierHref(tier);
94
132
  };
95
133
 
96
134
  return (
@@ -214,13 +214,13 @@ export function MusicLinkPage({
214
214
  const destinations = useMemo(() => link?.destinations ?? [], [link]);
215
215
 
216
216
  const onDestinationClick = (platform: string, url: string) => {
217
- // ONE id, shared by the browser pixel event and the server-side CAPI twin
218
- // that `maybeSendPageCapi` emits for this click. Meta deduplicates on
219
- // (event_id, event_name), so without a shared id the same tap is counted as
220
- // two conversions and the campaign optimises against inflated numbers.
217
+ // ONE id, the click's own, shared by the campaign pixel's Lead, its server
218
+ // copy and the first-party click. Meta deduplicates on (event_id,
219
+ // event_name), so without a shared id the same tap is counted as two
220
+ // conversions and the campaign optimises against inflated numbers.
221
221
  const eventId = newEventId();
222
222
 
223
- fire("Lead", { content_name: serviceMeta(platform).label, content_category: "music_link" });
223
+ fire("Lead", { content_name: serviceMeta(platform).label, content_category: "music_link" }, eventId);
224
224
 
225
225
  trackBeacon("click", {
226
226
  pathname: typeof window !== "undefined" ? window.location.pathname : undefined,
@@ -8,7 +8,7 @@ export interface PaywallProps {
8
8
  onAction?: () => void;
9
9
  }
10
10
 
11
- /** A themed "members only" card — pair with `<MembershipGate fallback={...}>`. */
11
+ /** A themed "members only" card. Pair with `<CommunityGate fallback={...}>`. */
12
12
  export function Paywall({
13
13
  title = "Members only",
14
14
  message = "This content is available to members. Join to unlock it.",
@@ -16,6 +16,7 @@ import { usePricesIncludeTax } from "../../data/queries/useWebsite";
16
16
  import { Button } from "./Button";
17
17
  import { Loading } from "./Loading";
18
18
  import { ReviewsSection } from "./ReviewsSection";
19
+ import { useMetaViewContent } from "../analytics/useMetaEvents";
19
20
 
20
21
  export interface ProductDetailProps {
21
22
  /** Product slug (resolves the product). */
@@ -68,6 +69,11 @@ export function ProductDetail({
68
69
  const tr = useForgeT();
69
70
  const { data: product, isLoading } = useGetProduct(slug, { initialData: initialProduct });
70
71
  const fmt = useAmountFormatter(formatAmount);
72
+ useMetaViewContent(
73
+ product
74
+ ? { id: product.id, name: product.title, value: lowestPrice(product.variants?.map((v) => v.price)) }
75
+ : null,
76
+ );
71
77
 
72
78
  if (isLoading) return <Loading fullPage />;
73
79
  if (!product) return <p style={{ color: t.text }}>{tr("forge.product_detail.not_found")}</p>;
@@ -101,6 +107,12 @@ export function ProductDetail({
101
107
  );
102
108
  }
103
109
 
110
+ /** The lowest numeric price, or undefined when none is a number. */
111
+ function lowestPrice(prices: (number | string | null | undefined)[] | undefined): number | undefined {
112
+ const numbers = (prices ?? []).map((p) => Number(p)).filter((n) => Number.isFinite(n));
113
+ return numbers.length ? Math.min(...numbers) : undefined;
114
+ }
115
+
104
116
  // ── Rich description (responsive media) ───────────────────────────────────────
105
117
  const RICH_CSS =
106
118
  ".forge-rich{overflow-wrap:break-word;line-height:1.6}" +
@@ -1,4 +1,5 @@
1
1
  import { useEffect } from "react";
2
+ import { captureInstallPrompt } from "../../data/queries/usePWA";
2
3
 
3
4
  export interface PwaRegistrationProps {
4
5
  /**
@@ -17,7 +18,7 @@ export interface PwaRegistrationProps {
17
18
  * Registers the Forge service worker (`/sw.js`) on the published site. Renders
18
19
  * nothing. The SW source lives in `@tribe-nest/forge/server`
19
20
  * (`forgeServiceWorkerScript()`) and is served by the starter's `/sw.js` route,
20
- * so a Forge version bump propagates SW fixes to every deployed site — never
21
+ * so a Forge version bump propagates SW fixes to every deployed site, never
21
22
  * frozen into per-site files.
22
23
  *
23
24
  * Gating (locked decision): registration is skipped unless `enabled` is true,
@@ -28,6 +29,9 @@ export interface PwaRegistrationProps {
28
29
  export function PwaRegistration({ enabled = false, swUrl = "/sw.js" }: PwaRegistrationProps) {
29
30
  useEffect(() => {
30
31
  if (!enabled) return;
32
+ // Hold the browser's one install prompt from the first page, so an app
33
+ // prompt shown later (after joining, after a purchase) can still use it.
34
+ captureInstallPrompt();
31
35
  if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) return;
32
36
  let cancelled = false;
33
37
  const register = () => {
@@ -18,13 +18,22 @@ export interface SignupFormProps {
18
18
  /** The membership invite token riding on the same redirect, if any. See `MembershipTierCallout`. */
19
19
  membershipInviteToken?: string;
20
20
  couponCode?: string;
21
+ /** Start with this email filled in (a client-portal invite's address, for one). */
22
+ defaultEmail?: string;
21
23
  }
22
24
 
23
25
  /** Themed account signup form, built on `useSignupForm`. */
24
- export function SignupForm({ onSuccess, loginHref, membershipTierId, membershipInviteToken, couponCode }: SignupFormProps) {
26
+ export function SignupForm({
27
+ onSuccess,
28
+ loginHref,
29
+ membershipTierId,
30
+ membershipInviteToken,
31
+ couponCode,
32
+ defaultEmail,
33
+ }: SignupFormProps) {
25
34
  const t = useForgeT();
26
35
  const theme = useForgeTheme();
27
- const form = useSignupForm({ onSuccess, membershipTierId, couponCode });
36
+ const form = useSignupForm({ onSuccess, membershipTierId, couponCode, defaultEmail });
28
37
 
29
38
  // One key for the whole sentence, split on its own `{email}` slot so the
30
39
  // address keeps its <strong> without the sentence being assembled from two
@@ -104,6 +113,7 @@ export function SignupForm({ onSuccess, loginHref, membershipTierId, membershipI
104
113
  value={form.code}
105
114
  onChange={(e) => form.setCode(e.target.value)}
106
115
  style={input}
116
+ data-testid="signup-code"
107
117
  placeholder="123456"
108
118
  />,
109
119
  )}
@@ -117,6 +127,7 @@ export function SignupForm({ onSuccess, loginHref, membershipTierId, membershipI
117
127
  value={form.firstName}
118
128
  onChange={(e) => form.setFirstName(e.target.value)}
119
129
  style={input}
130
+ data-testid="signup-first-name"
120
131
  placeholder={t("forge.signup_form.first_name_placeholder")}
121
132
  />,
122
133
  )}
@@ -126,6 +137,7 @@ export function SignupForm({ onSuccess, loginHref, membershipTierId, membershipI
126
137
  value={form.lastName}
127
138
  onChange={(e) => form.setLastName(e.target.value)}
128
139
  style={input}
140
+ data-testid="signup-last-name"
129
141
  placeholder={t("forge.signup_form.last_name_placeholder")}
130
142
  />,
131
143
  )}
@@ -137,6 +149,7 @@ export function SignupForm({ onSuccess, loginHref, membershipTierId, membershipI
137
149
  value={form.email}
138
150
  onChange={(e) => form.setEmail(e.target.value)}
139
151
  style={input}
152
+ data-testid="signup-email"
140
153
  placeholder={t("forge.signup_form.email_placeholder")}
141
154
  />,
142
155
  )}
@@ -147,6 +160,7 @@ export function SignupForm({ onSuccess, loginHref, membershipTierId, membershipI
147
160
  value={form.password}
148
161
  onChange={(e) => form.setPassword(e.target.value)}
149
162
  style={input}
163
+ data-testid="signup-password"
150
164
  placeholder={t("forge.signup_form.password_placeholder")}
151
165
  />,
152
166
  )}
@@ -155,13 +169,14 @@ export function SignupForm({ onSuccess, loginHref, membershipTierId, membershipI
155
169
  type="checkbox"
156
170
  checked={form.acceptedTerms}
157
171
  onChange={(e) => form.setAcceptedTerms(e.target.checked)}
172
+ data-testid="signup-accept-terms"
158
173
  style={{ accentColor: theme.colors.primary }}
159
174
  />
160
175
  {t("forge.signup_form.accept_terms")}
161
176
  </label>
162
177
  </>
163
178
  )}
164
- <button type="submit" disabled={form.isSubmitting} style={button}>
179
+ <button type="submit" disabled={form.isSubmitting} style={button} data-testid="signup-submit">
165
180
  {form.isSubmitting
166
181
  ? t("forge.signup_form.submitting")
167
182
  : form.awaitingCode
@@ -90,11 +90,11 @@ const booking = (overrides: Partial<MyBooking> = {}): MyBooking => {
90
90
 
91
91
  function render(rows: MyBooking[]): string {
92
92
  const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
93
- queryClient.setQueryData(["my-bookings", ACCOUNT_ID, PROFILE_ID, 1, 20], {
94
- data: rows,
95
- total: rows.length,
96
- page: 1,
97
- limit: 20,
93
+ // The list loads a page at a time (`useMyBookingsPaged`), so the cache holds
94
+ // infinite-query data: one page here.
95
+ queryClient.setQueryData(["my-bookings", ACCOUNT_ID, PROFILE_ID, "paged", 20], {
96
+ pages: [{ data: rows, total: rows.length, page: 1, limit: 20 }],
97
+ pageParams: [1],
98
98
  });
99
99
 
100
100
  return renderToStaticMarkup(
@@ -106,7 +106,7 @@ function render(rows: MyBooking[]): string {
106
106
  <ForgeThemeProvider
107
107
  theme={{ colors: { text: "#111111", background: "#ffffff", primary: "#6d28d9" }, cornerRadius: 8 }}
108
108
  >
109
- <AccountDashboard tab="bookings" onNavigateMembership={() => {}} />
109
+ <AccountDashboard tab="bookings" />
110
110
  </ForgeThemeProvider>
111
111
  </PublicAuthContext.Provider>
112
112
  </ForgeClientProvider>