@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
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
4
|
+
import { useForge } from "../../../provider/ForgeProvider";
|
|
5
|
+
import { usePublicAuth } from "../../../contexts/PublicAuthContext";
|
|
6
|
+
import { apiErrorMessage } from "../coupon/useCouponField";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The download page behind a delivery email (`/i/downloads/$token`).
|
|
10
|
+
*
|
|
11
|
+
* The token in the link finds the order and opens nothing. The files come out
|
|
12
|
+
* to a signed-in owner straight away, or to whoever types the code we email to
|
|
13
|
+
* the recipient. A right code also returns a GRANT, a signed one-hour pass for
|
|
14
|
+
* this delivery, held here in memory so a second click does not need a second
|
|
15
|
+
* email. Every file address is a ten-minute signed read, so they are fetched
|
|
16
|
+
* again (with the grant or the session) once they are close to expiring.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export interface DownloadItem {
|
|
20
|
+
title: string;
|
|
21
|
+
quantity: number;
|
|
22
|
+
coverImage: string | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface DownloadFileInfo {
|
|
26
|
+
id: string;
|
|
27
|
+
filename: string;
|
|
28
|
+
size: number | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface DownloadPageInfo {
|
|
32
|
+
items: DownloadItem[];
|
|
33
|
+
files: DownloadFileInfo[];
|
|
34
|
+
/** The recipient's address, masked: `b***@example.com`. */
|
|
35
|
+
sentTo: string;
|
|
36
|
+
isGift: boolean;
|
|
37
|
+
signedInAsOwner: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface DownloadFile extends DownloadFileInfo {
|
|
41
|
+
/** A signed read, valid for ten minutes from when it was fetched. */
|
|
42
|
+
url: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type FilesResponse = { files: DownloadFile[]; grant?: string };
|
|
46
|
+
|
|
47
|
+
/** Fetch again a minute before the ten-minute signature runs out. */
|
|
48
|
+
const URL_REFRESH_AFTER_MS = 9 * 60 * 1000;
|
|
49
|
+
|
|
50
|
+
export function useDownloadPage(token: string) {
|
|
51
|
+
const { client, profileId } = useForge();
|
|
52
|
+
const { user } = usePublicAuth();
|
|
53
|
+
const grant = useRef<string | null>(null);
|
|
54
|
+
const [files, setFiles] = useState<DownloadFile[] | null>(null);
|
|
55
|
+
const [fetchedAt, setFetchedAt] = useState(0);
|
|
56
|
+
const [codeSentTo, setCodeSentTo] = useState<string | null>(null);
|
|
57
|
+
|
|
58
|
+
const page = useQuery({
|
|
59
|
+
queryKey: ["download-page", profileId, token, user?.id ?? null],
|
|
60
|
+
queryFn: async () => {
|
|
61
|
+
const res = await client.get<DownloadPageInfo>(`/public/downloads/${encodeURIComponent(token)}`, {
|
|
62
|
+
params: { profileId },
|
|
63
|
+
});
|
|
64
|
+
return res.data;
|
|
65
|
+
},
|
|
66
|
+
retry: false,
|
|
67
|
+
enabled: !!token && !!profileId,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const unlock = useMutation({
|
|
71
|
+
mutationFn: async (input: { code?: string }) => {
|
|
72
|
+
const res = await client.post<FilesResponse>(`/public/downloads/${encodeURIComponent(token)}/files`, {
|
|
73
|
+
profileId,
|
|
74
|
+
...(input.code ? { code: input.code } : {}),
|
|
75
|
+
...(!input.code && grant.current ? { grant: grant.current } : {}),
|
|
76
|
+
});
|
|
77
|
+
return res.data;
|
|
78
|
+
},
|
|
79
|
+
onSuccess: (data) => {
|
|
80
|
+
if (data.grant) grant.current = data.grant;
|
|
81
|
+
setFiles(data.files);
|
|
82
|
+
setFetchedAt(Date.now());
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const requestCode = useMutation({
|
|
87
|
+
mutationFn: async () => {
|
|
88
|
+
const res = await client.post<{ sentTo: string }>(`/public/downloads/${encodeURIComponent(token)}/code`, {
|
|
89
|
+
profileId,
|
|
90
|
+
});
|
|
91
|
+
return res.data;
|
|
92
|
+
},
|
|
93
|
+
onSuccess: (data) => setCodeSentTo(data.sentTo),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// A signed-in owner has nothing to prove: fetch the files on arrival.
|
|
97
|
+
const signedInAsOwner = !!page.data?.signedInAsOwner;
|
|
98
|
+
const { mutate: unlockMutate } = unlock;
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (signedInAsOwner && !files) unlockMutate({});
|
|
101
|
+
}, [signedInAsOwner, files, unlockMutate]);
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The address to send the browser to for one file, re-fetching the set first
|
|
105
|
+
* when the signatures are about to lapse.
|
|
106
|
+
*/
|
|
107
|
+
const urlFor = useCallback(
|
|
108
|
+
async (fileId: string): Promise<string | null> => {
|
|
109
|
+
let current = files;
|
|
110
|
+
if (!current || Date.now() - fetchedAt > URL_REFRESH_AFTER_MS) {
|
|
111
|
+
current = (await unlock.mutateAsync({})).files;
|
|
112
|
+
}
|
|
113
|
+
return current.find((f) => f.id === fileId)?.url ?? null;
|
|
114
|
+
},
|
|
115
|
+
[files, fetchedAt, unlock],
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const status: "loading" | "missing" | "locked" | "unlocked" =
|
|
119
|
+
page.isLoading ? "loading" : page.isError || !page.data ? "missing" : files ? "unlocked" : "locked";
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
status,
|
|
123
|
+
info: page.data ?? null,
|
|
124
|
+
files,
|
|
125
|
+
codeSentTo,
|
|
126
|
+
requestCode: () => requestCode.mutate(),
|
|
127
|
+
isSendingCode: requestCode.isPending,
|
|
128
|
+
sendCodeError: requestCode.error ? (apiErrorMessage(requestCode.error) ?? null) : null,
|
|
129
|
+
submitCode: (code: string) => unlock.mutate({ code }),
|
|
130
|
+
isUnlocking: unlock.isPending,
|
|
131
|
+
unlockError: unlock.error ? (apiErrorMessage(unlock.error) ?? null) : null,
|
|
132
|
+
urlFor,
|
|
133
|
+
};
|
|
134
|
+
}
|
package/src/ui/headless/index.ts
CHANGED
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
export * from "./dialog";
|
|
4
4
|
export * from "./donation";
|
|
5
5
|
export * from "./offer";
|
|
6
|
-
|
|
6
|
+
// Communities: the provider every member-content hook reads, and the headless
|
|
7
|
+
// pages (list, About, join, invite, pending, upgrade, membership, gate).
|
|
8
|
+
// `CommunityGate` replaces the old `MembershipGate`, whose "any membership on
|
|
9
|
+
// the profile" rule is exactly what several communities make wrong.
|
|
10
|
+
export * from "./communities";
|
|
7
11
|
// S.4 members-only gates: the notice a storefront draws BEFORE the buyer pays.
|
|
8
12
|
export { useMembershipGateNotice, type UseMembershipGateNoticeInput } from "./membership/useMembershipGateNotice";
|
|
9
13
|
export { useEmailListForm, type UseEmailListFormOptions, type FormStatus } from "./forms/useEmailListForm";
|
|
@@ -27,7 +31,7 @@ export {
|
|
|
27
31
|
type CheckoutShippingData,
|
|
28
32
|
type SelectedShippingRate,
|
|
29
33
|
} from "./checkout/useCheckout";
|
|
30
|
-
// Inventory holds
|
|
34
|
+
// Inventory holds: the reservation a buyer is given while they pay, and the
|
|
31
35
|
// 409 they get when it lapses. Shared by both rendering stacks.
|
|
32
36
|
export { useInventoryHold, type InventoryHoldState, type UseInventoryHoldOptions } from "./checkout/useInventoryHold";
|
|
33
37
|
// Cart recovery: what a resume link does when the buyer clicks it. Shared so
|
|
@@ -57,8 +61,14 @@ export {
|
|
|
57
61
|
} from "./checkout/inventoryHold";
|
|
58
62
|
export { useLoginFlow, type LoginStep } from "./auth/useLoginFlow";
|
|
59
63
|
export { useSignupForm, type UseSignupFormOptions } from "./auth/useSignupForm";
|
|
64
|
+
// Email-first sign-in: one email field and a code; a name only for someone new.
|
|
65
|
+
export {
|
|
66
|
+
useEmailCodeSignIn,
|
|
67
|
+
type EmailCodeSignInStep,
|
|
68
|
+
type UseEmailCodeSignInOptions,
|
|
69
|
+
} from "./auth/useEmailCodeSignIn";
|
|
60
70
|
|
|
61
|
-
// Flow primitives (Part A2)
|
|
71
|
+
// Flow primitives (Part A2): multi-step purchase/booking/subscribe/chat flows,
|
|
62
72
|
// each composed from the data hooks so a creator can rebuild any page.
|
|
63
73
|
export {
|
|
64
74
|
useEventCheckout,
|
|
@@ -196,7 +206,14 @@ export * from "./document";
|
|
|
196
206
|
export * from "./film";
|
|
197
207
|
// Work (project management) client-portal headless primitives.
|
|
198
208
|
export * from "./work";
|
|
199
|
-
//
|
|
209
|
+
// The client portal (`/i/account/*`): summary, invoices, documents, activity, approvals.
|
|
210
|
+
export * from "./portal";
|
|
211
|
+
// The download page behind a digital delivery email (`/i/downloads/$token`).
|
|
212
|
+
export * from "./downloads";
|
|
213
|
+
// The site app: its bottom bar model, its home sections, and its install and
|
|
214
|
+
// notification prompts (`requestAppInstall`).
|
|
215
|
+
export * from "./siteApp";
|
|
216
|
+
// Funnel instrumentation: wrap a multi-step flow to record step-through and
|
|
200
217
|
// drop-off on the first-party analytics feed.
|
|
201
218
|
export * from "./funnel";
|
|
202
219
|
export * from "./useVariantSelection";
|
|
@@ -1,10 +1,20 @@
|
|
|
1
|
-
//
|
|
1
|
+
// A community's member home (`/i/communities/<slug>/home`): the headless nav model.
|
|
2
2
|
export {
|
|
3
3
|
useMemberHomeNav,
|
|
4
4
|
type MemberHomeNavItem,
|
|
5
5
|
type MemberHomeNavKey,
|
|
6
6
|
type UseMemberHomeNavOptions,
|
|
7
7
|
} from "./useMemberHomeNav";
|
|
8
|
+
// The phone's bottom bar and its "More" sheet, decided from the nav model.
|
|
9
|
+
export {
|
|
10
|
+
buildMemberHomeBar,
|
|
11
|
+
MEMBER_HOME_BAR_PRIORITY,
|
|
12
|
+
MEMBER_HOME_BAR_SLOTS,
|
|
13
|
+
MEMBER_HOME_MORE_GROUPS,
|
|
14
|
+
type MemberHomeBarInput,
|
|
15
|
+
type MemberHomeBarModel,
|
|
16
|
+
type MemberHomeMoreGroup,
|
|
17
|
+
} from "./memberHomeBar";
|
|
8
18
|
|
|
9
19
|
// The member call calendar's model. Grouping lives here so the two rendering
|
|
10
20
|
// surfaces cannot disagree about which calls are "today".
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { MemberHomeNavKey } from "./useMemberHomeNav";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The phone layout of a community's member home: a bottom bar of four slots,
|
|
5
|
+
* then "More", which opens a sheet with everything else. Kept apart from how it
|
|
6
|
+
* is drawn so the bar, the sheet and a spec can never disagree about what goes
|
|
7
|
+
* where.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** How many items the bar holds before "More". */
|
|
11
|
+
export const MEMBER_HOME_BAR_SLOTS = 4;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Who gets a slot, in order. The first four are the ones a member opens daily;
|
|
15
|
+
* when one of them is not shown for this community, the next shown item in this
|
|
16
|
+
* list moves up into its slot.
|
|
17
|
+
*/
|
|
18
|
+
export const MEMBER_HOME_BAR_PRIORITY: readonly MemberHomeNavKey[] = [
|
|
19
|
+
"posts",
|
|
20
|
+
"spaces",
|
|
21
|
+
"chat",
|
|
22
|
+
"notifications",
|
|
23
|
+
"calendar",
|
|
24
|
+
"classroom",
|
|
25
|
+
"members",
|
|
26
|
+
"wiki",
|
|
27
|
+
"collections",
|
|
28
|
+
"leaderboard",
|
|
29
|
+
"moderation",
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
/** The "More" sheet's groups and the order inside each. */
|
|
33
|
+
export const MEMBER_HOME_MORE_GROUPS: Readonly<Record<MemberHomeMoreGroup, readonly MemberHomeNavKey[]>> = {
|
|
34
|
+
community: ["collections", "calendar", "classroom", "wiki", "members", "leaderboard"],
|
|
35
|
+
you: ["moderation"],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type MemberHomeMoreGroup = "community" | "you";
|
|
39
|
+
|
|
40
|
+
/** The least a nav item needs for the bar model: its key, whether it is shown, and whether it is the page. */
|
|
41
|
+
export interface MemberHomeBarInput<T extends { key: MemberHomeNavKey }> {
|
|
42
|
+
/** The items shown for this community (`useMemberHomeNav().visibleItems`). */
|
|
43
|
+
visibleItems: readonly T[];
|
|
44
|
+
/** The page being shown, or null. */
|
|
45
|
+
activeKey: MemberHomeNavKey | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface MemberHomeBarModel<T extends { key: MemberHomeNavKey }> {
|
|
49
|
+
/** Up to four items for the bar, in slot order. */
|
|
50
|
+
bar: T[];
|
|
51
|
+
/** Everything else that is shown, grouped for the sheet. Empty groups are left out. */
|
|
52
|
+
more: Array<{ group: MemberHomeMoreGroup; items: T[] }>;
|
|
53
|
+
/** The page being shown lives in the sheet, so "More" is the highlighted button. */
|
|
54
|
+
moreActive: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Decide what sits in the bar and what goes behind "More". Pure.
|
|
59
|
+
*
|
|
60
|
+
* An item is never in both places, and every shown item is in one of them: a
|
|
61
|
+
* key no group names (none today) lands at the end of "Community" rather than
|
|
62
|
+
* disappearing.
|
|
63
|
+
*/
|
|
64
|
+
export function buildMemberHomeBar<T extends { key: MemberHomeNavKey }>({
|
|
65
|
+
visibleItems,
|
|
66
|
+
activeKey,
|
|
67
|
+
}: MemberHomeBarInput<T>): MemberHomeBarModel<T> {
|
|
68
|
+
const byKey = new Map(visibleItems.map((item) => [item.key, item] as const));
|
|
69
|
+
const bar = MEMBER_HOME_BAR_PRIORITY.filter((key) => byKey.has(key))
|
|
70
|
+
.slice(0, MEMBER_HOME_BAR_SLOTS)
|
|
71
|
+
.map((key) => byKey.get(key)!);
|
|
72
|
+
const inBar = new Set(bar.map((item) => item.key));
|
|
73
|
+
const rest = visibleItems.filter((item) => !inBar.has(item.key));
|
|
74
|
+
|
|
75
|
+
const placed = new Set<MemberHomeNavKey>();
|
|
76
|
+
const more = (Object.keys(MEMBER_HOME_MORE_GROUPS) as MemberHomeMoreGroup[]).map((group) => {
|
|
77
|
+
const items = MEMBER_HOME_MORE_GROUPS[group]
|
|
78
|
+
.map((key) => rest.find((item) => item.key === key))
|
|
79
|
+
.filter((item): item is T => !!item);
|
|
80
|
+
items.forEach((item) => placed.add(item.key));
|
|
81
|
+
return { group, items };
|
|
82
|
+
});
|
|
83
|
+
const unplaced = rest.filter((item) => !placed.has(item.key));
|
|
84
|
+
if (unplaced.length) more[0]!.items.push(...unplaced);
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
bar,
|
|
88
|
+
more: more.filter((g) => g.items.length > 0),
|
|
89
|
+
moreActive: !!activeKey && !inBar.has(activeKey) && rest.some((item) => item.key === activeKey),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -1,31 +1,55 @@
|
|
|
1
|
-
import { useMemo } from "react";
|
|
1
|
+
import { useContext, useMemo } from "react";
|
|
2
2
|
import { useGetPostCollections } from "../../../data/queries/usePostCollections";
|
|
3
|
-
import { useCommunitySpaces } from "../../../data/queries/useCommunity";
|
|
3
|
+
import { useCommunityCanModerate, useCommunitySpaces } from "../../../data/queries/useCommunity";
|
|
4
|
+
import { PublicAuthContext } from "../../../contexts/PublicAuthContext";
|
|
5
|
+
import { useCommunityWikiPages } from "../../../data/queries/useCommunityWiki";
|
|
6
|
+
import { useCommunityViewerState } from "../../../data/queries/useCommunities";
|
|
7
|
+
import { viewerHasAccess } from "../communities/useCommunity";
|
|
4
8
|
import { useChatChannels } from "../../../data/queries/useChat";
|
|
5
|
-
import {
|
|
9
|
+
import { MissingCommunityProviderError, useOptionalCommunityContext } from "../../../contexts/CommunityContext";
|
|
10
|
+
import { internalPath } from "../../../runtime/pages";
|
|
6
11
|
|
|
7
|
-
export type MemberHomeNavKey =
|
|
12
|
+
export type MemberHomeNavKey =
|
|
13
|
+
| "posts"
|
|
14
|
+
| "collections"
|
|
15
|
+
| "spaces"
|
|
16
|
+
| "calendar"
|
|
17
|
+
| "classroom"
|
|
18
|
+
| "chat"
|
|
19
|
+
| "leaderboard"
|
|
20
|
+
| "notifications"
|
|
21
|
+
| "members"
|
|
22
|
+
| "wiki"
|
|
23
|
+
| "moderation";
|
|
8
24
|
|
|
9
25
|
export interface MemberHomeNavItem {
|
|
10
26
|
key: MemberHomeNavKey;
|
|
11
27
|
label: string;
|
|
12
28
|
/** Absolute in-site href (already run through `buildHref` when supplied). */
|
|
13
29
|
href: string;
|
|
14
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* Conditional tabs (Collections/Spaces/Calendar/Chat/Wiki/Leaderboard/Notifications)
|
|
32
|
+
* hide until the community has content for them. Members, Calendar and
|
|
33
|
+
* Classroom show to members (the server answers them only). Moderation shows only to
|
|
34
|
+
* the community's moderators (and the profile's owners and admins).
|
|
35
|
+
*/
|
|
15
36
|
visible: boolean;
|
|
16
|
-
/** Whether `currentPath` falls
|
|
37
|
+
/** Whether `currentPath` (or `activeKey`) falls on this tab. */
|
|
17
38
|
active: boolean;
|
|
18
|
-
/**
|
|
39
|
+
/** Kept for hosts that drew an out-of-home marker. Always false: every tab is inside the community. */
|
|
19
40
|
external: boolean;
|
|
20
41
|
}
|
|
21
42
|
|
|
22
43
|
export interface UseMemberHomeNavOptions {
|
|
23
44
|
/** Current location pathname, used to compute `active`. */
|
|
24
45
|
currentPath?: string;
|
|
25
|
-
/**
|
|
46
|
+
/** The active tab, when the host knows it (wins over `currentPath`). */
|
|
47
|
+
activeKey?: MemberHomeNavKey | null;
|
|
48
|
+
/**
|
|
49
|
+
* Member home base path. Default: the surrounding community's home,
|
|
50
|
+
* `/i/communities/<slug>/home`.
|
|
51
|
+
*/
|
|
26
52
|
basePath?: string;
|
|
27
|
-
/** "My membership" target. Client: `/account?tab=membership` (default). Site-starter: `/i/account?tab=membership`. */
|
|
28
|
-
membershipHref?: string;
|
|
29
53
|
/** Optional href transform (e.g. locale prefixes). Applied to every item. */
|
|
30
54
|
buildHref?: (href: string) => string;
|
|
31
55
|
}
|
|
@@ -36,17 +60,21 @@ const normalize = (p: string) => {
|
|
|
36
60
|
};
|
|
37
61
|
|
|
38
62
|
/**
|
|
39
|
-
* Headless nav model for
|
|
40
|
-
* visibility derives client-side from
|
|
41
|
-
* shared via React Query)
|
|
42
|
-
* the
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
63
|
+
* Headless nav model for a community's member home
|
|
64
|
+
* (`/i/communities/<slug>/home`). Tab visibility derives client-side from the
|
|
65
|
+
* queries the pages already need (cache shared via React Query), all scoped to
|
|
66
|
+
* the surrounding `<CommunityProvider>`: Posts is always visible; Collections
|
|
67
|
+
* when the community has any; Spaces (with its Leaderboard and Notifications)
|
|
68
|
+
* when any space is accessible; Chat when any channel is. Calendar, Classroom
|
|
69
|
+
* and Members show to members, whose reads they are. Billing, cancel and downgrade live in the community header's
|
|
70
|
+
* Membership menu, not here, and nothing links to the account page. Hosts own
|
|
71
|
+
* routing: pair with any layout via `href`/`active`.
|
|
46
72
|
*/
|
|
47
73
|
export function useMemberHomeNav(opts: UseMemberHomeNavOptions = {}) {
|
|
48
|
-
const
|
|
49
|
-
|
|
74
|
+
const community = useOptionalCommunityContext();
|
|
75
|
+
if (!community) throw new MissingCommunityProviderError("useMemberHomeNav()");
|
|
76
|
+
const slug = community.slug;
|
|
77
|
+
const basePath = opts.basePath ?? (slug ? internalPath("communities/$slug/home", { slug }) : "");
|
|
50
78
|
const buildHref = opts.buildHref ?? ((href: string) => href);
|
|
51
79
|
const currentPath = normalize(opts.currentPath ?? "");
|
|
52
80
|
|
|
@@ -54,53 +82,65 @@ export function useMemberHomeNav(opts: UseMemberHomeNavOptions = {}) {
|
|
|
54
82
|
const spacesQ = useCommunitySpaces();
|
|
55
83
|
const channelsQ = useChatChannels();
|
|
56
84
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* appear over an empty page.
|
|
85
|
+
* The Moderation tab is for this community's moderators only. Display only:
|
|
86
|
+
* every moderation call is checked again by the server. Signed-out viewers
|
|
87
|
+
* never ask (read without `usePublicAuth()`, which throws outside a provider).
|
|
61
88
|
*/
|
|
89
|
+
const signedIn = !!useContext(PublicAuthContext)?.user;
|
|
90
|
+
const canModerate = useCommunityCanModerate(signedIn);
|
|
91
|
+
/** The wiki, once the community has any page this viewer may see listed. */
|
|
92
|
+
const wikiQ = useCommunityWikiPages();
|
|
93
|
+
const hasWiki = Array.isArray(wikiQ.data) && wikiQ.data.length > 0;
|
|
62
94
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* `limit: 1` because this is an existence check: the calendar fetches its own
|
|
71
|
-
* window, and the answer here is only "draw the tab or not".
|
|
95
|
+
* The member directory, the calendar and the classroom are for members (the
|
|
96
|
+
* server answers 403 to anyone else). The calendar lists calls AND the events
|
|
97
|
+
* promoted into the community, and is a month you can page through, so it is
|
|
98
|
+
* a member's tab from the start rather than one that waits for a call. The
|
|
99
|
+
* viewer state is the same read the community header makes, so this adds no
|
|
100
|
+
* request there.
|
|
72
101
|
*/
|
|
73
|
-
const
|
|
102
|
+
const viewerQ = useCommunityViewerState(community.ref, { enabled: signedIn });
|
|
103
|
+
const isMember = signedIn && (viewerHasAccess(viewerQ.data) || canModerate);
|
|
74
104
|
|
|
75
105
|
const hasCollections = (collectionsQ.data?.data?.length ?? 0) > 0;
|
|
76
106
|
const hasSpaces = (spacesQ.data?.length ?? 0) > 0;
|
|
77
107
|
const hasChannels = (channelsQ.data?.length ?? 0) > 0;
|
|
78
|
-
const hasCalls = (callsQ.data?.length ?? 0) > 0;
|
|
79
108
|
|
|
80
109
|
const items: MemberHomeNavItem[] = useMemo(() => {
|
|
81
110
|
const isUnder = (segment: string) =>
|
|
82
111
|
currentPath === normalize(`${basePath}/${segment}`) || currentPath.startsWith(normalize(`${basePath}/${segment}`) + "/");
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
{ key: "
|
|
112
|
+
const onKey = (key: MemberHomeNavKey, byPath: boolean) => (opts.activeKey !== undefined ? opts.activeKey === key : byPath);
|
|
113
|
+
const defs: Array<Omit<MemberHomeNavItem, "href"> & { path: string }> = [
|
|
114
|
+
// The home itself is the posts feed; `home/posts` is the same tab.
|
|
115
|
+
{ key: "posts", label: "Posts", path: basePath, visible: true, external: false, active: onKey("posts", currentPath === normalize(basePath) || isUnder("posts")) },
|
|
116
|
+
{ key: "collections", label: "Collections", path: `${basePath}/collections`, visible: hasCollections, external: false, active: onKey("collections", isUnder("collections")) },
|
|
117
|
+
{ key: "spaces", label: "Spaces", path: `${basePath}/spaces`, visible: hasSpaces, external: false, active: onKey("spaces", isUnder("spaces") || isUnder("profile")) },
|
|
87
118
|
// Scheduled calls. Sits above Chat because a call has a TIME on it: it is
|
|
88
119
|
// the one tab whose content stops being useful if it is found late.
|
|
89
|
-
{ key: "calendar", label: "Calendar", path: `${basePath}/calendar`, visible:
|
|
90
|
-
{ key: "
|
|
91
|
-
{ key: "
|
|
120
|
+
{ key: "calendar", label: "Calendar", path: `${basePath}/calendar`, visible: isMember, external: false, active: onKey("calendar", isUnder("calendar")) },
|
|
121
|
+
{ key: "classroom", label: "Classroom", path: `${basePath}/classroom`, visible: isMember, external: false, active: onKey("classroom", isUnder("classroom")) },
|
|
122
|
+
{ key: "chat", label: "Chat", path: `${basePath}/chat`, visible: hasChannels, external: false, active: onKey("chat", isUnder("chat")) },
|
|
123
|
+
{ key: "leaderboard", label: "Leaderboard", path: `${basePath}/leaderboard`, visible: hasSpaces, external: false, active: onKey("leaderboard", isUnder("leaderboard")) },
|
|
124
|
+
{ key: "notifications", label: "Notifications", path: `${basePath}/notifications`, visible: hasSpaces, external: false, active: onKey("notifications", isUnder("notifications")) },
|
|
125
|
+
{ key: "members", label: "Members", path: `${basePath}/members`, visible: isMember, external: false, active: onKey("members", isUnder("members")) },
|
|
126
|
+
{ key: "wiki", label: "Wiki", path: `${basePath}/wiki`, visible: hasWiki, external: false, active: onKey("wiki", isUnder("wiki")) },
|
|
127
|
+
{ key: "moderation", label: "Moderation", path: `${basePath}/moderation`, visible: canModerate, external: false, active: onKey("moderation", isUnder("moderation")) },
|
|
92
128
|
];
|
|
93
129
|
return defs.map(({ path, ...d }) => ({ ...d, href: buildHref(path) }));
|
|
94
130
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
95
|
-
}, [basePath,
|
|
131
|
+
}, [basePath, currentPath, opts.activeKey, hasCollections, hasSpaces, hasChannels, hasWiki, isMember, canModerate]);
|
|
96
132
|
|
|
97
133
|
return {
|
|
98
134
|
items,
|
|
99
|
-
/** Visible items only
|
|
135
|
+
/** Visible items only: what a shell actually renders. */
|
|
100
136
|
visibleItems: items.filter((i) => i.visible),
|
|
101
137
|
activeKey: items.find((i) => i.active)?.key ?? null,
|
|
102
138
|
/** True once every visibility query has settled (avoids tab flash). */
|
|
103
139
|
isReady:
|
|
104
|
-
|
|
140
|
+
!!basePath &&
|
|
141
|
+
!collectionsQ.isLoading &&
|
|
142
|
+
!spacesQ.isLoading &&
|
|
143
|
+
!channelsQ.isLoading &&
|
|
144
|
+
!wikiQ.isLoading,
|
|
105
145
|
};
|
|
106
146
|
}
|
|
@@ -43,6 +43,14 @@ vi.mock("../../../../data/queries/useMembership", async (importOriginal) => ({
|
|
|
43
43
|
useConfirmMembershipApplication: () => ({ mutateAsync: vi.fn(), isPending: false }),
|
|
44
44
|
}));
|
|
45
45
|
|
|
46
|
+
// Outside a community there is no viewer state to read (the hook falls back to
|
|
47
|
+
// `user.membership`); stubbed like the other queries, since this spec renders
|
|
48
|
+
// without a Forge provider.
|
|
49
|
+
vi.mock("../../../../data/queries/useCommunities", async (importOriginal) => ({
|
|
50
|
+
...(await importOriginal<typeof import("../../../../data/queries/useCommunities")>()),
|
|
51
|
+
useCommunityViewerState: () => ({ data: undefined, isLoading: false }),
|
|
52
|
+
}));
|
|
53
|
+
|
|
46
54
|
vi.mock("../../../../data/queries/useSubscriptions", async (importOriginal) => ({
|
|
47
55
|
...(await importOriginal<typeof import("../../../../data/queries/useSubscriptions")>()),
|
|
48
56
|
useCreateSubscription: () => ({ mutateAsync: subscriptions.createPaid, isPending: false }),
|
|
@@ -87,7 +87,7 @@ beforeEach(() => {
|
|
|
87
87
|
api.createFree.mockResolvedValue({});
|
|
88
88
|
api.createPaid.mockReset();
|
|
89
89
|
auth.refetchUser.mockClear();
|
|
90
|
-
window.history.replaceState(null, "", "/i/
|
|
90
|
+
window.history.replaceState(null, "", "/i/communities/fan-club");
|
|
91
91
|
});
|
|
92
92
|
|
|
93
93
|
const onComplete = vi.fn();
|
|
@@ -214,13 +214,14 @@ describe("application tiers: subscribe() applies", () => {
|
|
|
214
214
|
|
|
215
215
|
describe("invite tokens", () => {
|
|
216
216
|
it("reads the invite off the url and forwards it to the tier list and the paid subscribe", async () => {
|
|
217
|
-
window.history.replaceState(null, "", "/i/
|
|
217
|
+
window.history.replaceState(null, "", "/i/communities/fan-club?tier=tier-1&invite=tok-abc");
|
|
218
218
|
tiers = [tier({ priceMonthly: 12, joinMode: "invite" })];
|
|
219
219
|
api.createPaid.mockResolvedValue({ requiresPayment: true, clientSecret: "pi_1_secret_2" });
|
|
220
220
|
const { result } = renderHook(() => useMembershipCheckout({ initialTierId: "tier-1" }));
|
|
221
221
|
|
|
222
222
|
expect(result.current.inviteToken).toBe("tok-abc");
|
|
223
|
-
|
|
223
|
+
// `enabled` is on because this checkout was handed no tier list of its own.
|
|
224
|
+
expect(api.tiersOptions.at(-1)).toEqual({ inviteToken: "tok-abc", enabled: true });
|
|
224
225
|
await act(async () => {
|
|
225
226
|
await result.current.subscribe();
|
|
226
227
|
});
|
|
@@ -247,7 +248,7 @@ describe("invite tokens", () => {
|
|
|
247
248
|
await result.current.subscribe();
|
|
248
249
|
});
|
|
249
250
|
|
|
250
|
-
expect(api.tiersOptions.at(-1)).toEqual({ inviteToken: null });
|
|
251
|
+
expect(api.tiersOptions.at(-1)).toEqual({ inviteToken: null, enabled: true });
|
|
251
252
|
expect(api.createFree.mock.calls[0][0]).not.toHaveProperty("inviteToken");
|
|
252
253
|
});
|
|
253
254
|
});
|
|
@@ -75,7 +75,7 @@ beforeEach(() => {
|
|
|
75
75
|
api.createFree.mockResolvedValue({});
|
|
76
76
|
api.createPaid.mockReset();
|
|
77
77
|
onComplete.mockClear();
|
|
78
|
-
window.history.replaceState(null, "", "/i/
|
|
78
|
+
window.history.replaceState(null, "", "/i/communities/fan-club");
|
|
79
79
|
});
|
|
80
80
|
|
|
81
81
|
function answerRequired(result: { current: ReturnType<typeof useMembershipCheckout> }) {
|
|
@@ -61,7 +61,7 @@ beforeEach(() => {
|
|
|
61
61
|
api.createPaid.mockReset();
|
|
62
62
|
onComplete.mockReset();
|
|
63
63
|
auth.user.membership = undefined;
|
|
64
|
-
window.history.replaceState(null, "", "/i/
|
|
64
|
+
window.history.replaceState(null, "", "/i/communities/fan-club");
|
|
65
65
|
});
|
|
66
66
|
|
|
67
67
|
describe("useMembershipCheckout: trial exposure", () => {
|