@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tribe-nest/forge",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.61.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"@tribe-nest/media-protocol": "^1.0.0",
|
|
36
36
|
"@tribe-nest/seating-core": "^1.0.0",
|
|
37
37
|
"axios": "^1.7.9",
|
|
38
|
+
"cobe": "2.0.1",
|
|
38
39
|
"libphonenumber-js": "^1.13.13",
|
|
39
40
|
"lucide-react": "^0.510.0",
|
|
40
41
|
"react-hook-form": "^7.60.0",
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { CommunityProvider } from "../contexts/CommunityContext";
|
|
3
|
+
import type { PublicCommunity } from "../data/queries/useCommunities";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A community for specs. Member-content hooks throw outside a
|
|
7
|
+
* `<CommunityProvider>`, so a spec that renders one wraps it in `InCommunity`.
|
|
8
|
+
* `initialData` makes the id known on the first render, with no request.
|
|
9
|
+
*/
|
|
10
|
+
export const TEST_COMMUNITY: PublicCommunity = {
|
|
11
|
+
id: "11111111-1111-4111-8111-111111111111",
|
|
12
|
+
name: "Fan Club",
|
|
13
|
+
slug: "fan-club",
|
|
14
|
+
description: "Where the fans are.",
|
|
15
|
+
coverUrl: null,
|
|
16
|
+
logoUrl: null,
|
|
17
|
+
category: "Music",
|
|
18
|
+
tags: [],
|
|
19
|
+
memberCount: 42,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function InCommunity({ children, community = TEST_COMMUNITY }: { children: ReactNode; community?: PublicCommunity }) {
|
|
23
|
+
return (
|
|
24
|
+
<CommunityProvider community={community.slug} initialData={community}>
|
|
25
|
+
{children}
|
|
26
|
+
</CommunityProvider>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import type { IBookingFee, ProductDeliveryType } from "../types/models";
|
|
3
|
-
import { useCallback, useContext, useMemo, useState } from "react";
|
|
3
|
+
import { useCallback, useContext, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { trackMeta } from "../utils/metaEvents";
|
|
4
5
|
import { createSharedContext } from "../runtime/registry";
|
|
5
6
|
import type { ReactNode } from "react";
|
|
6
7
|
import { useEffect } from "react";
|
|
@@ -333,6 +334,15 @@ export function CartProvider({ children }: { children: ReactNode }) {
|
|
|
333
334
|
return false;
|
|
334
335
|
}
|
|
335
336
|
setCartItems((prev) => [...prev, next]);
|
|
337
|
+
// Meta AddToCart, only for a line that is new: a duplicate updates the
|
|
338
|
+
// line and adds nothing.
|
|
339
|
+
trackMeta("AddToCart", {
|
|
340
|
+
content_ids: [item.productId],
|
|
341
|
+
content_name: item.title,
|
|
342
|
+
content_type: "product",
|
|
343
|
+
value: Number(item.price) * (item.quantity || 1),
|
|
344
|
+
num_items: item.quantity || 1,
|
|
345
|
+
});
|
|
336
346
|
return true;
|
|
337
347
|
},
|
|
338
348
|
[cartItems],
|
|
@@ -392,7 +402,30 @@ export function CartProvider({ children }: { children: ReactNode }) {
|
|
|
392
402
|
}
|
|
393
403
|
}, []);
|
|
394
404
|
|
|
405
|
+
// The latest selection, read by `setTickets` to tell an increase (an add,
|
|
406
|
+
// for Meta) from a decrease, outside the state updater (which must stay pure).
|
|
407
|
+
const ticketItemsRef = useRef(ticketItems);
|
|
408
|
+
ticketItemsRef.current = ticketItems;
|
|
409
|
+
|
|
395
410
|
const setTickets = useCallback((item: TicketCartItem) => {
|
|
411
|
+
const before = ticketItemsRef.current.find((t) => t.eventId === item.eventId);
|
|
412
|
+
let added = 0;
|
|
413
|
+
let addedValue = 0;
|
|
414
|
+
for (const [ticketId, qty] of Object.entries(item.tickets)) {
|
|
415
|
+
const increase = qty - (before?.tickets[ticketId] ?? 0);
|
|
416
|
+
if (increase <= 0) continue;
|
|
417
|
+
added += increase;
|
|
418
|
+
addedValue += increase * Number(item.ticketMeta[ticketId]?.price ?? 0);
|
|
419
|
+
}
|
|
420
|
+
if (added > 0) {
|
|
421
|
+
trackMeta("AddToCart", {
|
|
422
|
+
content_ids: [item.eventId],
|
|
423
|
+
content_name: item.eventTitle,
|
|
424
|
+
content_type: "product",
|
|
425
|
+
value: Number.isFinite(addedValue) ? addedValue : undefined,
|
|
426
|
+
num_items: added,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
396
429
|
setTicketItems((prev) => [...prev.filter((t) => t.eventId !== item.eventId), item]);
|
|
397
430
|
setCartOpen(true);
|
|
398
431
|
}, []);
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useContext, useEffect, useMemo, type ReactNode } from "react";
|
|
3
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
4
|
+
import { createSharedContext } from "../runtime/registry";
|
|
5
|
+
import { useForge } from "../provider/ForgeProvider";
|
|
6
|
+
import {
|
|
7
|
+
communitiesKeys,
|
|
8
|
+
usePublicCommunity,
|
|
9
|
+
useViewerAccountId,
|
|
10
|
+
type CommunityDetail,
|
|
11
|
+
type PublicCommunity,
|
|
12
|
+
} from "../data/queries/useCommunities";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Which community the surrounding UI is about.
|
|
16
|
+
*
|
|
17
|
+
* A profile can run several communities, each fully isolated (members, tiers,
|
|
18
|
+
* feed, spaces, collections, chat, calls, identity). So every member-content
|
|
19
|
+
* hook has to be told WHICH one, and it is told by this provider rather than by
|
|
20
|
+
* an argument threaded through every component: the hosted pages fill it from
|
|
21
|
+
* the `$slug` in the URL, and a creator's own page wraps a section in it.
|
|
22
|
+
*
|
|
23
|
+
* <CommunityProvider community="fan-club">
|
|
24
|
+
* <CommunityHome />
|
|
25
|
+
* </CommunityProvider>
|
|
26
|
+
*
|
|
27
|
+
* A member-content hook used outside it THROWS. That is deliberate: guessing
|
|
28
|
+
* ("the first community", "any membership on the profile") is exactly the
|
|
29
|
+
* cross-community leak this model exists to prevent, and a developer error at
|
|
30
|
+
* build time is far cheaper than a fan reading the wrong community's feed.
|
|
31
|
+
*
|
|
32
|
+
* The context is shared across bundles (`createSharedContext`), so a provider a
|
|
33
|
+
* site mounts is the one the hosted pages bundle reads, and vice versa.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
export interface CommunityContextValue {
|
|
37
|
+
/** The community, once loaded. */
|
|
38
|
+
community: PublicCommunity | undefined;
|
|
39
|
+
/** Its id. Known before the community loads when the provider was given an id. */
|
|
40
|
+
communityId: string | undefined;
|
|
41
|
+
/** Its slug. Known before the community loads when the provider was given a slug. */
|
|
42
|
+
slug: string | undefined;
|
|
43
|
+
isLoading: boolean;
|
|
44
|
+
/** True when the server answered that no such live community exists. */
|
|
45
|
+
notFound: boolean;
|
|
46
|
+
/** What the provider was given: a slug or an id. */
|
|
47
|
+
ref: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const CommunityContext = createSharedContext<CommunityContextValue | null>("community", null);
|
|
51
|
+
|
|
52
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
53
|
+
|
|
54
|
+
/** True when a community reference is an id rather than a slug. */
|
|
55
|
+
export function isCommunityId(ref: string): boolean {
|
|
56
|
+
return UUID_RE.test(ref);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface CommunityProviderProps {
|
|
60
|
+
/** The community's slug (as in `/i/communities/<slug>`) or its id. */
|
|
61
|
+
community: string;
|
|
62
|
+
/**
|
|
63
|
+
* The community when the host already has it (SSR, a list it came from).
|
|
64
|
+
* Seeds the cache so nothing below waits on a request. Optional.
|
|
65
|
+
*/
|
|
66
|
+
initialData?: PublicCommunity;
|
|
67
|
+
children: ReactNode;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Supplies one community to every hook and component below it. See the file
|
|
72
|
+
* docblock for why a member-content hook refuses to run without one.
|
|
73
|
+
*/
|
|
74
|
+
export function CommunityProvider({ community: ref, initialData, children }: CommunityProviderProps) {
|
|
75
|
+
const { profileId } = useForge();
|
|
76
|
+
const queryClient = useQueryClient();
|
|
77
|
+
const accountId = useViewerAccountId();
|
|
78
|
+
|
|
79
|
+
// Seed the detail cache from `initialData` so `community` is there on the
|
|
80
|
+
// first render. The seed carries no tiers or viewer, so the real read still
|
|
81
|
+
// runs and replaces it.
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (!initialData || !profileId) return;
|
|
84
|
+
const key = communitiesKeys.detail(profileId, ref, {}, accountId);
|
|
85
|
+
if (!queryClient.getQueryData(key)) {
|
|
86
|
+
queryClient.setQueryData<CommunityDetail>(
|
|
87
|
+
key,
|
|
88
|
+
{ community: initialData, tiers: [], viewer: EMPTY_VIEWER, invite: null },
|
|
89
|
+
{ updatedAt: 0 },
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}, [initialData, profileId, ref, accountId, queryClient]);
|
|
93
|
+
|
|
94
|
+
const detail = usePublicCommunity(ref);
|
|
95
|
+
const community = detail.data?.community ?? initialData;
|
|
96
|
+
const byId = isCommunityId(ref);
|
|
97
|
+
|
|
98
|
+
const value = useMemo<CommunityContextValue>(
|
|
99
|
+
() => ({
|
|
100
|
+
community,
|
|
101
|
+
communityId: community?.id ?? (byId ? ref : undefined),
|
|
102
|
+
slug: community?.slug ?? (byId ? undefined : ref),
|
|
103
|
+
isLoading: !community && detail.isLoading,
|
|
104
|
+
notFound: !community && (detail.error as { response?: { status?: number } } | null)?.response?.status === 404,
|
|
105
|
+
ref,
|
|
106
|
+
}),
|
|
107
|
+
[community, byId, ref, detail.isLoading, detail.error],
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
return <CommunityContext.Provider value={value}>{children}</CommunityContext.Provider>;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const EMPTY_VIEWER = {
|
|
114
|
+
state: "none" as const,
|
|
115
|
+
membership: null,
|
|
116
|
+
application: null,
|
|
117
|
+
upgradeTiers: [],
|
|
118
|
+
canInteract: false,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/** Thrown by any member-content hook used outside `<CommunityProvider>`. */
|
|
122
|
+
export class MissingCommunityProviderError extends Error {
|
|
123
|
+
constructor(hookName: string) {
|
|
124
|
+
super(
|
|
125
|
+
`${hookName} must be used inside <CommunityProvider community="<slug or id>">. ` +
|
|
126
|
+
"A creator can run several communities, so Forge will not guess which one this is. " +
|
|
127
|
+
"Wrap the page (or section) in <CommunityProvider>, or pass `community` to the component that accepts it.",
|
|
128
|
+
);
|
|
129
|
+
this.name = "MissingCommunityProviderError";
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The surrounding community. Throws outside `<CommunityProvider>`.
|
|
135
|
+
*
|
|
136
|
+
* `communityId` can be undefined for a moment while a slug resolves, so data
|
|
137
|
+
* hooks gate on it rather than on the provider alone.
|
|
138
|
+
*/
|
|
139
|
+
export function useCommunityContext(): CommunityContextValue {
|
|
140
|
+
const ctx = useContext(CommunityContext);
|
|
141
|
+
if (!ctx) throw new MissingCommunityProviderError("useCommunityContext()");
|
|
142
|
+
return ctx;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** The surrounding community, or null outside a provider. For components that work either way. */
|
|
146
|
+
export function useOptionalCommunityContext(): CommunityContextValue | null {
|
|
147
|
+
return useContext(CommunityContext);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* What a member-content data hook needs: the API client, the profile and the
|
|
152
|
+
* community. Throws `MissingCommunityProviderError` naming the calling hook.
|
|
153
|
+
*/
|
|
154
|
+
export function useCommunityScope(hookName: string) {
|
|
155
|
+
const ctx = useContext(CommunityContext);
|
|
156
|
+
if (!ctx) throw new MissingCommunityProviderError(hookName);
|
|
157
|
+
const { client, profileId } = useForge();
|
|
158
|
+
return { client, profileId, communityId: ctx.communityId, slug: ctx.slug };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---- The upgrade sheet opener ----------------------------------------------------
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Lets any locked item inside a community ("Unlock with {tier}") open the
|
|
165
|
+
* community's ONE upgrade sheet, wherever it is drawn. `CommunityHome` provides
|
|
166
|
+
* it; a custom member area can provide its own. Absent, locked items fall back
|
|
167
|
+
* to a link to the community's About page with the tier preselected.
|
|
168
|
+
*/
|
|
169
|
+
export interface CommunityUpgradeOpener {
|
|
170
|
+
/** Open the upgrade sheet, optionally preselecting the tier that unlocks the item. */
|
|
171
|
+
openUpgrade: (tierId?: string | null) => void;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const CommunityUpgradeOpenerContext = createSharedContext<CommunityUpgradeOpener | null>(
|
|
175
|
+
"communityUpgradeOpener",
|
|
176
|
+
null,
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
export function CommunityUpgradeOpenerProvider({
|
|
180
|
+
value,
|
|
181
|
+
children,
|
|
182
|
+
}: {
|
|
183
|
+
value: CommunityUpgradeOpener;
|
|
184
|
+
children: ReactNode;
|
|
185
|
+
}) {
|
|
186
|
+
return <CommunityUpgradeOpenerContext.Provider value={value}>{children}</CommunityUpgradeOpenerContext.Provider>;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** The surrounding upgrade opener, or null when no community home provides one. */
|
|
190
|
+
export function useCommunityUpgradeOpener(): CommunityUpgradeOpener | null {
|
|
191
|
+
return useContext(CommunityUpgradeOpenerContext);
|
|
192
|
+
}
|
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
storeTokenPair,
|
|
32
32
|
} from "../client/tokenStorage";
|
|
33
33
|
|
|
34
|
-
// Kept for compat
|
|
34
|
+
// Kept for compat. The canonical constants live in client/tokenStorage.ts.
|
|
35
35
|
export const ACCESS_TOKEN_KEY = PUBLIC_ACCESS_TOKEN_KEY;
|
|
36
36
|
export { PUBLIC_ACCESS_TOKEN_KEY };
|
|
37
37
|
|
|
@@ -129,7 +129,7 @@ function PublicAuthProvider({ children }: AuthProviderProps) {
|
|
|
129
129
|
}, []);
|
|
130
130
|
|
|
131
131
|
const { data: currencies } = useQuery({
|
|
132
|
-
// profileId scopes the cache
|
|
132
|
+
// profileId scopes the cache: supportedCurrencies is narrowed to the tenant's
|
|
133
133
|
// connected account (must match useCurrencies' key so they share one fetch).
|
|
134
134
|
queryKey: ["public-currencies", profileId],
|
|
135
135
|
queryFn: async () => {
|
|
@@ -241,6 +241,17 @@ function PublicAuthProvider({ children }: AuthProviderProps) {
|
|
|
241
241
|
}
|
|
242
242
|
};
|
|
243
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Sign in with a session another call already minted (the email-first flow's
|
|
246
|
+
* `signed_in` answers). The same three steps every sign-in here ends with:
|
|
247
|
+
* store the token pair, hand the access token to the client, record the user.
|
|
248
|
+
*/
|
|
249
|
+
const adoptSession = (session: { token: string; refreshToken?: string; account: PublicAuthUser }) => {
|
|
250
|
+
storeTokenPair(PUBLIC_ACCESS_TOKEN_KEY, PUBLIC_REFRESH_TOKEN_KEY, session.token, session.refreshToken);
|
|
251
|
+
setToken(session.token);
|
|
252
|
+
dispatch({ type: Types.Login, payload: { user: session.account } });
|
|
253
|
+
};
|
|
254
|
+
|
|
244
255
|
const resendLoginCode = async (email: string) => {
|
|
245
256
|
// /public/sessions requires the profile context.
|
|
246
257
|
await client.post("/public/sessions/resend-code", { email, profileId });
|
|
@@ -303,7 +314,7 @@ function PublicAuthProvider({ children }: AuthProviderProps) {
|
|
|
303
314
|
...data,
|
|
304
315
|
});
|
|
305
316
|
// No token: signup issues no session until the emailed code is redeemed
|
|
306
|
-
// (see PublicAuthChallenge). Keep the caller's data
|
|
317
|
+
// (see PublicAuthChallenge). Keep the caller's data: `verifyRegistration`
|
|
307
318
|
// needs the coupon/consent flags again to finish the job.
|
|
308
319
|
const { email } = response.data as { status: "verify_email"; email: string };
|
|
309
320
|
dispatch({ type: Types.Loading, payload: { isLoading: false } });
|
|
@@ -372,6 +383,7 @@ function PublicAuthProvider({ children }: AuthProviderProps) {
|
|
|
372
383
|
validateLoginCredentials,
|
|
373
384
|
verifyLoginCode,
|
|
374
385
|
resendLoginCode,
|
|
386
|
+
adoptSession,
|
|
375
387
|
logout,
|
|
376
388
|
register,
|
|
377
389
|
verifyRegistration,
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
3
|
+
import { act, renderHook } from "@testing-library/react";
|
|
4
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
5
|
+
import type { ReactNode } from "react";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Every request that creates a visitor record carries `metaSignals`, which is
|
|
9
|
+
* what lets a server event sent later (from a worker, with no request) still
|
|
10
|
+
* match the visitor. A create response that carries `metaEvent` means the
|
|
11
|
+
* server queued that event: the browser fires only its pixel twin.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const post = vi.fn();
|
|
15
|
+
|
|
16
|
+
vi.mock("../../../provider/ForgeProvider", () => ({
|
|
17
|
+
useForge: () => ({ client: { post }, profileId: "profile-1" }),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
import { useSubmitPublicForm } from "../useForms";
|
|
21
|
+
import { useJoinEmailList } from "../useLeadMagnet";
|
|
22
|
+
import { useCreateOrder } from "../useOrders";
|
|
23
|
+
import { useUpdateCoachingBooking } from "../useCoachingAvailability";
|
|
24
|
+
import { resetMetaPolicy, setMetaPolicy } from "../../../utils/metaEvents";
|
|
25
|
+
import { saveCookieConsent, GRANT_ALL, DENY_ALL } from "../../../utils/cookieConsent";
|
|
26
|
+
|
|
27
|
+
const wrapper = ({ children }: { children: ReactNode }) => {
|
|
28
|
+
const queryClient = new QueryClient({ defaultOptions: { mutations: { retry: false } } });
|
|
29
|
+
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
let fbq: ReturnType<typeof vi.fn>;
|
|
33
|
+
let fetchSpy: ReturnType<typeof vi.fn>;
|
|
34
|
+
const pixel = () => fbq.mock.calls.filter((c) => c[0] === "trackSingle");
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
post.mockReset();
|
|
38
|
+
resetMetaPolicy();
|
|
39
|
+
window.localStorage.clear();
|
|
40
|
+
document.cookie = "_fbp=fb.1.1.2; path=/";
|
|
41
|
+
fbq = vi.fn();
|
|
42
|
+
(window as unknown as { fbq: unknown }).fbq = fbq;
|
|
43
|
+
fetchSpy = vi.fn().mockResolvedValue({ ok: true });
|
|
44
|
+
vi.stubGlobal("fetch", fetchSpy);
|
|
45
|
+
window.history.replaceState({}, "", "/contact");
|
|
46
|
+
setMetaPolicy({
|
|
47
|
+
pixelId: "555",
|
|
48
|
+
enabled: true,
|
|
49
|
+
requireConsent: true,
|
|
50
|
+
apiUrl: "https://api.test",
|
|
51
|
+
subdomain: "artist",
|
|
52
|
+
currency: "USD",
|
|
53
|
+
resolved: true,
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
vi.unstubAllGlobals();
|
|
59
|
+
resetMetaPolicy();
|
|
60
|
+
document.cookie = "_fbp=; path=/; max-age=0";
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("useSubmitPublicForm", () => {
|
|
64
|
+
it("attaches metaSignals and fires the pixel copy of the server's Lead", async () => {
|
|
65
|
+
saveCookieConsent(GRANT_ALL);
|
|
66
|
+
post.mockResolvedValue({ data: { id: "sub-1", metaEvent: { name: "Lead", eventId: "sub-1", customData: { content_name: "Contact" } } } });
|
|
67
|
+
const { result } = renderHook(() => useSubmitPublicForm("form-1"), { wrapper });
|
|
68
|
+
|
|
69
|
+
await act(async () => {
|
|
70
|
+
await result.current.mutateAsync({ answers: [], respondentEmail: "a@b.test" });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(post).toHaveBeenCalledWith("/public/forms/form-1/submit", {
|
|
74
|
+
answers: [],
|
|
75
|
+
respondentEmail: "a@b.test",
|
|
76
|
+
idempotencyKey: expect.any(String),
|
|
77
|
+
metaSignals: { pageUrl: "http://localhost:3000/contact", marketingConsent: true, fbp: "fb.1.1.2" },
|
|
78
|
+
});
|
|
79
|
+
expect(pixel()).toEqual([["trackSingle", "555", "Lead", { content_name: "Contact" }, { eventID: "sub-1" }]]);
|
|
80
|
+
// The server already queued its copy.
|
|
81
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("sends consent false and no Meta ids when marketing was declined, and fires no pixel", async () => {
|
|
85
|
+
saveCookieConsent(DENY_ALL);
|
|
86
|
+
post.mockResolvedValue({ data: { id: "sub-2", metaEvent: { name: "Lead", eventId: "sub-2" } } });
|
|
87
|
+
const { result } = renderHook(() => useSubmitPublicForm("form-1"), { wrapper });
|
|
88
|
+
|
|
89
|
+
await act(async () => {
|
|
90
|
+
await result.current.mutateAsync([]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(post.mock.calls[0][1].metaSignals).toEqual({ pageUrl: "http://localhost:3000/contact", marketingConsent: false });
|
|
94
|
+
expect(pixel()).toEqual([]);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("other create requests", () => {
|
|
99
|
+
beforeEach(() => saveCookieConsent(GRANT_ALL));
|
|
100
|
+
|
|
101
|
+
it("useJoinEmailList attaches metaSignals and fires CompleteRegistration from the response", async () => {
|
|
102
|
+
post.mockResolvedValue({ data: { metaEvent: { name: "CompleteRegistration", eventId: "subscriber-1" } } });
|
|
103
|
+
const { result } = renderHook(() => useJoinEmailList(), { wrapper });
|
|
104
|
+
await act(async () => {
|
|
105
|
+
await result.current.mutateAsync({ email: "a@b.test", emailListId: "list-1" });
|
|
106
|
+
});
|
|
107
|
+
expect(post.mock.calls[0][1]).toMatchObject({ email: "a@b.test", profileId: "profile-1", metaSignals: { marketingConsent: true } });
|
|
108
|
+
expect(pixel()).toEqual([["trackSingle", "555", "CompleteRegistration", {}, { eventID: "subscriber-1" }]]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("useUpdateCoachingBooking fires Schedule from the response", async () => {
|
|
112
|
+
post.mockResolvedValue({ data: { bookingId: "b-1", isConfirmed: true, metaEvent: { name: "Schedule", eventId: "b-1" } } });
|
|
113
|
+
const { result } = renderHook(() => useUpdateCoachingBooking("coach-1"), { wrapper });
|
|
114
|
+
await act(async () => {
|
|
115
|
+
await result.current.mutateAsync({ bookingId: "b-1", email: "a@b.test", firstName: "A", lastName: "B", confirmIfFree: true });
|
|
116
|
+
});
|
|
117
|
+
expect(post.mock.calls[0][1].metaSignals).toMatchObject({ marketingConsent: true, fbp: "fb.1.1.2" });
|
|
118
|
+
expect(pixel()).toEqual([["trackSingle", "555", "Schedule", {}, { eventID: "b-1" }]]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("useCreateOrder attaches metaSignals and fires nothing itself", async () => {
|
|
122
|
+
post.mockResolvedValue({ data: { orderId: "o-1", totalAmount: 10, subTotal: 10 } });
|
|
123
|
+
const { result } = renderHook(() => useCreateOrder(), { wrapper });
|
|
124
|
+
await act(async () => {
|
|
125
|
+
await result.current.mutateAsync({ amount: 10, email: "a@b.test", cartItems: [] });
|
|
126
|
+
});
|
|
127
|
+
expect(post.mock.calls[0][1]).toMatchObject({ amount: 10, profileId: "profile-1", metaSignals: { marketingConsent: true } });
|
|
128
|
+
expect(pixel()).toEqual([]);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { IPublicOrder, IPublicPost, PaginatedData } from "../../types/models";
|
|
2
2
|
import { useForge } from "../../provider/ForgeProvider";
|
|
3
|
-
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
3
|
+
import { useInfiniteQuery, useMutation, useQuery } from "@tanstack/react-query";
|
|
4
4
|
|
|
5
5
|
/** Orders placed by the signed-in member on this profile. */
|
|
6
6
|
export function useUserOrders(accountId?: string) {
|
|
@@ -40,6 +40,11 @@ export type NotificationPreferences = {
|
|
|
40
40
|
};
|
|
41
41
|
|
|
42
42
|
/** The member's notification-type preferences for this profile. */
|
|
43
|
+
/**
|
|
44
|
+
* @deprecated Reads `newPosts` / `chatMessages`, which the server never sends:
|
|
45
|
+
* the preferences endpoint speaks the `"<type>.<channel>"` matrix. Use
|
|
46
|
+
* `useNotificationMatrix` and `useUpdateNotificationMatrix`.
|
|
47
|
+
*/
|
|
43
48
|
export function useNotificationPreferences(enabled = true) {
|
|
44
49
|
const { client, profileId } = useForge();
|
|
45
50
|
|
|
@@ -55,7 +60,11 @@ export function useNotificationPreferences(enabled = true) {
|
|
|
55
60
|
});
|
|
56
61
|
}
|
|
57
62
|
|
|
58
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* @deprecated Sends `{ newPosts: false }`, which the server drops without an
|
|
65
|
+
* error (it accepts only `"<type>.<channel>"` keys), so nothing is ever saved.
|
|
66
|
+
* Use `useUpdateNotificationMatrix`.
|
|
67
|
+
*/
|
|
59
68
|
export function useUpdateNotificationPreference() {
|
|
60
69
|
const { client, profileId } = useForge();
|
|
61
70
|
|
|
@@ -69,3 +78,25 @@ export function useUpdateNotificationPreference() {
|
|
|
69
78
|
},
|
|
70
79
|
});
|
|
71
80
|
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Every order, a page at a time, for a list with a "Show more" button.
|
|
84
|
+
*
|
|
85
|
+
* `useUserOrders` sends no page, so the server's default of 10 applies and a
|
|
86
|
+
* buyer with more orders than that never saw the older ones. `/public/orders`
|
|
87
|
+
* returns a bare array with no total, so a FULL page is the only sign that
|
|
88
|
+
* another may follow; an exact multiple costs one extra, empty request.
|
|
89
|
+
*/
|
|
90
|
+
export function useUserOrdersPaged(accountId?: string, limit = 20) {
|
|
91
|
+
const { client, profileId } = useForge();
|
|
92
|
+
return useInfiniteQuery({
|
|
93
|
+
queryKey: ["user-orders", accountId, profileId, "paged", limit],
|
|
94
|
+
initialPageParam: 1,
|
|
95
|
+
queryFn: async ({ pageParam }) => {
|
|
96
|
+
const res = await client.get("/public/orders", { params: { profileId, page: pageParam, limit } });
|
|
97
|
+
return res.data as IPublicOrder[];
|
|
98
|
+
},
|
|
99
|
+
getNextPageParam: (last, pages) => (last.length === limit ? pages.length + 1 : undefined),
|
|
100
|
+
enabled: !!accountId && !!profileId && !!client,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
@@ -5,7 +5,7 @@ import { useForge } from "../../provider/ForgeProvider";
|
|
|
5
5
|
* server-side CAPI twin, and for correlating first-party events. */
|
|
6
6
|
export function newEventId(): string {
|
|
7
7
|
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
8
|
-
// Fallback for older runtimes
|
|
8
|
+
// Fallback for older runtimes: not cryptographically strong, only needs to be
|
|
9
9
|
// unique enough to pair a pixel + CAPI event.
|
|
10
10
|
return `evt_${Date.now().toString(36)}_${Math.abs(hashString(String(performance?.now?.() ?? Date.now()))).toString(36)}`;
|
|
11
11
|
}
|
|
@@ -33,7 +33,7 @@ export function getAnalyticsSessionId(subdomain: string): string | null {
|
|
|
33
33
|
}
|
|
34
34
|
return sessionId;
|
|
35
35
|
} catch {
|
|
36
|
-
// Private mode / storage disabled
|
|
36
|
+
// Private mode / storage disabled: fall back to an ephemeral per-load id.
|
|
37
37
|
return `${subdomain}_${Date.now()}`;
|
|
38
38
|
}
|
|
39
39
|
}
|
|
@@ -44,7 +44,7 @@ export interface TrackEventOptions {
|
|
|
44
44
|
/**
|
|
45
45
|
* Store a session id in `sessionStorage` and send it with events. Default
|
|
46
46
|
* `true`. Set `false` for cookieless, Plausible-style analytics: nothing is
|
|
47
|
-
* written to the device and no id is sent
|
|
47
|
+
* written to the device and no id is sent; the backend derives a rotating
|
|
48
48
|
* per-day visitor hash instead, so the "visits" metric still works without
|
|
49
49
|
* needing storage consent.
|
|
50
50
|
*/
|
|
@@ -53,7 +53,7 @@ export interface TrackEventOptions {
|
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
55
|
* The first-party website-analytics primitive. Returns a `track(eventType,
|
|
56
|
-
* eventData)` function that posts to `POST /public/websites/track-event
|
|
56
|
+
* eventData)` function that posts to `POST /public/websites/track-event`, the
|
|
57
57
|
* same feed the platform's admin analytics dashboard reads (visits, page views,
|
|
58
58
|
* clicks, geo). The backend enriches each event with geo-IP + user agent.
|
|
59
59
|
*
|
|
@@ -68,7 +68,7 @@ export function useTrackEvent(opts?: TrackEventOptions) {
|
|
|
68
68
|
|
|
69
69
|
const buildPayload = useCallback(
|
|
70
70
|
(eventData: Record<string, unknown>) => {
|
|
71
|
-
// Cookieless mode omits the sessionId entirely
|
|
71
|
+
// Cookieless mode omits the sessionId entirely; the backend derives a
|
|
72
72
|
// rotating per-day visitor hash so nothing touches the device.
|
|
73
73
|
const sessionId = storeSession && subdomain ? getAnalyticsSessionId(subdomain) : undefined;
|
|
74
74
|
const payload: Record<string, unknown> = { ...eventData };
|
|
@@ -133,5 +133,5 @@ export function useTrackEvent(opts?: TrackEventOptions) {
|
|
|
133
133
|
[apiUrl, subdomain, buildPayload],
|
|
134
134
|
);
|
|
135
135
|
|
|
136
|
-
return { track, trackBeacon };
|
|
136
|
+
return { track, trackBeacon, apiUrl, subdomain };
|
|
137
137
|
}
|
|
@@ -22,7 +22,7 @@ type ReactionRow = { emoji: string; count: number; reactedByMe: boolean };
|
|
|
22
22
|
type MessageWithReactions = ChatMessage & { reactions?: ReactionRow[] };
|
|
23
23
|
|
|
24
24
|
const senderName = (message: MessageWithReactions): string =>
|
|
25
|
-
[message.senderFirstName, message.senderLastName].filter(Boolean).join(" ").trim() || message.senderAccountId;
|
|
25
|
+
[message.senderFirstName, message.senderLastName].filter(Boolean).join(" ").trim() || (message.senderAccountId ?? "");
|
|
26
26
|
|
|
27
27
|
export function useCallChat(callId: string | undefined): CallStageChat | undefined {
|
|
28
28
|
const { client, profileId } = useForge();
|
|
@@ -145,7 +145,7 @@ export function useCallChat(callId: string | undefined): CallStageChat | undefin
|
|
|
145
145
|
id: message.id,
|
|
146
146
|
content: message.content,
|
|
147
147
|
senderName: senderName(message),
|
|
148
|
-
senderKey: message.senderAccountId,
|
|
148
|
+
senderKey: message.senderAccountId ?? message.senderAgentId ?? "",
|
|
149
149
|
at: message.createdAt,
|
|
150
150
|
reactions: message.reactions ?? [],
|
|
151
151
|
}));
|