@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
|
@@ -2,6 +2,7 @@ import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tansta
|
|
|
2
2
|
import { useEffect, useRef, useState } from "react";
|
|
3
3
|
import { io, type Socket } from "socket.io-client";
|
|
4
4
|
import { useForge } from "../../provider/ForgeProvider";
|
|
5
|
+
import { useCommunityScope } from "../../contexts/CommunityContext";
|
|
5
6
|
import { getForgeWebSocketUrl } from "../../client/createForgeClient";
|
|
6
7
|
import { PUBLIC_ACCESS_TOKEN_KEY } from "../../contexts/PublicAuthContext";
|
|
7
8
|
|
|
@@ -13,6 +14,11 @@ export interface ChatChannel {
|
|
|
13
14
|
description: string | null;
|
|
14
15
|
/** For DM channels, the counterpart participant (backend-enriched). */
|
|
15
16
|
otherParticipant?: ChatDmPeer | null;
|
|
17
|
+
/**
|
|
18
|
+
* A DM's pair key. Null for the member and creator DM; `agent:...` for the
|
|
19
|
+
* member's DM with the community AI (see `isCommunityAgentDm`).
|
|
20
|
+
*/
|
|
21
|
+
dmKey?: string | null;
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
export interface ChatAttachment {
|
|
@@ -26,7 +32,11 @@ export interface ChatMessage {
|
|
|
26
32
|
id: string;
|
|
27
33
|
channelId: string;
|
|
28
34
|
content: string | null;
|
|
29
|
-
|
|
35
|
+
/** Null when the community AI sent it (`senderAgentId` is then set). */
|
|
36
|
+
senderAccountId: string | null;
|
|
37
|
+
/** The community AI, when it sent this message. `senderFirstName` is then its name. */
|
|
38
|
+
senderAgentId?: string | null;
|
|
39
|
+
senderAgentAvatarUrl?: string | null;
|
|
30
40
|
senderFirstName: string;
|
|
31
41
|
senderLastName: string;
|
|
32
42
|
isPinned: boolean;
|
|
@@ -67,83 +77,85 @@ export interface ChatMessagesPage {
|
|
|
67
77
|
// Query keys are exported so the realtime layer (and creators) can patch the
|
|
68
78
|
// same caches the data hooks populate.
|
|
69
79
|
export const chatKeys = {
|
|
70
|
-
|
|
80
|
+
// Channel lists, unread counts, DMs and the directory are per COMMUNITY: a
|
|
81
|
+
// profile runs several, and each has its own channels and members.
|
|
82
|
+
channels: (communityId?: string) => ["public-chat-channels", communityId] as const,
|
|
71
83
|
messages: (channelId?: string) => ["public-chat-messages", channelId] as const,
|
|
72
84
|
pinned: (channelId?: string) => ["public-chat-pinned", channelId] as const,
|
|
73
|
-
unread: (
|
|
74
|
-
dm: (
|
|
85
|
+
unread: (communityId?: string) => ["public-chat-unread-counts", communityId] as const,
|
|
86
|
+
dm: (communityId?: string) => ["public-chat-dm", communityId] as const,
|
|
75
87
|
replies: (parentMessageId?: string) => ["public-chat-replies", parentMessageId] as const,
|
|
76
|
-
memberDirectory: (
|
|
88
|
+
memberDirectory: (communityId?: string) => ["public-chat-member-directory", communityId] as const,
|
|
77
89
|
};
|
|
78
90
|
|
|
79
91
|
// ── Data hooks ───────────────────────────────────────────────────────────────
|
|
80
92
|
|
|
81
93
|
/** Channels visible to the current member (GET /public/chat/channels). */
|
|
82
94
|
export function useChatChannels() {
|
|
83
|
-
const { client, profileId } =
|
|
95
|
+
const { client, profileId, communityId } = useCommunityScope("useChatChannels()");
|
|
84
96
|
return useQuery<ChatChannel[]>({
|
|
85
|
-
queryKey: chatKeys.channels(
|
|
97
|
+
queryKey: chatKeys.channels(communityId),
|
|
86
98
|
queryFn: async () => {
|
|
87
|
-
const res = await client.get("/public/chat/channels", { params: { profileId } });
|
|
99
|
+
const res = await client.get("/public/chat/channels", { params: { profileId, communityId } });
|
|
88
100
|
return res.data;
|
|
89
101
|
},
|
|
90
|
-
enabled: !!profileId && !!client,
|
|
102
|
+
enabled: !!profileId && !!communityId && !!client,
|
|
91
103
|
});
|
|
92
104
|
}
|
|
93
105
|
|
|
94
106
|
/** Paginated messages for a channel, newest first (GET /public/chat/channels/:id/messages). */
|
|
95
107
|
export function useChatMessages(channelId?: string, limit = 50) {
|
|
96
|
-
const { client, profileId } =
|
|
108
|
+
const { client, profileId, communityId } = useCommunityScope("useChatMessages()");
|
|
97
109
|
return useInfiniteQuery<ChatMessagesPage>({
|
|
98
110
|
queryKey: chatKeys.messages(channelId),
|
|
99
111
|
queryFn: async ({ pageParam = 1 }) => {
|
|
100
112
|
const res = await client.get(`/public/chat/channels/${channelId}/messages`, {
|
|
101
|
-
params: { profileId, page: pageParam, limit },
|
|
113
|
+
params: { profileId, communityId, page: pageParam, limit },
|
|
102
114
|
});
|
|
103
115
|
return res.data;
|
|
104
116
|
},
|
|
105
117
|
initialPageParam: 1,
|
|
106
118
|
getNextPageParam: (lastPage) =>
|
|
107
119
|
lastPage.page * lastPage.limit < lastPage.total ? lastPage.page + 1 : undefined,
|
|
108
|
-
enabled: !!channelId && !!client && !!profileId,
|
|
120
|
+
enabled: !!channelId && !!client && !!profileId && !!communityId,
|
|
109
121
|
});
|
|
110
122
|
}
|
|
111
123
|
|
|
112
124
|
/** Pinned messages for a channel (GET /public/chat/channels/:id/pinned). */
|
|
113
125
|
export function useChatPinnedMessages(channelId?: string) {
|
|
114
|
-
const { client, profileId } =
|
|
126
|
+
const { client, profileId, communityId } = useCommunityScope("useChatPinnedMessages()");
|
|
115
127
|
return useQuery<ChatMessage[]>({
|
|
116
128
|
queryKey: chatKeys.pinned(channelId),
|
|
117
129
|
queryFn: async () => {
|
|
118
|
-
const res = await client.get(`/public/chat/channels/${channelId}/pinned`, { params: { profileId } });
|
|
130
|
+
const res = await client.get(`/public/chat/channels/${channelId}/pinned`, { params: { profileId, communityId } });
|
|
119
131
|
return res.data;
|
|
120
132
|
},
|
|
121
|
-
enabled: !!channelId && !!client && !!profileId,
|
|
133
|
+
enabled: !!channelId && !!client && !!profileId && !!communityId,
|
|
122
134
|
});
|
|
123
135
|
}
|
|
124
136
|
|
|
125
137
|
/** Per-channel unread counts, polled every 30s (GET /public/chat/unread-counts). */
|
|
126
138
|
export function useChatUnreadCounts() {
|
|
127
|
-
const { client, profileId } =
|
|
139
|
+
const { client, profileId, communityId } = useCommunityScope("useChatUnreadCounts()");
|
|
128
140
|
return useQuery<ChatUnreadCount[]>({
|
|
129
|
-
queryKey: chatKeys.unread(
|
|
141
|
+
queryKey: chatKeys.unread(communityId),
|
|
130
142
|
queryFn: async () => {
|
|
131
|
-
const res = await client.get("/public/chat/unread-counts", { params: { profileId } });
|
|
143
|
+
const res = await client.get("/public/chat/unread-counts", { params: { profileId, communityId } });
|
|
132
144
|
return res.data;
|
|
133
145
|
},
|
|
134
|
-
enabled: !!profileId && !!client,
|
|
146
|
+
enabled: !!profileId && !!communityId && !!client,
|
|
135
147
|
refetchInterval: 30000,
|
|
136
148
|
});
|
|
137
149
|
}
|
|
138
150
|
|
|
139
151
|
/** Mark messages as read (POST /public/chat/read-receipts); invalidates unread counts. */
|
|
140
152
|
export function useMarkChatRead() {
|
|
141
|
-
const { client, profileId } =
|
|
153
|
+
const { client, profileId, communityId } = useCommunityScope("useMarkChatRead()");
|
|
142
154
|
const queryClient = useQueryClient();
|
|
143
155
|
return useMutation({
|
|
144
156
|
mutationFn: async (messageIds: string[]) => {
|
|
145
157
|
if (messageIds.length === 0) return;
|
|
146
|
-
await client.post("/public/chat/read-receipts", { profileId, messageIds });
|
|
158
|
+
await client.post("/public/chat/read-receipts", { profileId, communityId, messageIds });
|
|
147
159
|
},
|
|
148
160
|
onSuccess: () => {
|
|
149
161
|
queryClient.invalidateQueries({ queryKey: ["public-chat-unread-counts"] });
|
|
@@ -153,7 +165,7 @@ export function useMarkChatRead() {
|
|
|
153
165
|
|
|
154
166
|
/** Send a message over REST (POST /public/chat/messages). Realtime path uses the socket. */
|
|
155
167
|
export function useSendChatMessage() {
|
|
156
|
-
const { client, profileId } =
|
|
168
|
+
const { client, profileId, communityId } = useCommunityScope("useSendChatMessage()");
|
|
157
169
|
return useMutation({
|
|
158
170
|
mutationFn: async (input: {
|
|
159
171
|
channelId: string;
|
|
@@ -161,7 +173,7 @@ export function useSendChatMessage() {
|
|
|
161
173
|
attachments?: ChatAttachment[];
|
|
162
174
|
parentMessageId?: string;
|
|
163
175
|
}) => {
|
|
164
|
-
const res = await client.post("/public/chat/messages", { profileId, ...input });
|
|
176
|
+
const res = await client.post("/public/chat/messages", { profileId, communityId, ...input });
|
|
165
177
|
return res.data as ChatMessage;
|
|
166
178
|
},
|
|
167
179
|
});
|
|
@@ -169,69 +181,69 @@ export function useSendChatMessage() {
|
|
|
169
181
|
|
|
170
182
|
/** Paginated replies for a thread parent (GET /public/chat/messages/:id/replies), oldest first. */
|
|
171
183
|
export function useChatMessageReplies(parentMessageId?: string, limit = 50) {
|
|
172
|
-
const { client, profileId } =
|
|
184
|
+
const { client, profileId, communityId } = useCommunityScope("useChatMessageReplies()");
|
|
173
185
|
return useInfiniteQuery<ChatMessagesPage>({
|
|
174
186
|
queryKey: chatKeys.replies(parentMessageId),
|
|
175
187
|
queryFn: async ({ pageParam = 1 }) => {
|
|
176
188
|
const res = await client.get(`/public/chat/messages/${parentMessageId}/replies`, {
|
|
177
|
-
params: { profileId, page: pageParam, limit },
|
|
189
|
+
params: { profileId, communityId, page: pageParam, limit },
|
|
178
190
|
});
|
|
179
191
|
return res.data;
|
|
180
192
|
},
|
|
181
193
|
initialPageParam: 1,
|
|
182
194
|
getNextPageParam: (lastPage) =>
|
|
183
195
|
lastPage.page * lastPage.limit < lastPage.total ? lastPage.page + 1 : undefined,
|
|
184
|
-
enabled: !!parentMessageId && !!client && !!profileId,
|
|
196
|
+
enabled: !!parentMessageId && !!client && !!profileId && !!communityId,
|
|
185
197
|
});
|
|
186
198
|
}
|
|
187
199
|
|
|
188
200
|
/** Same-tier members the current member may start a DM with (GET /public/chat/dm/directory). */
|
|
189
201
|
export function useChatMemberDirectory() {
|
|
190
|
-
const { client, profileId } =
|
|
202
|
+
const { client, profileId, communityId } = useCommunityScope("useChatMemberDirectory()");
|
|
191
203
|
return useQuery<ChatDirectoryMember[]>({
|
|
192
|
-
queryKey: chatKeys.memberDirectory(
|
|
204
|
+
queryKey: chatKeys.memberDirectory(communityId),
|
|
193
205
|
queryFn: async () => {
|
|
194
|
-
const res = await client.get("/public/chat/dm/directory", { params: { profileId } });
|
|
206
|
+
const res = await client.get("/public/chat/dm/directory", { params: { profileId, communityId } });
|
|
195
207
|
return res.data;
|
|
196
208
|
},
|
|
197
|
-
enabled: !!profileId && !!client,
|
|
209
|
+
enabled: !!profileId && !!communityId && !!client,
|
|
198
210
|
});
|
|
199
211
|
}
|
|
200
212
|
|
|
201
213
|
/** Open/create a DM with another member (POST /public/chat/dm/member). */
|
|
202
214
|
export function useCreateMemberDm() {
|
|
203
|
-
const { client, profileId } =
|
|
215
|
+
const { client, profileId, communityId } = useCommunityScope("useCreateMemberDm()");
|
|
204
216
|
const queryClient = useQueryClient();
|
|
205
217
|
return useMutation({
|
|
206
218
|
mutationFn: async (targetAccountId: string) => {
|
|
207
|
-
const res = await client.post("/public/chat/dm/member", { profileId, targetAccountId });
|
|
219
|
+
const res = await client.post("/public/chat/dm/member", { profileId, communityId, targetAccountId });
|
|
208
220
|
return res.data as ChatChannel;
|
|
209
221
|
},
|
|
210
222
|
onSuccess: () => {
|
|
211
|
-
queryClient.invalidateQueries({ queryKey: chatKeys.channels(
|
|
223
|
+
queryClient.invalidateQueries({ queryKey: chatKeys.channels(communityId) });
|
|
212
224
|
},
|
|
213
225
|
});
|
|
214
226
|
}
|
|
215
227
|
|
|
216
228
|
/** The current member's DM channel, if any (GET /public/chat/dm). */
|
|
217
229
|
export function useChatDm() {
|
|
218
|
-
const { client, profileId } =
|
|
230
|
+
const { client, profileId, communityId } = useCommunityScope("useChatDm()");
|
|
219
231
|
return useQuery<ChatChannel | null>({
|
|
220
|
-
queryKey: chatKeys.dm(
|
|
232
|
+
queryKey: chatKeys.dm(communityId),
|
|
221
233
|
queryFn: async () => {
|
|
222
|
-
const res = await client.get("/public/chat/dm", { params: { profileId } });
|
|
234
|
+
const res = await client.get("/public/chat/dm", { params: { profileId, communityId } });
|
|
223
235
|
return res.data;
|
|
224
236
|
},
|
|
225
|
-
enabled: !!profileId && !!client,
|
|
237
|
+
enabled: !!profileId && !!communityId && !!client,
|
|
226
238
|
});
|
|
227
239
|
}
|
|
228
240
|
|
|
229
241
|
/** Open/create the member's DM channel (POST /public/chat/dm). */
|
|
230
242
|
export function useCreateChatDm() {
|
|
231
|
-
const { client, profileId } =
|
|
243
|
+
const { client, profileId, communityId } = useCommunityScope("useCreateChatDm()");
|
|
232
244
|
return useMutation({
|
|
233
245
|
mutationFn: async () => {
|
|
234
|
-
const res = await client.post("/public/chat/dm", { profileId });
|
|
246
|
+
const res = await client.post("/public/chat/dm", { profileId, communityId });
|
|
235
247
|
return res.data as ChatChannel;
|
|
236
248
|
},
|
|
237
249
|
});
|
|
@@ -259,7 +271,7 @@ export function useChatSocket(enabled: boolean) {
|
|
|
259
271
|
const next = io(getForgeWebSocketUrl(apiUrl), { auth: { token }, transports: ["websocket"] });
|
|
260
272
|
socketRef.current = next;
|
|
261
273
|
setSocket(next);
|
|
262
|
-
// Intentionally not disconnecting on unmount
|
|
274
|
+
// Intentionally not disconnecting on unmount: mirrors the client app, where the
|
|
263
275
|
// chat socket persists across channel selection. Callers manage room join/leave.
|
|
264
276
|
}, [enabled, apiUrl]);
|
|
265
277
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useMutation } from "@tanstack/react-query";
|
|
2
|
+
import { readMetaSignals } from "../../utils/metaEvents";
|
|
2
3
|
import { useForge } from "../../provider/ForgeProvider";
|
|
3
4
|
import { bundleCouponRequestBody } from "../../ui/headless/checkout/bundleCoupon";
|
|
4
5
|
import type { CartItem, TicketCartItem } from "../../contexts/CartContext";
|
|
@@ -133,7 +134,7 @@ export function useCreateCheckout() {
|
|
|
133
134
|
|
|
134
135
|
return useMutation<CreateCheckoutResult, unknown, Record<string, unknown>>({
|
|
135
136
|
mutationFn: async (body) => {
|
|
136
|
-
const res = await client.post("/public/checkouts", { profileId, ...body });
|
|
137
|
+
const res = await client.post("/public/checkouts", { profileId, ...body, metaSignals: readMetaSignals() });
|
|
137
138
|
return res.data;
|
|
138
139
|
},
|
|
139
140
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BookingSlot, CancellationTermsView, PillarDiscountQuote } from "../../types/models";
|
|
2
|
+
import { fireServerMetaEvent, readMetaSignals, type ServerMetaEvent } from "../../utils/metaEvents";
|
|
2
3
|
import { useForge } from "../../provider/ForgeProvider";
|
|
3
4
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
4
5
|
|
|
@@ -44,6 +45,7 @@ export function useReserveCoachingBooking(productId?: string) {
|
|
|
44
45
|
mutationFn: async ({ slotId }) => {
|
|
45
46
|
const res = await client.post(`/public/coaching/products/${productId}/booking/reserve`, {
|
|
46
47
|
slotId,
|
|
48
|
+
metaSignals: readMetaSignals(),
|
|
47
49
|
});
|
|
48
50
|
return res.data;
|
|
49
51
|
},
|
|
@@ -71,6 +73,8 @@ export type UpdateCoachingBookingInput = {
|
|
|
71
73
|
export type UpdateCoachingBookingResult = PillarDiscountQuote & {
|
|
72
74
|
bookingId: string;
|
|
73
75
|
isConfirmed: boolean;
|
|
76
|
+
/** Set when this call newly confirmed a free booking and the server queued Schedule. */
|
|
77
|
+
metaEvent?: ServerMetaEvent;
|
|
74
78
|
};
|
|
75
79
|
|
|
76
80
|
/**
|
|
@@ -87,9 +91,14 @@ export function useUpdateCoachingBooking(productId?: string) {
|
|
|
87
91
|
|
|
88
92
|
return useMutation<UpdateCoachingBookingResult, unknown, UpdateCoachingBookingInput>({
|
|
89
93
|
mutationFn: async (body) => {
|
|
90
|
-
const res = await client.post(`/public/coaching/products/${productId}/booking/update`,
|
|
94
|
+
const res = await client.post(`/public/coaching/products/${productId}/booking/update`, {
|
|
95
|
+
...body,
|
|
96
|
+
metaSignals: readMetaSignals(),
|
|
97
|
+
});
|
|
91
98
|
return res.data;
|
|
92
99
|
},
|
|
100
|
+
// A free booking newly confirmed: the server queued Schedule; fire its pixel twin.
|
|
101
|
+
onSuccess: (data) => fireServerMetaEvent(data),
|
|
93
102
|
});
|
|
94
103
|
}
|
|
95
104
|
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { useContext } from "react";
|
|
2
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3
|
+
import { useForge } from "../../provider/ForgeProvider";
|
|
4
|
+
import { PublicAuthContext } from "../../contexts/PublicAuthContext";
|
|
5
|
+
import type { MembershipTier } from "../../types/models";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The public communities API (`/public/communities/*` and the fan's own
|
|
9
|
+
* memberships). Every creator can run several communities, each with its own
|
|
10
|
+
* tiers, members, feed, chat and calls, so a community is always NAMED by the
|
|
11
|
+
* caller: by slug in a URL, by id everywhere else.
|
|
12
|
+
*
|
|
13
|
+
* These are the raw data hooks. The step machines and state a page is built
|
|
14
|
+
* from live in `ui/headless/communities`, and the member-content hooks
|
|
15
|
+
* (spaces, feed, chat, calls, collections) read the community from
|
|
16
|
+
* `<CommunityProvider>` rather than taking it as an argument.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// ---- Types (mirror docs/proposals/communities-build-contract.md section 3.2) ----
|
|
20
|
+
|
|
21
|
+
/** A community as a visitor sees it. */
|
|
22
|
+
export interface PublicCommunity {
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
slug: string;
|
|
26
|
+
description: string | null;
|
|
27
|
+
coverUrl: string | null;
|
|
28
|
+
logoUrl: string | null;
|
|
29
|
+
category: string | null;
|
|
30
|
+
tags: string[];
|
|
31
|
+
memberCount: number;
|
|
32
|
+
/** How a visitor gets in. Absent on older servers: treat as open. */
|
|
33
|
+
joinMode?: "open" | "application" | "invite";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A tier the viewer may join: the public tier shape plus the community it belongs to. */
|
|
37
|
+
export type PublicTier = MembershipTier & { communityId: string };
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Where the viewer stands in ONE community. Never read across communities: a
|
|
41
|
+
* member of community A is `none` in community B.
|
|
42
|
+
*/
|
|
43
|
+
export type CommunityViewerStateName = "anonymous" | "none" | "pending" | "member" | "past_due" | "cancelled";
|
|
44
|
+
|
|
45
|
+
export interface CommunityViewerMembership {
|
|
46
|
+
id: string;
|
|
47
|
+
tierId: string;
|
|
48
|
+
tierName: string;
|
|
49
|
+
status: string;
|
|
50
|
+
cancelAtPeriodEnd: boolean;
|
|
51
|
+
currentPeriodEnd: string | null;
|
|
52
|
+
/** A downgrade waiting for the end of the paid period, or null. */
|
|
53
|
+
pendingDowngradeTierId: string | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface CommunityViewerApplication {
|
|
57
|
+
id: string;
|
|
58
|
+
tierId: string;
|
|
59
|
+
status: string;
|
|
60
|
+
createdAt: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface CommunityUpgradeTier {
|
|
64
|
+
id: string;
|
|
65
|
+
name: string;
|
|
66
|
+
priceMonthly: number | null;
|
|
67
|
+
priceYearly: number | null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface CommunityViewerState {
|
|
71
|
+
state: CommunityViewerStateName;
|
|
72
|
+
membership: CommunityViewerMembership | null;
|
|
73
|
+
application: CommunityViewerApplication | null;
|
|
74
|
+
/** Tiers above the current one, in this community. Empty for a non-member. */
|
|
75
|
+
upgradeTiers: CommunityUpgradeTier[];
|
|
76
|
+
canInteract: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type CommunityInviteKind = "email" | "link";
|
|
80
|
+
export type CommunityInviteStateName =
|
|
81
|
+
| "valid"
|
|
82
|
+
| "expired"
|
|
83
|
+
| "revoked"
|
|
84
|
+
| "used"
|
|
85
|
+
| "used_up"
|
|
86
|
+
| "email_mismatch"
|
|
87
|
+
| "not_found";
|
|
88
|
+
|
|
89
|
+
export interface CommunityInviteState {
|
|
90
|
+
kind: CommunityInviteKind;
|
|
91
|
+
state: CommunityInviteStateName;
|
|
92
|
+
/** The address a per-email invite was sent to. Null for a shareable link. */
|
|
93
|
+
invitedEmail: string | null;
|
|
94
|
+
/** The tier the invite opens, or null when the token is dead. */
|
|
95
|
+
tier: PublicTier | null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** `GET /public/communities/:slug` */
|
|
99
|
+
export interface CommunityDetail {
|
|
100
|
+
community: PublicCommunity;
|
|
101
|
+
tiers: PublicTier[];
|
|
102
|
+
viewer: CommunityViewerState;
|
|
103
|
+
invite: CommunityInviteState | null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The two invite credentials a URL can carry. Only one is ever set. */
|
|
107
|
+
export interface CommunityTokens {
|
|
108
|
+
/** A per-email invite token (`?invite=`). */
|
|
109
|
+
invite?: string | null;
|
|
110
|
+
/** A shareable invite link token (`?join=`). */
|
|
111
|
+
join?: string | null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface CommunityUpgradePreview {
|
|
115
|
+
amount: number;
|
|
116
|
+
currency: string;
|
|
117
|
+
interval: string;
|
|
118
|
+
/** True on Paystack: the upgrade collects the card again and replaces the subscription. */
|
|
119
|
+
requiresCard: boolean;
|
|
120
|
+
prorated: boolean;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface CommunityUpgradeResult {
|
|
124
|
+
status: "changed" | "requires_payment";
|
|
125
|
+
/** STRIPE: a PaymentIntent secret when the provider needs the card confirmed. */
|
|
126
|
+
clientSecret?: string;
|
|
127
|
+
/** PAYSTACK: the new subscription's inline checkout session. */
|
|
128
|
+
accessCode?: string;
|
|
129
|
+
/** PAYSTACK: hosted page fallback. */
|
|
130
|
+
checkoutUrl?: string;
|
|
131
|
+
membership: CommunityViewerMembership | null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** One entry of `GET /public/memberships/mine`: a membership in one community. */
|
|
135
|
+
export interface MyCommunityMembership {
|
|
136
|
+
id: string;
|
|
137
|
+
tierId?: string;
|
|
138
|
+
membershipTierId?: string;
|
|
139
|
+
tierName?: string;
|
|
140
|
+
status: string;
|
|
141
|
+
cancelAtPeriodEnd?: boolean;
|
|
142
|
+
currentPeriodEnd?: string | null;
|
|
143
|
+
endDate?: string | null;
|
|
144
|
+
pendingDowngradeTierId?: string | null;
|
|
145
|
+
subscriptionAmount?: number | null;
|
|
146
|
+
subscriptionCurrency?: string | null;
|
|
147
|
+
billingCycle?: string | null;
|
|
148
|
+
trialEndsAt?: string | null;
|
|
149
|
+
membershipTier?: { id: string; name: string } | null;
|
|
150
|
+
community: { id: string; name: string; slug: string };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The signed-in account, read softly: these hooks also run on a page with no
|
|
155
|
+
* auth provider (a static About page), where the viewer is simply anonymous.
|
|
156
|
+
*/
|
|
157
|
+
export function useViewerAccountId(): string | null {
|
|
158
|
+
return useContext(PublicAuthContext)?.user?.id ?? null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---- Keys ---------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
export const communitiesKeys = {
|
|
164
|
+
all: ["public-communities"] as const,
|
|
165
|
+
list: (profileId?: string) => ["public-communities", "list", profileId] as const,
|
|
166
|
+
detail: (profileId: string | undefined, ref: string | undefined, tokens: CommunityTokens, accountId?: string | null) =>
|
|
167
|
+
["public-communities", "detail", profileId, ref, tokens.invite ?? null, tokens.join ?? null, accountId ?? null] as const,
|
|
168
|
+
me: (profileId: string | undefined, ref: string | undefined, accountId?: string | null) =>
|
|
169
|
+
["public-communities", "me", profileId, ref, accountId ?? null] as const,
|
|
170
|
+
invite: (profileId: string | undefined, ref: string | undefined, tokens: CommunityTokens, accountId?: string | null) =>
|
|
171
|
+
["public-communities", "invite", profileId, ref, tokens.invite ?? null, tokens.join ?? null, accountId ?? null] as const,
|
|
172
|
+
upgradePreview: (profileId: string | undefined, ref: string | undefined, tierId: string | undefined) =>
|
|
173
|
+
["public-communities", "upgrade-preview", profileId, ref, tierId ?? null] as const,
|
|
174
|
+
mine: (profileId?: string, accountId?: string | null) => ["public-communities", "mine", profileId, accountId ?? null] as const,
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const tokenParams = (tokens: CommunityTokens) => ({
|
|
178
|
+
...(tokens.invite ? { invite: tokens.invite } : {}),
|
|
179
|
+
...(tokens.join ? { join: tokens.join } : {}),
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const enc = (ref: string) => encodeURIComponent(ref);
|
|
183
|
+
|
|
184
|
+
// ---- Reads ----------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
/** Every live community on the profile (`GET /public/communities`). */
|
|
187
|
+
export function usePublicCommunities(opts: { enabled?: boolean } = {}) {
|
|
188
|
+
const { client, profileId } = useForge();
|
|
189
|
+
return useQuery<PublicCommunity[]>({
|
|
190
|
+
queryKey: communitiesKeys.list(profileId),
|
|
191
|
+
queryFn: async () => (await client.get("/public/communities", { params: { profileId } })).data,
|
|
192
|
+
enabled: !!profileId && !!client && opts.enabled !== false,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* One community with its joinable tiers, the viewer's state and, when the URL
|
|
198
|
+
* carried a token, the invite (`GET /public/communities/:slug`). `ref` is a
|
|
199
|
+
* slug or an id. The token is part of the key because it changes which tiers
|
|
200
|
+
* come back: an invite-only tier is listed only with a token that opens it.
|
|
201
|
+
*/
|
|
202
|
+
export function usePublicCommunity(ref: string | undefined, tokens: CommunityTokens = {}, opts: { enabled?: boolean } = {}) {
|
|
203
|
+
const { client, profileId } = useForge();
|
|
204
|
+
const accountId = useViewerAccountId();
|
|
205
|
+
return useQuery<CommunityDetail>({
|
|
206
|
+
queryKey: communitiesKeys.detail(profileId, ref, tokens, accountId),
|
|
207
|
+
queryFn: async () =>
|
|
208
|
+
(await client.get(`/public/communities/${enc(ref!)}`, { params: { profileId, ...tokenParams(tokens) } })).data,
|
|
209
|
+
enabled: !!ref && !!profileId && !!client && opts.enabled !== false,
|
|
210
|
+
retry: (count, error) => !isNotFound(error) && count < 2,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** The viewer's state in one community (`GET /public/communities/:slug/me`). Keyed per account. */
|
|
215
|
+
export function useCommunityViewerState(ref: string | undefined, opts: { enabled?: boolean } = {}) {
|
|
216
|
+
const { client, profileId } = useForge();
|
|
217
|
+
const accountId = useViewerAccountId();
|
|
218
|
+
return useQuery<CommunityViewerState>({
|
|
219
|
+
queryKey: communitiesKeys.me(profileId, ref, accountId),
|
|
220
|
+
queryFn: async () => (await client.get(`/public/communities/${enc(ref!)}/me`, { params: { profileId } })).data,
|
|
221
|
+
enabled: !!ref && !!profileId && !!client && opts.enabled !== false,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* What an invite token is worth (`GET /public/communities/:slug/invite`).
|
|
227
|
+
* Keyed per account because `email_mismatch` depends on who is signed in.
|
|
228
|
+
*/
|
|
229
|
+
export function useCommunityInviteState(ref: string | undefined, tokens: CommunityTokens) {
|
|
230
|
+
const { client, profileId } = useForge();
|
|
231
|
+
const accountId = useViewerAccountId();
|
|
232
|
+
const hasToken = !!(tokens.invite || tokens.join);
|
|
233
|
+
return useQuery<CommunityInviteState>({
|
|
234
|
+
queryKey: communitiesKeys.invite(profileId, ref, tokens, accountId),
|
|
235
|
+
queryFn: async () =>
|
|
236
|
+
(await client.get(`/public/communities/${enc(ref!)}/invite`, { params: { profileId, ...tokenParams(tokens) } })).data,
|
|
237
|
+
enabled: !!ref && hasToken && !!profileId && !!client,
|
|
238
|
+
retry: (count, error) => !isNotFound(error) && count < 2,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** What moving to `membershipTierId` costs now (`GET /public/communities/:slug/upgrade-preview`). */
|
|
243
|
+
export function useCommunityUpgradePreview(ref: string | undefined, membershipTierId: string | undefined) {
|
|
244
|
+
const { client, profileId } = useForge();
|
|
245
|
+
return useQuery<CommunityUpgradePreview>({
|
|
246
|
+
queryKey: communitiesKeys.upgradePreview(profileId, ref, membershipTierId),
|
|
247
|
+
queryFn: async () =>
|
|
248
|
+
(
|
|
249
|
+
await client.get(`/public/communities/${enc(ref!)}/upgrade-preview`, {
|
|
250
|
+
params: { profileId, membershipTierId },
|
|
251
|
+
})
|
|
252
|
+
).data,
|
|
253
|
+
enabled: !!ref && !!membershipTierId && !!profileId && !!client,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Every membership the signed-in fan holds with this creator, one per community. */
|
|
258
|
+
export function useMyCommunityMemberships(opts: { enabled?: boolean } = {}) {
|
|
259
|
+
const { client, profileId } = useForge();
|
|
260
|
+
const accountId = useViewerAccountId();
|
|
261
|
+
return useQuery<MyCommunityMembership[]>({
|
|
262
|
+
queryKey: communitiesKeys.mine(profileId, accountId),
|
|
263
|
+
// The server answers `[{ community, membership }]`; the hooks read one flat
|
|
264
|
+
// row per community, so the envelope is unwrapped here, in one place.
|
|
265
|
+
queryFn: async () => {
|
|
266
|
+
const rows = (await client.get("/public/memberships/mine", { params: { profileId } })).data as Array<{
|
|
267
|
+
community: MyCommunityMembership["community"];
|
|
268
|
+
membership: Record<string, unknown> & { amount?: number | null; currency?: string | null };
|
|
269
|
+
}>;
|
|
270
|
+
return rows.map(
|
|
271
|
+
({ community, membership }) =>
|
|
272
|
+
({
|
|
273
|
+
...membership,
|
|
274
|
+
community,
|
|
275
|
+
subscriptionAmount: membership.amount ?? null,
|
|
276
|
+
subscriptionCurrency: membership.currency ?? null,
|
|
277
|
+
}) as unknown as MyCommunityMembership,
|
|
278
|
+
);
|
|
279
|
+
},
|
|
280
|
+
enabled: !!accountId && !!profileId && !!client && opts.enabled !== false,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ---- Writes -----------------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
/** Every community read goes stale together after a membership change. */
|
|
287
|
+
export function useInvalidateCommunities() {
|
|
288
|
+
const queryClient = useQueryClient();
|
|
289
|
+
return () =>
|
|
290
|
+
Promise.all([
|
|
291
|
+
queryClient.invalidateQueries({ queryKey: communitiesKeys.all }),
|
|
292
|
+
queryClient.invalidateQueries({ queryKey: ["membership-tiers"] }),
|
|
293
|
+
queryClient.invalidateQueries({ queryKey: ["my-membership-applications"] }),
|
|
294
|
+
]);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Move to a higher tier (`POST /public/communities/:slug/upgrade`). */
|
|
298
|
+
export function useUpgradeCommunityTier(ref: string | undefined) {
|
|
299
|
+
const { client, profileId } = useForge();
|
|
300
|
+
const invalidate = useInvalidateCommunities();
|
|
301
|
+
return useMutation<CommunityUpgradeResult, unknown, { membershipTierId: string; returnUrl?: string }>({
|
|
302
|
+
mutationFn: async (body) =>
|
|
303
|
+
(await client.post(`/public/communities/${enc(ref!)}/upgrade`, { profileId, ...body })).data,
|
|
304
|
+
onSuccess: (data) => {
|
|
305
|
+
if (data.status === "changed") void invalidate();
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Cancel (`POST /public/communities/:slug/membership/cancel`). A paid period runs out first. */
|
|
311
|
+
export function useCancelCommunityMembership(ref: string | undefined) {
|
|
312
|
+
const { client, profileId } = useForge();
|
|
313
|
+
const invalidate = useInvalidateCommunities();
|
|
314
|
+
return useMutation<unknown, unknown, void>({
|
|
315
|
+
mutationFn: async () => (await client.post(`/public/communities/${enc(ref!)}/membership/cancel`, { profileId })).data,
|
|
316
|
+
onSuccess: () => void invalidate(),
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Downgrade at the end of the paid period (`POST /public/communities/:slug/membership/downgrade`). */
|
|
321
|
+
export function useDowngradeCommunityMembership(ref: string | undefined) {
|
|
322
|
+
const { client, profileId } = useForge();
|
|
323
|
+
const invalidate = useInvalidateCommunities();
|
|
324
|
+
return useMutation<unknown, unknown, { membershipTierId: string }>({
|
|
325
|
+
mutationFn: async (body) =>
|
|
326
|
+
(await client.post(`/public/communities/${enc(ref!)}/membership/downgrade`, { profileId, ...body })).data,
|
|
327
|
+
onSuccess: () => void invalidate(),
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Withdraw a pending downgrade (`POST /public/communities/:slug/membership/undo-downgrade`). */
|
|
332
|
+
export function useUndoCommunityDowngrade(ref: string | undefined) {
|
|
333
|
+
const { client, profileId } = useForge();
|
|
334
|
+
const invalidate = useInvalidateCommunities();
|
|
335
|
+
return useMutation<unknown, unknown, void>({
|
|
336
|
+
mutationFn: async () =>
|
|
337
|
+
(await client.post(`/public/communities/${enc(ref!)}/membership/undo-downgrade`, { profileId })).data,
|
|
338
|
+
onSuccess: () => void invalidate(),
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** True when an error is a 404: an answer (no such community, dead token), not a transient failure. */
|
|
343
|
+
export function isNotFound(error: unknown): boolean {
|
|
344
|
+
return (error as { response?: { status?: number } } | null)?.response?.status === 404;
|
|
345
|
+
}
|