@tribe-nest/forge 3.60.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/contexts/CartContext.tsx +34 -1
- 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 +10 -1
- package/src/data/queries/useCheckouts.ts +2 -1
- package/src/data/queries/useCoachingAvailability.ts +10 -1
- package/src/data/queries/useCommunities.ts +2 -0
- package/src/data/queries/useCommunity.ts +362 -18
- 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/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/useSiteApp.ts +130 -0
- package/src/data/queries/useSubscriptions.ts +6 -1
- package/src/i18n/de.json +746 -1
- package/src/i18n/en.json +746 -1
- package/src/index.ts +17 -0
- package/src/runtime/pages.spec.ts +5 -0
- package/src/runtime/pages.ts +32 -6
- 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/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/communityHarness.tsx +10 -3
- 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/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/useCommunityJoin.spec.tsx +228 -14
- package/src/ui/headless/_tests/useCommunityMembership.spec.tsx +29 -1
- package/src/ui/headless/_tests/useCommunityModeration.spec.tsx +420 -0
- package/src/ui/headless/_tests/useCommunityReads.spec.tsx +11 -2
- 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 +20 -1
- package/src/ui/headless/chat/useChatAttachmentUpload.ts +9 -6
- package/src/ui/headless/chat/useChatRoom.ts +35 -1
- package/src/ui/headless/communities/index.ts +41 -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 +3 -0
- package/src/ui/headless/communities/useCommunityJoin.ts +73 -23
- package/src/ui/headless/communities/useCommunityMembership.ts +48 -2
- package/src/ui/headless/communities/useCommunityWiki.ts +266 -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/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 +13 -0
- package/src/ui/headless/memberHome/index.ts +10 -0
- package/src/ui/headless/memberHome/memberHomeBar.ts +91 -0
- package/src/ui/headless/memberHome/useMemberHomeNav.ts +56 -18
- 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/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 +11 -0
- package/src/ui/styled/AccountDashboard.tsx +712 -352
- 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/MusicLinkPage.tsx +5 -5
- 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 +5 -5
- package/src/ui/styled/_tests/AccountNotifications.spec.tsx +135 -0
- package/src/ui/styled/_tests/DownloadPage.spec.tsx +68 -0
- 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/CommunityAbout.tsx +6 -1
- 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 +134 -11
- package/src/ui/styled/communities/CommunityJoinSheet.tsx +119 -19
- package/src/ui/styled/communities/CommunityMembershipMenu.tsx +168 -52
- package/src/ui/styled/communities/CommunityPublicWiki.tsx +75 -0
- package/src/ui/styled/communities/CommunityWiki.tsx +289 -0
- package/src/ui/styled/communities/MyMembershipsList.tsx +31 -5
- 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/CommunityWiki.spec.tsx +223 -0
- package/src/ui/styled/communities/index.ts +9 -0
- package/src/ui/styled/communities/shared.tsx +39 -11
- 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/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/index.ts +37 -2
- package/src/ui/styled/forge-utilities.css +234 -0
- package/src/ui/styled/members/CallRecordingActions.tsx +182 -0
- package/src/ui/styled/members/MemberCallsCalendar.tsx +13 -158
- package/src/ui/styled/members/MemberHomeLayout.tsx +295 -15
- package/src/ui/styled/members/MemberPostsFeed.tsx +3 -2
- 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/landing.ts +19 -6
- package/src/utils/metaEvents.ts +302 -0
package/src/index.ts
CHANGED
|
@@ -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,6 +155,18 @@ 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";
|
|
154
170
|
// Communities: the list, one community, the viewer's state in it, invites,
|
|
155
171
|
// upgrades and the fan's memberships across communities.
|
|
156
172
|
export * from "./data/queries/useCommunities";
|
|
@@ -176,6 +192,7 @@ export * from "./content";
|
|
|
176
192
|
export * from "./utils/attribution";
|
|
177
193
|
export * from "./utils/landing";
|
|
178
194
|
export * from "./utils/metaPixel";
|
|
195
|
+
export * from "./utils/metaEvents";
|
|
179
196
|
export * from "./utils/cookieConsent";
|
|
180
197
|
export * from "./utils/structuredData";
|
|
181
198
|
// Paystack inline checkout: the ONE implementation of the popup, exported so a
|
|
@@ -47,15 +47,20 @@ describe("internal page links", () => {
|
|
|
47
47
|
"communities/$slug/home",
|
|
48
48
|
"communities/$slug/home/calendar",
|
|
49
49
|
"communities/$slug/home/chat",
|
|
50
|
+
"communities/$slug/home/classroom",
|
|
50
51
|
"communities/$slug/home/collections",
|
|
51
52
|
"communities/$slug/home/collections/$collectionId",
|
|
52
53
|
"communities/$slug/home/leaderboard",
|
|
54
|
+
"communities/$slug/home/members",
|
|
55
|
+
"communities/$slug/home/moderation",
|
|
53
56
|
"communities/$slug/home/notifications",
|
|
54
57
|
"communities/$slug/home/posts",
|
|
55
58
|
"communities/$slug/home/profile/$accountId",
|
|
56
59
|
"communities/$slug/home/spaces",
|
|
57
60
|
"communities/$slug/home/spaces/$spaceId",
|
|
58
61
|
"communities/$slug/home/spaces/posts/$postId",
|
|
62
|
+
"communities/$slug/home/wiki",
|
|
63
|
+
"communities/$slug/home/wiki/$page",
|
|
59
64
|
]);
|
|
60
65
|
const removed = INTERNAL_PAGES.filter((page) => /^(membership|members|community|chat)(\/|$)/.test(page));
|
|
61
66
|
expect(removed).toEqual([]);
|
package/src/runtime/pages.ts
CHANGED
|
@@ -9,12 +9,16 @@
|
|
|
9
9
|
*
|
|
10
10
|
* The page list is the CONTRACT between a site and the hosted pages, so it
|
|
11
11
|
* normally only grows: removing a page breaks links on sites already published.
|
|
12
|
-
* The
|
|
12
|
+
* The first deliberate exception is Communities (2026-09-27). A profile can now
|
|
13
13
|
* run several communities, so the single-community member pages (`membership`,
|
|
14
14
|
* `members/*`, `community/*`, `chat`) were removed with no redirects, because
|
|
15
15
|
* nobody used them yet and there is no one community they could redirect to.
|
|
16
16
|
* Every member page now lives under `communities/$slug`.
|
|
17
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.
|
|
21
|
+
*
|
|
18
22
|
* <Link {...internalLink("store/$slug", { slug: product.slug })}>Buy</Link>
|
|
19
23
|
* navigate(internalLink("checkout"))
|
|
20
24
|
* const href = internalPath("events/$slug", { slug });
|
|
@@ -29,6 +33,27 @@ export const INTERNAL_ROUTE = "/i/$" as const;
|
|
|
29
33
|
/** Every hosted page, by its path pattern. Generated from the route table. */
|
|
30
34
|
export const INTERNAL_PAGES = [
|
|
31
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",
|
|
32
57
|
"blog",
|
|
33
58
|
"blog-category/$slug",
|
|
34
59
|
"blog/$slug",
|
|
@@ -45,15 +70,20 @@ export const INTERNAL_PAGES = [
|
|
|
45
70
|
"communities/$slug/home",
|
|
46
71
|
"communities/$slug/home/calendar",
|
|
47
72
|
"communities/$slug/home/chat",
|
|
73
|
+
"communities/$slug/home/classroom",
|
|
48
74
|
"communities/$slug/home/collections",
|
|
49
75
|
"communities/$slug/home/collections/$collectionId",
|
|
50
76
|
"communities/$slug/home/leaderboard",
|
|
77
|
+
"communities/$slug/home/members",
|
|
78
|
+
"communities/$slug/home/moderation",
|
|
51
79
|
"communities/$slug/home/notifications",
|
|
52
80
|
"communities/$slug/home/posts",
|
|
53
81
|
"communities/$slug/home/profile/$accountId",
|
|
54
82
|
"communities/$slug/home/spaces",
|
|
55
83
|
"communities/$slug/home/spaces/$spaceId",
|
|
56
84
|
"communities/$slug/home/spaces/posts/$postId",
|
|
85
|
+
"communities/$slug/home/wiki",
|
|
86
|
+
"communities/$slug/home/wiki/$page",
|
|
57
87
|
"confirm-subscription",
|
|
58
88
|
"course-access/$accessId",
|
|
59
89
|
"courses",
|
|
@@ -62,6 +92,7 @@ export const INTERNAL_PAGES = [
|
|
|
62
92
|
"courses/claim",
|
|
63
93
|
"documents/$token",
|
|
64
94
|
"donation",
|
|
95
|
+
"downloads/$token",
|
|
65
96
|
"events",
|
|
66
97
|
"events/$slug",
|
|
67
98
|
"events/$slug/finalise",
|
|
@@ -91,11 +122,6 @@ export const INTERNAL_PAGES = [
|
|
|
91
122
|
"store/$slug",
|
|
92
123
|
"tickets/claim",
|
|
93
124
|
"video-calls/$callId",
|
|
94
|
-
"work",
|
|
95
|
-
"work/accept-invite",
|
|
96
|
-
"work/projects/$projectId",
|
|
97
|
-
"work/projects/$projectId/tasks/$taskId",
|
|
98
|
-
"work/time-report/$token",
|
|
99
125
|
] as const;
|
|
100
126
|
|
|
101
127
|
export type InternalPage = (typeof INTERNAL_PAGES)[number];
|
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
|
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
3
|
+
import { render, act, cleanup } from "@testing-library/react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The page view's Meta pair: the profile pixel's PageView and its server copy
|
|
7
|
+
* (POST /public/websites/meta-event) carry the id the first-party page view
|
|
8
|
+
* already has, so Meta keeps one. Consent loads AFTER mount, which used to
|
|
9
|
+
* mean the first page never sent a Meta PageView at all.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const tracked: { eventType: string; data: Record<string, unknown> }[] = [];
|
|
13
|
+
let seq = 0;
|
|
14
|
+
|
|
15
|
+
vi.mock("../../../data/queries/useAnalytics", () => ({
|
|
16
|
+
newEventId: () => `evt-${++seq}`,
|
|
17
|
+
useTrackEvent: () => ({
|
|
18
|
+
track: (eventType: string, data: Record<string, unknown>) => tracked.push({ eventType, data }),
|
|
19
|
+
trackBeacon: () => {},
|
|
20
|
+
apiUrl: "https://api.test",
|
|
21
|
+
subdomain: "artist",
|
|
22
|
+
}),
|
|
23
|
+
}));
|
|
24
|
+
vi.mock("../../../data/queries/useWebsite", () => ({
|
|
25
|
+
useSiteConfig: () => ({ data: { metaPixelId: "999", currency: "USD" }, isLoading: false }),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
import { ForgeAnalytics } from "../ForgeAnalytics";
|
|
29
|
+
import { resetMetaPolicy } from "../../../utils/metaEvents";
|
|
30
|
+
import { saveCookieConsent, GRANT_ALL, DENY_ALL } from "../../../utils/cookieConsent";
|
|
31
|
+
import { suppressProfilePixel, unsuppressProfilePixel } from "../../../utils/analyticsBus";
|
|
32
|
+
|
|
33
|
+
let fetchSpy: ReturnType<typeof vi.fn>;
|
|
34
|
+
let fbq: ReturnType<typeof vi.fn>;
|
|
35
|
+
|
|
36
|
+
const metaPosts = () =>
|
|
37
|
+
fetchSpy.mock.calls
|
|
38
|
+
.filter((c) => String(c[0]).endsWith("/public/websites/meta-event"))
|
|
39
|
+
.map((c) => JSON.parse((c[1] as RequestInit).body as string));
|
|
40
|
+
|
|
41
|
+
const flush = () => act(async () => {});
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
tracked.length = 0;
|
|
45
|
+
seq = 0;
|
|
46
|
+
resetMetaPolicy();
|
|
47
|
+
window.localStorage.clear();
|
|
48
|
+
fetchSpy = vi.fn().mockResolvedValue({ ok: true });
|
|
49
|
+
vi.stubGlobal("fetch", fetchSpy);
|
|
50
|
+
fbq = vi.fn();
|
|
51
|
+
(window as unknown as { fbq: unknown }).fbq = fbq;
|
|
52
|
+
window.history.replaceState({}, "", "/first");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
cleanup();
|
|
57
|
+
vi.unstubAllGlobals();
|
|
58
|
+
resetMetaPolicy();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("<ForgeAnalytics /> Meta PageView", () => {
|
|
62
|
+
it("sends the pixel PageView and its server copy with the page view's id, once consent loads", async () => {
|
|
63
|
+
saveCookieConsent(GRANT_ALL);
|
|
64
|
+
render(<ForgeAnalytics trackClicks={false} firstPartyMode="cookieless" />);
|
|
65
|
+
await flush();
|
|
66
|
+
|
|
67
|
+
const pageView = tracked.find((t) => t.eventType === "page_view");
|
|
68
|
+
expect(pageView?.data.eventId).toBe("evt-1");
|
|
69
|
+
expect(metaPosts()).toEqual([
|
|
70
|
+
{
|
|
71
|
+
subdomain: "artist",
|
|
72
|
+
pixel: "profile",
|
|
73
|
+
eventName: "PageView",
|
|
74
|
+
eventId: "evt-1",
|
|
75
|
+
eventSourceUrl: "http://localhost:3000/first",
|
|
76
|
+
metaSignals: { pageUrl: "http://localhost:3000/first", marketingConsent: true },
|
|
77
|
+
},
|
|
78
|
+
]);
|
|
79
|
+
expect(fbq).toHaveBeenCalledWith("trackSingle", "999", "PageView", {}, { eventID: "evt-1" });
|
|
80
|
+
// One first-party page view, not a second one when consent arrived.
|
|
81
|
+
expect(tracked.filter((t) => t.eventType === "page_view")).toHaveLength(1);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("sends a fresh pair on an SPA navigation", async () => {
|
|
85
|
+
saveCookieConsent(GRANT_ALL);
|
|
86
|
+
render(<ForgeAnalytics trackClicks={false} firstPartyMode="cookieless" />);
|
|
87
|
+
await flush();
|
|
88
|
+
act(() => window.history.pushState({}, "", "/second"));
|
|
89
|
+
await flush();
|
|
90
|
+
|
|
91
|
+
expect(metaPosts().map((b) => [b.eventId, b.eventSourceUrl])).toEqual([
|
|
92
|
+
["evt-1", "http://localhost:3000/first"],
|
|
93
|
+
["evt-2", "http://localhost:3000/second"],
|
|
94
|
+
]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("sends no server copy where a campaign pixel took the page over", async () => {
|
|
98
|
+
saveCookieConsent(GRANT_ALL);
|
|
99
|
+
suppressProfilePixel("/first");
|
|
100
|
+
render(<ForgeAnalytics trackClicks={false} firstPartyMode="cookieless" />);
|
|
101
|
+
await flush();
|
|
102
|
+
unsuppressProfilePixel("/first");
|
|
103
|
+
expect(metaPosts()).toEqual([]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("sends nothing to Meta when marketing was declined", async () => {
|
|
107
|
+
saveCookieConsent(DENY_ALL);
|
|
108
|
+
render(<ForgeAnalytics trackClicks={false} firstPartyMode="cookieless" />);
|
|
109
|
+
await flush();
|
|
110
|
+
expect(metaPosts()).toEqual([]);
|
|
111
|
+
expect(tracked.filter((t) => t.eventType === "page_view")).toHaveLength(1);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("sends a plain click first-party only", async () => {
|
|
115
|
+
saveCookieConsent(GRANT_ALL);
|
|
116
|
+
const { container } = render(
|
|
117
|
+
<>
|
|
118
|
+
<ForgeAnalytics firstPartyMode="cookieless" />
|
|
119
|
+
<button type="button">Buy</button>
|
|
120
|
+
</>,
|
|
121
|
+
);
|
|
122
|
+
await flush();
|
|
123
|
+
const before = metaPosts().length;
|
|
124
|
+
act(() => container.querySelector("button")!.click());
|
|
125
|
+
await flush();
|
|
126
|
+
expect(tracked.some((t) => t.eventType === "click")).toBe(true);
|
|
127
|
+
expect(metaPosts()).toHaveLength(before);
|
|
128
|
+
});
|
|
129
|
+
});
|