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