@tribe-nest/forge 3.60.0 → 3.63.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 (220) hide show
  1. package/package.json +5 -2
  2. package/src/contexts/CartContext.tsx +34 -1
  3. package/src/contexts/PublicAuthContext.tsx +15 -3
  4. package/src/data/queries/_tests/metaSignals.spec.tsx +130 -0
  5. package/src/data/queries/useAccount.ts +33 -2
  6. package/src/data/queries/useAnalytics.ts +6 -6
  7. package/src/data/queries/useCallChat.ts +2 -2
  8. package/src/data/queries/useChat.ts +10 -1
  9. package/src/data/queries/useCheckouts.ts +2 -1
  10. package/src/data/queries/useCoachingAvailability.ts +10 -1
  11. package/src/data/queries/useCommunities.ts +2 -0
  12. package/src/data/queries/useCommunity.ts +362 -18
  13. package/src/data/queries/useCommunityAgent.ts +247 -0
  14. package/src/data/queries/useCommunityCalendar.ts +150 -0
  15. package/src/data/queries/useCommunityClassroom.ts +75 -0
  16. package/src/data/queries/useCommunityMembers.ts +108 -0
  17. package/src/data/queries/useCommunityModeration.ts +327 -0
  18. package/src/data/queries/useCommunityWiki.ts +137 -0
  19. package/src/data/queries/useCourses.ts +2 -0
  20. package/src/data/queries/useEmailSignIn.ts +79 -0
  21. package/src/data/queries/useEvents.ts +6 -1
  22. package/src/data/queries/useForms.ts +5 -1
  23. package/src/data/queries/useLeadMagnet.ts +6 -2
  24. package/src/data/queries/useMyBookings.ts +24 -1
  25. package/src/data/queries/useMyTickets.ts +24 -1
  26. package/src/data/queries/useNotifications.ts +6 -2
  27. package/src/data/queries/useOrders.ts +2 -1
  28. package/src/data/queries/usePWA.ts +111 -35
  29. package/src/data/queries/useSiteApp.ts +130 -0
  30. package/src/data/queries/useSubscriptions.ts +6 -1
  31. package/src/i18n/de.json +746 -1
  32. package/src/i18n/en.json +746 -1
  33. package/src/index.ts +20 -0
  34. package/src/runtime/pages.spec.ts +5 -0
  35. package/src/runtime/pages.ts +32 -6
  36. package/src/seo/_tests/seo.spec.ts +326 -0
  37. package/src/seo/applySeoToHead.ts +98 -0
  38. package/src/seo/index.ts +9 -0
  39. package/src/seo/resolve.ts +211 -0
  40. package/src/seo/seoHead.ts +61 -0
  41. package/src/seo/siteSeoHead.ts +97 -0
  42. package/src/seo/types.ts +99 -0
  43. package/src/server/_tests/siteBootstrap.spec.ts +65 -0
  44. package/src/server/index.ts +33 -4
  45. package/src/types/models.ts +11 -0
  46. package/src/ui/analytics/ForgeAnalytics.tsx +134 -34
  47. package/src/ui/analytics/PageMetaPixel.tsx +23 -8
  48. package/src/ui/analytics/_tests/ForgeAnalytics.meta.spec.tsx +129 -0
  49. package/src/ui/analytics/_tests/ForgeAnalytics.webVitals.spec.tsx +129 -0
  50. package/src/ui/analytics/_tests/metaCommerceEvents.spec.tsx +159 -0
  51. package/src/ui/analytics/useMetaEvents.ts +78 -0
  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/communityHarness.tsx +10 -3
  56. package/src/ui/headless/_tests/communityMembers.spec.tsx +316 -0
  57. package/src/ui/headless/_tests/communityPoll.spec.tsx +389 -0
  58. package/src/ui/headless/_tests/memberHomeBar.spec.ts +84 -0
  59. package/src/ui/headless/_tests/siteAppBar.spec.ts +83 -0
  60. package/src/ui/headless/_tests/siteAppHarness.ts +19 -0
  61. package/src/ui/headless/_tests/siteAppHome.spec.ts +226 -0
  62. package/src/ui/headless/_tests/siteAppPrompts.spec.ts +185 -0
  63. package/src/ui/headless/_tests/useCommunityJoin.spec.tsx +228 -14
  64. package/src/ui/headless/_tests/useCommunityMembership.spec.tsx +29 -1
  65. package/src/ui/headless/_tests/useCommunityModeration.spec.tsx +420 -0
  66. package/src/ui/headless/_tests/useCommunityReads.spec.tsx +11 -2
  67. package/src/ui/headless/_tests/useCommunityWiki.spec.tsx +224 -0
  68. package/src/ui/headless/_tests/useSiteApp.spec.tsx +52 -0
  69. package/src/ui/headless/_tests/wikiMediaImage.spec.tsx +132 -0
  70. package/src/ui/headless/auth/useEmailCodeSignIn.ts +186 -0
  71. package/src/ui/headless/auth/useSignupForm.ts +20 -1
  72. package/src/ui/headless/chat/useChatAttachmentUpload.ts +9 -6
  73. package/src/ui/headless/chat/useChatRoom.ts +35 -1
  74. package/src/ui/headless/communities/index.ts +41 -0
  75. package/src/ui/headless/communities/useCommunityCalendar.ts +156 -0
  76. package/src/ui/headless/communities/useCommunityClassroom.ts +84 -0
  77. package/src/ui/headless/communities/useCommunityHome.ts +3 -0
  78. package/src/ui/headless/communities/useCommunityJoin.ts +73 -23
  79. package/src/ui/headless/communities/useCommunityMembership.ts +48 -2
  80. package/src/ui/headless/communities/useCommunityWiki.ts +266 -0
  81. package/src/ui/headless/community/communityPoll.ts +297 -0
  82. package/src/ui/headless/community/countries.ts +123 -0
  83. package/src/ui/headless/community/index.ts +81 -1
  84. package/src/ui/headless/community/linkPreview.ts +71 -0
  85. package/src/ui/headless/community/richLexical.ts +27 -2
  86. package/src/ui/headless/community/useCommunityAgent.ts +285 -0
  87. package/src/ui/headless/community/useCommunityMembers.ts +175 -0
  88. package/src/ui/headless/community/useCommunityModeration.ts +300 -0
  89. package/src/ui/headless/community/useCommunitySpaceFeed.ts +22 -4
  90. package/src/ui/headless/community/useCommunityThread.ts +16 -3
  91. package/src/ui/headless/community/useMentionComposer.ts +17 -9
  92. package/src/ui/headless/community/useRichMentionComposer.ts +21 -5
  93. package/src/ui/headless/downloads/index.ts +8 -0
  94. package/src/ui/headless/downloads/useDownloadPage.ts +134 -0
  95. package/src/ui/headless/index.ts +13 -0
  96. package/src/ui/headless/memberHome/index.ts +10 -0
  97. package/src/ui/headless/memberHome/memberHomeBar.ts +91 -0
  98. package/src/ui/headless/memberHome/useMemberHomeNav.ts +56 -18
  99. package/src/ui/headless/portal/index.ts +57 -0
  100. package/src/ui/headless/portal/usePortal.ts +274 -0
  101. package/src/ui/headless/portal/usePortalFiles.ts +68 -0
  102. package/src/ui/headless/portal/usePortalMessages.ts +189 -0
  103. package/src/ui/headless/portal/usePortalPreview.ts +142 -0
  104. package/src/ui/headless/siteApp/appBar.ts +102 -0
  105. package/src/ui/headless/siteApp/appHome.ts +344 -0
  106. package/src/ui/headless/siteApp/appPrompts.ts +229 -0
  107. package/src/ui/headless/siteApp/index.ts +58 -0
  108. package/src/ui/headless/siteApp/useAppHome.ts +210 -0
  109. package/src/ui/headless/siteApp/useAppInstallRequest.ts +16 -0
  110. package/src/ui/headless/work/index.ts +2 -0
  111. package/src/ui/headless/work/useWorkPortal.ts +37 -26
  112. package/src/ui/index.ts +11 -0
  113. package/src/ui/styled/AccountDashboard.tsx +712 -352
  114. package/src/ui/styled/AiAgentWidget.tsx +13 -1
  115. package/src/ui/styled/ChatRoom.tsx +126 -16
  116. package/src/ui/styled/Checkout.tsx +8 -0
  117. package/src/ui/styled/CheckoutConfirmation.tsx +8 -0
  118. package/src/ui/styled/CoachingConfirmation.tsx +8 -0
  119. package/src/ui/styled/CoachingDetail.tsx +2 -0
  120. package/src/ui/styled/CourseConfirmation.tsx +5 -0
  121. package/src/ui/styled/CourseDetail.tsx +2 -0
  122. package/src/ui/styled/DownloadPage.tsx +263 -0
  123. package/src/ui/styled/EventConfirmation.tsx +10 -0
  124. package/src/ui/styled/EventDetail.tsx +10 -0
  125. package/src/ui/styled/FilmLibrary.tsx +22 -9
  126. package/src/ui/styled/MusicLinkPage.tsx +5 -5
  127. package/src/ui/styled/ProductDetail.tsx +12 -0
  128. package/src/ui/styled/PwaRegistration.tsx +5 -1
  129. package/src/ui/styled/SignupForm.tsx +18 -3
  130. package/src/ui/styled/_tests/AccountDashboardBookingCall.spec.tsx +5 -5
  131. package/src/ui/styled/_tests/AccountNotifications.spec.tsx +135 -0
  132. package/src/ui/styled/_tests/DownloadPage.spec.tsx +68 -0
  133. package/src/ui/styled/app/AppBar.tsx +298 -0
  134. package/src/ui/styled/app/AppHome.tsx +1139 -0
  135. package/src/ui/styled/app/AppHomeMark.tsx +66 -0
  136. package/src/ui/styled/app/AppPrompts.tsx +464 -0
  137. package/src/ui/styled/app/_tests/AppBar.spec.tsx +96 -0
  138. package/src/ui/styled/app/_tests/AppHome.spec.tsx +209 -0
  139. package/src/ui/styled/app/_tests/AppPrompts.spec.tsx +232 -0
  140. package/src/ui/styled/app/index.ts +7 -0
  141. package/src/ui/styled/chat/ChatThreadPanel.tsx +29 -4
  142. package/src/ui/styled/chat/MessageBubble.tsx +43 -6
  143. package/src/ui/styled/communities/CommunityAbout.tsx +6 -1
  144. package/src/ui/styled/communities/CommunityCalendar.tsx +484 -0
  145. package/src/ui/styled/communities/CommunityClassroom.tsx +302 -0
  146. package/src/ui/styled/communities/CommunityHome.tsx +134 -11
  147. package/src/ui/styled/communities/CommunityJoinSheet.tsx +119 -19
  148. package/src/ui/styled/communities/CommunityMembershipMenu.tsx +168 -52
  149. package/src/ui/styled/communities/CommunityPublicWiki.tsx +75 -0
  150. package/src/ui/styled/communities/CommunityWiki.tsx +289 -0
  151. package/src/ui/styled/communities/MyMembershipsList.tsx +31 -5
  152. package/src/ui/styled/communities/_tests/CommunityAppHome.spec.tsx +92 -0
  153. package/src/ui/styled/communities/_tests/CommunityClassroomCalendar.spec.tsx +453 -0
  154. package/src/ui/styled/communities/_tests/CommunityMobile.spec.tsx +399 -0
  155. package/src/ui/styled/communities/_tests/CommunityWiki.spec.tsx +223 -0
  156. package/src/ui/styled/communities/index.ts +9 -0
  157. package/src/ui/styled/communities/shared.tsx +39 -11
  158. package/src/ui/styled/community/CommunityAgent.tsx +286 -0
  159. package/src/ui/styled/community/CommunityComposer.tsx +266 -15
  160. package/src/ui/styled/community/CommunityFeed.tsx +50 -1
  161. package/src/ui/styled/community/CommunityLinkPreview.tsx +159 -0
  162. package/src/ui/styled/community/CommunityMemberGlobe.tsx +171 -0
  163. package/src/ui/styled/community/CommunityMemberProfile.tsx +137 -4
  164. package/src/ui/styled/community/CommunityMembers.tsx +387 -0
  165. package/src/ui/styled/community/CommunityModeration.tsx +587 -0
  166. package/src/ui/styled/community/CommunityModerationState.tsx +127 -0
  167. package/src/ui/styled/community/CommunityModeratorMenu.tsx +317 -0
  168. package/src/ui/styled/community/CommunityNotifications.tsx +52 -6
  169. package/src/ui/styled/community/CommunityPollCard.tsx +250 -0
  170. package/src/ui/styled/community/CommunityPostCard.tsx +110 -34
  171. package/src/ui/styled/community/CommunityPostDetail.tsx +157 -71
  172. package/src/ui/styled/community/CommunityRichText.tsx +177 -32
  173. package/src/ui/styled/community/CommunitySpaces.tsx +49 -14
  174. package/src/ui/styled/community/_tests/CommunityAgent.spec.tsx +571 -0
  175. package/src/ui/styled/community/_tests/CommunityLinkPreview.spec.tsx +167 -0
  176. package/src/ui/styled/community/_tests/CommunityMembers.spec.tsx +137 -0
  177. package/src/ui/styled/community/_tests/CommunityModeration.spec.tsx +291 -0
  178. package/src/ui/styled/community/_tests/CommunityPoll.spec.tsx +386 -0
  179. package/src/ui/styled/community/index.ts +37 -2
  180. package/src/ui/styled/forge-utilities.css +234 -0
  181. package/src/ui/styled/members/CallRecordingActions.tsx +182 -0
  182. package/src/ui/styled/members/MemberCallsCalendar.tsx +13 -158
  183. package/src/ui/styled/members/MemberHomeLayout.tsx +295 -15
  184. package/src/ui/styled/members/MemberPostsFeed.tsx +3 -2
  185. package/src/ui/styled/members/index.ts +8 -0
  186. package/src/ui/styled/portal/PortalAccountSection.tsx +61 -0
  187. package/src/ui/styled/portal/PortalActivity.tsx +167 -0
  188. package/src/ui/styled/portal/PortalBilling.tsx +197 -0
  189. package/src/ui/styled/portal/PortalFiles.tsx +279 -0
  190. package/src/ui/styled/portal/PortalHome.tsx +401 -0
  191. package/src/ui/styled/portal/PortalMessages.tsx +300 -0
  192. package/src/ui/styled/portal/PortalPreviewBanner.tsx +75 -0
  193. package/src/ui/styled/portal/PortalProjects.tsx +407 -0
  194. package/src/ui/styled/portal/PortalShell.tsx +397 -0
  195. package/src/ui/styled/portal/_tests/PortalAppHome.spec.tsx +90 -0
  196. package/src/ui/styled/portal/_tests/PortalFiles.spec.tsx +159 -0
  197. package/src/ui/styled/portal/_tests/PortalHome.spec.tsx +148 -0
  198. package/src/ui/styled/portal/_tests/PortalMessages.spec.tsx +168 -0
  199. package/src/ui/styled/portal/_tests/PortalPreviewBanner.spec.tsx +132 -0
  200. package/src/ui/styled/portal/_tests/portalNav.spec.ts +123 -0
  201. package/src/ui/styled/portal/index.ts +13 -0
  202. package/src/ui/styled/portal/portalNav.ts +260 -0
  203. package/src/ui/styled/portal/shared.tsx +206 -0
  204. package/src/ui/styled/work/WorkInviteAccept.tsx +228 -168
  205. package/src/ui/styled/work/WorkPortalProjects.tsx +68 -120
  206. package/src/ui/styled/work/WorkProjectDocuments.tsx +99 -128
  207. package/src/ui/styled/work/WorkProjectInvoices.tsx +91 -86
  208. package/src/ui/styled/work/WorkProjectReport.tsx +11 -17
  209. package/src/ui/styled/work/WorkReportBody.tsx +223 -248
  210. package/src/ui/styled/work/WorkTaskAttachments.tsx +23 -29
  211. package/src/ui/styled/work/WorkTaskComments.tsx +75 -102
  212. package/src/ui/styled/work/WorkTaskDetail.tsx +48 -59
  213. package/src/ui/styled/work/WorkTokenReport.tsx +8 -16
  214. package/src/ui/styled/work/index.ts +1 -1
  215. package/src/utils/_tests/landing.spec.ts +39 -0
  216. package/src/utils/_tests/metaEvents.spec.ts +264 -0
  217. package/src/utils/_tests/structuredDataSeo.spec.ts +247 -0
  218. package/src/utils/landing.ts +36 -10
  219. package/src/utils/metaEvents.ts +302 -0
  220. package/src/utils/structuredData.ts +339 -1
@@ -0,0 +1,58 @@
1
+ // The site app (one installable app per website): the bar model, the home
2
+ // sections, and the install and notification prompts.
3
+ export {
4
+ APP_BAR_HREFS,
5
+ buildAppBar,
6
+ isAppBarItemActive,
7
+ normaliseBarItems,
8
+ type AppBarItem,
9
+ type AppBarKey,
10
+ type AppBarModel,
11
+ } from "./appBar";
12
+ export {
13
+ APP_HOME_LIST_LIMIT,
14
+ APP_PORTAL_TILES,
15
+ attentionAction,
16
+ buildAppHomeSections,
17
+ buildPortalTiles,
18
+ courseProgress,
19
+ greetingPart,
20
+ isSafeLinkHref,
21
+ pickLearningCourse,
22
+ pickNextCall,
23
+ pickNextTicket,
24
+ upcomingEvents,
25
+ type AppAttentionAction,
26
+ type AppCommunityCard,
27
+ type AppCourseProgress,
28
+ type AppHomeData,
29
+ type AppHomeSectionView,
30
+ type AppLearningCourse,
31
+ type AppNextCall,
32
+ type AppNextTicket,
33
+ type AppPortalTile,
34
+ type AppPortalTileKey,
35
+ } from "./appHome";
36
+ export { useAppHome, useAppCommunityUnread, type UseAppHomeResult } from "./useAppHome";
37
+ export { useAppInstallRequest } from "./useAppInstallRequest";
38
+ export {
39
+ APP_INSTALL_CONTEXTS,
40
+ APP_INSTALL_SNOOZE_MS,
41
+ APP_PUSH_CATEGORIES,
42
+ clearPendingAppInstall,
43
+ decideAppInstall,
44
+ decideAppPush,
45
+ pushPreferencePatch,
46
+ readInstallDismissedAt,
47
+ readPendingAppInstall,
48
+ readPushAnswer,
49
+ rememberInstallDismissed,
50
+ rememberPushAnswer,
51
+ requestAppInstall,
52
+ subscribeAppInstallRequests,
53
+ type AppInstallContext,
54
+ type AppInstallMode,
55
+ type AppInstallRequest,
56
+ type AppPushAnswer,
57
+ type AppPushCategoryKey,
58
+ } from "./appPrompts";
@@ -0,0 +1,210 @@
1
+ import { useContext, useMemo } from "react";
2
+ import { useQuery } from "@tanstack/react-query";
3
+ import { useForge } from "../../../provider/ForgeProvider";
4
+ import { PublicAuthContext } from "../../../contexts/PublicAuthContext";
5
+ import {
6
+ SITE_APP_DEFAULT_SECTIONS,
7
+ useSiteApp,
8
+ type SiteAppHomeSection,
9
+ type SiteAppSectionKey,
10
+ } from "../../../data/queries/useSiteApp";
11
+ import { useMyCommunityMemberships, usePublicCommunities } from "../../../data/queries/useCommunities";
12
+ import { communityKeys } from "../../../data/queries/useCommunity";
13
+ import { useMyBookings } from "../../../data/queries/useMyBookings";
14
+ import { useMyTickets } from "../../../data/queries/useMyTickets";
15
+ import { useMyCourses } from "../../../data/queries/useMyCourses";
16
+ import { useCourseAccess } from "../../../data/queries/useCourseAccess";
17
+ import { useGetCourses } from "../../../data/queries/useCourses";
18
+ import { useGetProducts } from "../../../data/queries/useProducts";
19
+ import { membershipCallKeys, type MyMembershipCall } from "../../../data/queries/useMembershipCalls";
20
+ import type { IEvent } from "../../../types/models";
21
+ import { internalPath } from "../../../runtime/pages";
22
+ import { getMembershipAccess } from "../../../utils/membershipAccess";
23
+ import { usePortalSummary } from "../portal/usePortal";
24
+ import { communityAboutPath, communityHomePath } from "../communities/paths";
25
+ import {
26
+ buildAppHomeSections,
27
+ buildPortalTiles,
28
+ courseProgress,
29
+ pickLearningCourse,
30
+ pickNextCall,
31
+ pickNextTicket,
32
+ upcomingEvents,
33
+ type AppCommunityCard,
34
+ type AppHomeSectionView,
35
+ type AppLearningCourse,
36
+ } from "./appHome";
37
+
38
+ export interface UseAppHomeResult {
39
+ /** The settings or the account are still being read: draw a loading state. */
40
+ isLoading: boolean;
41
+ /** Signed out: the home is the sign-in screen. */
42
+ isSignedIn: boolean;
43
+ /** The app's name (the website's, unless the creator renamed the app). */
44
+ appName: string | null;
45
+ iconUrl: string | null;
46
+ /** The member's first name, for the greeting. */
47
+ firstName: string | null;
48
+ /** The sections to draw, in order. Empty while loading. */
49
+ sections: AppHomeSectionView[];
50
+ }
51
+
52
+ /**
53
+ * The data behind the site app's home (`/i/app`).
54
+ *
55
+ * Reads the website's app settings, then only what the configured sections
56
+ * need: a section the creator hid costs no request. Signed out, nothing
57
+ * member-shaped is fetched at all. Built on the same public hooks the portal
58
+ * and the communities use, so the home never disagrees with the page a tile
59
+ * opens. `buildAppHomeSections` decides order, visibility and the empty-skip.
60
+ */
61
+ export function useAppHome(opts: { now?: Date } = {}): UseAppHomeResult {
62
+ const { client, profileId } = useForge();
63
+ const auth = useContext(PublicAuthContext);
64
+ const user = auth?.user ?? null;
65
+ const signedIn = !!user;
66
+ const now = opts.now ?? new Date();
67
+
68
+ const app = useSiteApp();
69
+ // An unknown host (a preview) answers 404: the contract's defaults stand in.
70
+ const config: readonly SiteAppHomeSection[] | undefined =
71
+ app.data?.homeSections ?? (app.isError ? SITE_APP_DEFAULT_SECTIONS : undefined);
72
+ const wants = (key: SiteAppSectionKey) =>
73
+ !!config?.some((entry) => entry.type === "builtin" && entry.key === key && entry.visible !== false);
74
+
75
+ const summary = usePortalSummary();
76
+ const sectionsCount = summary.data?.sections;
77
+
78
+ const wantsCommunities = wants("communities") || wants("explore_communities");
79
+ const mine = useMyCommunityMemberships({ enabled: signedIn && wantsCommunities });
80
+ const everyCommunity = usePublicCommunities({ enabled: wantsCommunities });
81
+
82
+ const comingUp = signedIn && wants("coming_up");
83
+ const calls = useQuery<MyMembershipCall[]>({
84
+ // The member's whole call calendar, across communities and courses.
85
+ queryKey: [...membershipCallKeys.mine(profileId), "now", "open", "app-home", user?.id ?? null],
86
+ queryFn: async () =>
87
+ (await client.get("/public/scheduled-calls/mine", { params: { profileId, limit: 5 } })).data as MyMembershipCall[],
88
+ enabled: comingUp && !!profileId && !!client,
89
+ refetchInterval: 60_000,
90
+ });
91
+ const bookings = useMyBookings(comingUp && (sectionsCount?.bookings ?? 0) > 0 ? user?.id : undefined, 1, 20);
92
+ const tickets = useMyTickets(comingUp && (sectionsCount?.tickets ?? 0) > 0 ? user?.id : undefined, 1, 20);
93
+
94
+ const myCourses = useMyCourses({ accountId: user?.id, enabled: signedIn && wants("learning") });
95
+ const learningCourse = pickLearningCourse(myCourses.data);
96
+ const access = useCourseAccess(learningCourse?.accessId);
97
+
98
+ const wantsEvents = wants("events");
99
+ const events = useQuery<IEvent[]>({
100
+ queryKey: ["events", profileId],
101
+ queryFn: async () => (await client.get("/public/events", { params: { profileId } })).data as IEvent[],
102
+ enabled: wantsEvents && !!profileId && !!client,
103
+ });
104
+ const courses = useGetCourses(undefined, wants("courses"));
105
+ const products = useGetProducts(undefined, wants("shop"));
106
+
107
+ const sections = useMemo(() => {
108
+ if (!config) return [];
109
+ const byId = new Map((everyCommunity.data ?? []).map((community) => [community.id, community]));
110
+ const memberships = (mine.data ?? []).filter(
111
+ (row) => getMembershipAccess({ status: row.status, cancelAtPeriodEnd: !!row.cancelAtPeriodEnd }).hasAccess,
112
+ );
113
+ const joined = new Set(memberships.map((row) => row.community.id));
114
+ const communities: AppCommunityCard[] = memberships.map((row) => {
115
+ const full = byId.get(row.community.id);
116
+ return {
117
+ id: row.community.id,
118
+ name: row.community.name,
119
+ slug: row.community.slug,
120
+ logoUrl: full?.logoUrl ?? null,
121
+ coverUrl: full?.coverUrl ?? null,
122
+ tierName: row.tierName ?? row.membershipTier?.name ?? null,
123
+ memberCount: full?.memberCount ?? null,
124
+ href: communityHomePath(row.community.slug),
125
+ };
126
+ });
127
+ // Signed out, or a membership read that failed, lists nothing as "yours";
128
+ // explore then shows every community rather than none.
129
+ const explore: AppCommunityCard[] = (everyCommunity.data ?? [])
130
+ .filter((community) => !joined.has(community.id) && !(mine.data ?? []).some((row) => row.community.id === community.id))
131
+ .map((community) => ({
132
+ id: community.id,
133
+ name: community.name,
134
+ slug: community.slug,
135
+ logoUrl: community.logoUrl,
136
+ coverUrl: community.coverUrl,
137
+ tierName: null,
138
+ memberCount: community.memberCount,
139
+ href: communityAboutPath(community.slug),
140
+ joinMode: community.joinMode ?? "open",
141
+ }));
142
+ const learning: AppLearningCourse | null = learningCourse
143
+ ? {
144
+ accessId: learningCourse.accessId,
145
+ title: learningCourse.title,
146
+ href: internalPath("course-access/$accessId", { accessId: learningCourse.accessId }),
147
+ progress: courseProgress(access.data),
148
+ }
149
+ : null;
150
+ return buildAppHomeSections(config, {
151
+ attention: summary.data?.attention ?? [],
152
+ portalTiles: buildPortalTiles(summary.data),
153
+ communities,
154
+ nextCall: pickNextCall(calls.data, bookings.data?.data, now),
155
+ nextTicket: pickNextTicket(tickets.data?.data, now),
156
+ learning,
157
+ explore,
158
+ events: upcomingEvents(events.data, now),
159
+ courses: courses.data?.data ?? [],
160
+ products: products.data?.data ?? [],
161
+ });
162
+ // `now` is read per render on purpose; the inputs below are what change the answer.
163
+ // eslint-disable-next-line react-hooks/exhaustive-deps
164
+ }, [
165
+ config,
166
+ everyCommunity.data,
167
+ mine.data,
168
+ learningCourse,
169
+ access.data,
170
+ summary.data,
171
+ calls.data,
172
+ bookings.data,
173
+ tickets.data,
174
+ events.data,
175
+ courses.data,
176
+ products.data,
177
+ ]);
178
+
179
+ const authReady = auth ? auth.isInitialized !== false : true;
180
+ const isLoading = !config || !authReady || (signedIn && summary.isLoading);
181
+
182
+ return {
183
+ isLoading,
184
+ isSignedIn: signedIn,
185
+ appName: app.data?.appName ?? null,
186
+ iconUrl: app.data?.iconUrl ?? null,
187
+ firstName: user?.firstName?.trim() || null,
188
+ sections: isLoading ? [] : sections,
189
+ };
190
+ }
191
+
192
+ /**
193
+ * Unread notifications in one community, for its card on the app home. The
194
+ * same request and cache entry as the community's own bell, so opening the
195
+ * community and coming back shows the same number.
196
+ */
197
+ export function useAppCommunityUnread(communityId: string | undefined, enabled = true) {
198
+ const { client, profileId } = useForge();
199
+ return useQuery<number>({
200
+ queryKey: communityKeys.unread(communityId),
201
+ queryFn: async () => {
202
+ const res = await client.get("/public/community/notifications", {
203
+ params: { profileId, communityId, page: 1, limit: 1, unreadOnly: true },
204
+ });
205
+ return res.data.unreadCount as number;
206
+ },
207
+ enabled: enabled && !!profileId && !!communityId && !!client,
208
+ refetchInterval: 30_000,
209
+ });
210
+ }
@@ -0,0 +1,16 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { requestAppInstall, type AppInstallContext } from "./appPrompts";
3
+
4
+ /**
5
+ * Ask for the app install once, the first time `ready` is true (a payment
6
+ * confirmed, a booking confirmed). For a success that is a callback rather
7
+ * than a state, call `requestAppInstall` directly.
8
+ */
9
+ export function useAppInstallRequest(ready: boolean, context: AppInstallContext): void {
10
+ const asked = useRef(false);
11
+ useEffect(() => {
12
+ if (!ready || asked.current) return;
13
+ asked.current = true;
14
+ requestAppInstall(context);
15
+ }, [ready, context]);
16
+ }
@@ -7,6 +7,8 @@ export {
7
7
  useWorkTaskAttachments,
8
8
  useWorkInvite,
9
9
  useAcceptWorkInvite,
10
+ rememberSignupEmail,
11
+ SIGNUP_EMAIL_PREFILL_KEY,
10
12
  useWorkProjectInvoices,
11
13
  useWorkSharedDocuments,
12
14
  useWorkOpenSharedDocument,
@@ -129,25 +129,19 @@ export interface WorkInviteContext {
129
129
  email: string;
130
130
  firstName: string | null;
131
131
  /**
132
- * The invited address already has an account, so the invite grants portal
133
- * access but cannot set a credential: render "sign in", not a password
134
- * form (C10).
132
+ * The invited address has no account yet. The invite never creates one: the
133
+ * page sends the person through the normal signup (which verifies the
134
+ * address with an emailed code) and back.
135
135
  */
136
+ needsAccount: boolean;
137
+ /** The complement of `needsAccount`: sign in, then come back. */
136
138
  requiresExistingLogin?: boolean;
137
139
  }
138
140
 
141
+ /** POST /public/work/invite/accept, made as the signed-in invited account. */
139
142
  export interface WorkInviteAcceptResult {
140
- /**
141
- * True when the invited address already has an account: the invite granted
142
- * portal access but issued NO session, and the person signs in normally
143
- * (C10). `token` is absent in that case.
144
- */
145
- requiresLogin?: boolean;
146
- token?: string;
147
- /** Rotating refresh token for the portal session (backend §7). Persist it. */
148
- refreshToken?: string;
143
+ accepted: true;
149
144
  projectId: string;
150
- account: { id: string; email: string; firstName: string | null; lastName: string | null };
151
145
  }
152
146
 
153
147
  // ── Query keys ───────────────────────────────────────────────────────────────
@@ -410,22 +404,23 @@ export function useUploadWorkDocument(projectId: string) {
410
404
  });
411
405
  }
412
406
 
413
- // ── Client invite accept (public, no auth) ───────────────────────────────────
407
+ // ── Client invite accept ─────────────────────────────────────────────────────
414
408
 
415
409
  /**
416
410
  * Fetch the branded context for an invite link (GET /public/work/invite).
417
- * Public: no bearer token needed; the link's token IS the credential.
418
- * `null`/errored when the token or project is missing/invalid/expired.
411
+ * Public: no bearer token needed. Sends the site's `profileId`, so a token for
412
+ * another tenant's project is refused here. Errors when the token or project is
413
+ * missing, invalid, expired or already used.
419
414
  */
420
415
  export function useWorkInvite(
421
416
  token: string | undefined,
422
417
  projectId: string | undefined,
423
418
  ): UseQueryResult<WorkInviteContext> {
424
- const { client } = useForge();
419
+ const { client, profileId } = useForge();
425
420
  return useQuery<WorkInviteContext>({
426
- queryKey: ["work-invite", token, projectId],
421
+ queryKey: ["work-invite", token, projectId, profileId],
427
422
  queryFn: async () => {
428
- const res = await client.get(`/public/work/invite`, { params: { token, projectId } });
423
+ const res = await client.get(`/public/work/invite`, { params: { token, projectId, profileId } });
429
424
  return res.data as WorkInviteContext;
430
425
  },
431
426
  enabled: !!client && !!token && !!projectId,
@@ -434,20 +429,36 @@ export function useWorkInvite(
434
429
  }
435
430
 
436
431
  /**
437
- * Accept an invite (POST /public/work/invite/accept): set a password and
438
- * receive a fresh portal session token. The caller persists the token +
439
- * navigates. Public: no bearer token needed.
432
+ * Accept an invite (POST /public/work/invite/accept) as the SIGNED-IN account.
433
+ * The account's email must be the invited address; nothing is created and no
434
+ * password is involved.
440
435
  */
441
436
  export function useAcceptWorkInvite() {
442
- const { client } = useForge();
443
- return useMutation<WorkInviteAcceptResult, unknown, { token: string; projectId: string; password: string }>({
444
- mutationFn: async ({ token, projectId, password }) => {
445
- const res = await client.post(`/public/work/invite/accept`, { token, projectId, password });
437
+ const { client, profileId } = useForge();
438
+ return useMutation<WorkInviteAcceptResult, unknown, { token: string; projectId: string }>({
439
+ mutationFn: async ({ token, projectId }) => {
440
+ const res = await client.post(`/public/work/invite/accept`, { token, projectId, profileId });
446
441
  return res.data as WorkInviteAcceptResult;
447
442
  },
448
443
  });
449
444
  }
450
445
 
446
+ /** Session-storage key the signup form reads its prefilled email from. */
447
+ export const SIGNUP_EMAIL_PREFILL_KEY = "forge:signup-email";
448
+
449
+ /**
450
+ * Hand an email to the next signup form in this tab, without putting it in a
451
+ * URL (where analytics would record it). Read once by `useSignupForm`.
452
+ */
453
+ export function rememberSignupEmail(email: string | null | undefined) {
454
+ if (!email || typeof window === "undefined") return;
455
+ try {
456
+ window.sessionStorage.setItem(SIGNUP_EMAIL_PREFILL_KEY, email);
457
+ } catch {
458
+ // Storage unavailable: the person types it.
459
+ }
460
+ }
461
+
451
462
  // ── Presentational helpers ───────────────────────────────────────────────────
452
463
 
453
464
  /** Major-unit amount → display string using the project currency symbol. */
package/src/ui/index.ts CHANGED
@@ -148,7 +148,9 @@ export { ForgotPasswordForm, type ForgotPasswordFormProps } from "./styled/Forgo
148
148
  export { ResetPasswordForm, type ResetPasswordFormProps } from "./styled/ResetPasswordForm";
149
149
  export {
150
150
  AccountDashboard,
151
+ AccountSection,
151
152
  type AccountDashboardProps,
153
+ type AccountSectionProps,
152
154
  type AccountTabKey,
153
155
  ACCOUNT_TABS,
154
156
  } from "./styled/AccountDashboard";
@@ -161,6 +163,7 @@ export {
161
163
  export { AudioPlayer, type AudioPlayerProps } from "./styled/AudioPlayer";
162
164
  export { ForgeAnalytics, type ForgeAnalyticsProps } from "./analytics/ForgeAnalytics";
163
165
  export { PageMetaPixel, usePageMetaPixel, type PageMetaPixelProps } from "./analytics/PageMetaPixel";
166
+ export { useMetaViewContent, useMetaPurchase, useMetaInitiateCheckout, type MetaViewContentInput } from "./analytics/useMetaEvents";
164
167
  export { CookieConsent, type CookieConsentProps } from "./styled/CookieConsent";
165
168
  export { Loading, type LoadingProps } from "./styled/Loading";
166
169
  /** The inventory-hold countdown + its lapsed/409 state. See `useInventoryHold`. */
@@ -374,6 +377,13 @@ export * from "./styled/communities";
374
377
  // Work (project management) client-portal styled blocks.
375
378
  export * from "./styled/work";
376
379
 
380
+ // The client portal (`/i/account/*`): shell, home, projects, invoices, documents, activity.
381
+ export * from "./styled/portal";
382
+
383
+ // The site app (`/i/app`): its home, the bottom bar a custom start page can
384
+ // drop in (`<AppBar />`), and the install and notification prompts.
385
+ export * from "./styled/app";
386
+
377
387
  // A community's member home (`/i/communities/<slug>/home`) styled blocks.
378
388
  export * from "./styled/members";
379
389
 
@@ -385,3 +395,4 @@ export * from "./styled/ReviewRequestPage";
385
395
  // it, at BOTH the canonical `/i/documents/$token` and the older
386
396
  // `/documents/$token` that already-sent links point at.
387
397
  export * from "./styled/DocumentSigningPage";
398
+ export * from "./styled/DownloadPage";