@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,151 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { describe, it, expect, afterEach, vi } from "vitest";
|
|
3
|
+
import { render, screen, fireEvent, cleanup, within } from "@testing-library/react";
|
|
4
|
+
import axios, { type AxiosAdapter, type InternalAxiosRequestConfig } from "axios";
|
|
5
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
6
|
+
import { ForgeClientProvider } from "../../../provider/ForgeProvider";
|
|
7
|
+
import { PublicAuthContext } from "../../../contexts/PublicAuthContext";
|
|
8
|
+
import { ForgeThemeProvider } from "../../theme/ForgeThemeProvider";
|
|
9
|
+
import { AccountDashboard } from "../AccountDashboard";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The account page's Memberships tab. A fan can belong to several communities
|
|
13
|
+
* of one creator, so the tab lists ONE CARD PER COMMUNITY from
|
|
14
|
+
* `/public/memberships/mine`, never the account's single legacy membership.
|
|
15
|
+
* Managing a membership happens inside its community, so each card opens it.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const DAY = 24 * 60 * 60 * 1000;
|
|
19
|
+
const inDays = (n: number) => new Date(Date.now() + n * DAY).toISOString();
|
|
20
|
+
const fmt = (iso: string) => new Date(iso).toLocaleDateString("en", { year: "numeric", month: "short", day: "numeric" });
|
|
21
|
+
|
|
22
|
+
const fan = {
|
|
23
|
+
id: "account-1",
|
|
24
|
+
email: "fan@example.com",
|
|
25
|
+
firstName: "Fan",
|
|
26
|
+
lastName: "Person",
|
|
27
|
+
kind: "fan",
|
|
28
|
+
status: "active",
|
|
29
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
30
|
+
updatedAt: "2026-01-01T00:00:00.000Z",
|
|
31
|
+
// The legacy single membership must NOT be what the tab shows.
|
|
32
|
+
membership: { id: "legacy", membershipTierId: "t-legacy", status: "active", membershipTier: { id: "t-legacy", name: "LEGACY" } },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
let realAdapter: AxiosAdapter | undefined;
|
|
36
|
+
let requested: string[] = [];
|
|
37
|
+
|
|
38
|
+
function mount(mine: unknown[], applications: unknown[] = [], communities: unknown[] = []) {
|
|
39
|
+
requested = [];
|
|
40
|
+
realAdapter = axios.defaults.adapter as AxiosAdapter | undefined;
|
|
41
|
+
axios.defaults.adapter = (async (config: InternalAxiosRequestConfig) => {
|
|
42
|
+
const url = config.url ?? "";
|
|
43
|
+
requested.push(url);
|
|
44
|
+
// `/public/memberships/mine` answers the server's real envelope,
|
|
45
|
+
// `[{ community, membership }]`, built from the flat rows the tests list.
|
|
46
|
+
const data = url.includes("/public/memberships/mine")
|
|
47
|
+
? (mine as Array<Record<string, unknown>>).map(({ community, subscriptionAmount, subscriptionCurrency, ...membership }) => ({
|
|
48
|
+
community,
|
|
49
|
+
membership: { ...membership, amount: subscriptionAmount ?? null, currency: subscriptionCurrency ?? null },
|
|
50
|
+
}))
|
|
51
|
+
: url.includes("/public/membership-applications/mine")
|
|
52
|
+
? applications
|
|
53
|
+
: url === "/public/communities"
|
|
54
|
+
? communities
|
|
55
|
+
: [];
|
|
56
|
+
return { data, status: 200, statusText: "OK", headers: {}, config } as never;
|
|
57
|
+
}) as AxiosAdapter;
|
|
58
|
+
const navigate = vi.fn();
|
|
59
|
+
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
60
|
+
render(
|
|
61
|
+
<QueryClientProvider client={queryClient}>
|
|
62
|
+
<ForgeClientProvider apiUrl="http://api.test" profileId="profile-1">
|
|
63
|
+
<PublicAuthContext.Provider value={{ user: fan, currencies: null, userSelectedCurrency: "USD", logout: () => {} } as never}>
|
|
64
|
+
<ForgeThemeProvider theme={{ colors: { text: "#111111", background: "#ffffff", primary: "#6d28d9" }, cornerRadius: 8 }}>
|
|
65
|
+
<AccountDashboard tab="membership" navigate={navigate} />
|
|
66
|
+
</ForgeThemeProvider>
|
|
67
|
+
</PublicAuthContext.Provider>
|
|
68
|
+
</ForgeClientProvider>
|
|
69
|
+
</QueryClientProvider>,
|
|
70
|
+
);
|
|
71
|
+
return { navigate };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
afterEach(() => {
|
|
75
|
+
cleanup();
|
|
76
|
+
if (realAdapter) axios.defaults.adapter = realAdapter;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const row = (overrides: Record<string, unknown>) => ({
|
|
80
|
+
id: "m-1",
|
|
81
|
+
tierId: "tier-1",
|
|
82
|
+
tierName: "Inner Circle",
|
|
83
|
+
status: "active",
|
|
84
|
+
cancelAtPeriodEnd: false,
|
|
85
|
+
currentPeriodEnd: inDays(20),
|
|
86
|
+
subscriptionAmount: 10,
|
|
87
|
+
subscriptionCurrency: "USD",
|
|
88
|
+
billingCycle: "month",
|
|
89
|
+
community: { id: "c-1", name: "Fan Club", slug: "fan-club" },
|
|
90
|
+
...overrides,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("AccountDashboard: My memberships", () => {
|
|
94
|
+
it("lists one card per community, from the memberships endpoint, not the legacy single membership", async () => {
|
|
95
|
+
mount([row({}), row({ id: "m-2", tierName: "Producers", community: { id: "c-2", name: "Producers Circle", slug: "producers" } })]);
|
|
96
|
+
|
|
97
|
+
const cards = await screen.findAllByTestId("my-membership");
|
|
98
|
+
expect(cards).toHaveLength(2);
|
|
99
|
+
expect(cards[0].textContent).toContain("Fan Club");
|
|
100
|
+
expect(cards[0].textContent).toContain("Inner Circle");
|
|
101
|
+
expect(cards[1].textContent).toContain("Producers Circle");
|
|
102
|
+
expect(document.body.textContent).not.toContain("LEGACY");
|
|
103
|
+
expect(requested).toContain("/public/memberships/mine");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("opens the membership's community home, never the account page", async () => {
|
|
107
|
+
const { navigate } = mount([row({})]);
|
|
108
|
+
const card = await screen.findByTestId("my-membership");
|
|
109
|
+
fireEvent.click(within(card).getByRole("button", { name: "Open community" }));
|
|
110
|
+
expect(navigate).toHaveBeenCalledWith("/i/communities/fan-club/home");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("says Renews on for a renewing membership and Ends on for a cancelled one", async () => {
|
|
114
|
+
const end = inDays(20);
|
|
115
|
+
mount([row({ currentPeriodEnd: end }), row({ id: "m-2", cancelAtPeriodEnd: true, currentPeriodEnd: end, community: { id: "c-2", name: "B", slug: "b" } })]);
|
|
116
|
+
const [renewing, ending] = await screen.findAllByTestId("my-membership");
|
|
117
|
+
expect(renewing.textContent).toContain(`Renews on ${fmt(end)}`);
|
|
118
|
+
expect(ending.textContent).toContain(`Ends on ${fmt(end)}`);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("shows the trial end instead of the renewal during a free trial", async () => {
|
|
122
|
+
const ends = inDays(10);
|
|
123
|
+
mount([row({ trialEndsAt: ends, currentPeriodEnd: ends })]);
|
|
124
|
+
expect((await screen.findByTestId("my-membership-trial")).textContent).toContain(`Free trial until ${fmt(ends)}`);
|
|
125
|
+
expect(document.body.textContent).not.toContain("Renews on");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("says so when the fan belongs to no community", async () => {
|
|
129
|
+
mount([]);
|
|
130
|
+
expect((await screen.findByTestId("my-memberships-empty")).textContent).toContain("not a member of any community");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("finishes a payment_failed application on its community's About page, on that tier", async () => {
|
|
134
|
+
mount(
|
|
135
|
+
[],
|
|
136
|
+
[
|
|
137
|
+
{
|
|
138
|
+
id: "app-1",
|
|
139
|
+
membershipTierId: "tier-9",
|
|
140
|
+
membershipTierName: "Gold",
|
|
141
|
+
communityId: "c-9",
|
|
142
|
+
status: "payment_failed",
|
|
143
|
+
createdAt: "2026-09-01T00:00:00.000Z",
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
[{ id: "c-9", name: "Gold Club", slug: "gold-club", memberCount: 1, tags: [] }],
|
|
147
|
+
);
|
|
148
|
+
const link = (await screen.findByRole("link", { name: /Complete/i })) as HTMLAnchorElement;
|
|
149
|
+
await vi.waitFor(() => expect(link.getAttribute("href")).toBe("/i/communities/gold-club?tier=tier-9"));
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -106,7 +106,7 @@ function render(opts: { rentals: MyFilmRental[] | null; tab?: AccountTabKey }):
|
|
|
106
106
|
<ForgeThemeProvider
|
|
107
107
|
theme={{ colors: { text: "#111111", background: "#ffffff", primary: "#6d28d9" }, cornerRadius: 8 }}
|
|
108
108
|
>
|
|
109
|
-
<AccountDashboard tab={opts.tab ?? "rentals"}
|
|
109
|
+
<AccountDashboard tab={opts.tab ?? "rentals"} />
|
|
110
110
|
</ForgeThemeProvider>
|
|
111
111
|
</PublicAuthContext.Provider>
|
|
112
112
|
</ForgeClientProvider>
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
3
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
4
|
+
import { ForgeClientProvider } from "../../../provider/ForgeProvider";
|
|
5
|
+
import { PublicAuthContext } from "../../../contexts/PublicAuthContext";
|
|
6
|
+
import { ForgeThemeProvider } from "../../theme/ForgeThemeProvider";
|
|
7
|
+
import type { NotificationMatrix } from "../../../data/queries/useNotifications";
|
|
8
|
+
import { AccountSection } from "../AccountDashboard";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The notification settings, drawn from the server's matrix.
|
|
12
|
+
*
|
|
13
|
+
* The panel this replaced sent `{ newPosts: false }`, which the server dropped,
|
|
14
|
+
* and read `newPosts` back, which the server never sends: every switch looked
|
|
15
|
+
* on and nothing ever saved. These pin the two things that made it wrong (the
|
|
16
|
+
* keys it reads and writes are the server's `"<type>.<channel>"` keys) and the
|
|
17
|
+
* rule that only a channel a type can use is offered at all.
|
|
18
|
+
*
|
|
19
|
+
* The matrix is SEEDED into the query cache under `useNotificationMatrix`'s own
|
|
20
|
+
* key, so a rename of that key fails here.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const PROFILE_ID = "profile-1";
|
|
24
|
+
|
|
25
|
+
const member = {
|
|
26
|
+
id: "account-1",
|
|
27
|
+
email: "fan@example.com",
|
|
28
|
+
firstName: "Fan",
|
|
29
|
+
lastName: "Person",
|
|
30
|
+
kind: "fan",
|
|
31
|
+
status: "active",
|
|
32
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
33
|
+
updatedAt: "2026-01-01T00:00:00.000Z",
|
|
34
|
+
membership: null,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// The server's defaults, in the shape GET /public/notifications/preferences returns.
|
|
38
|
+
const defaults: NotificationMatrix["defaults"] = {
|
|
39
|
+
dm: { inapp: true, push: true, email: true, emailMode: "immediate" },
|
|
40
|
+
chat_message: { inapp: false, push: true, email: false, emailMode: "none" },
|
|
41
|
+
mention: { inapp: true, push: true, email: true, emailMode: "immediate" },
|
|
42
|
+
reply: { inapp: true, push: true, email: true, emailMode: "digest" },
|
|
43
|
+
reaction: { inapp: true, push: false, email: true, emailMode: "digest" },
|
|
44
|
+
new_community_post: { inapp: true, push: false, email: true, emailMode: "digest" },
|
|
45
|
+
new_post: { inapp: true, push: true, email: true, emailMode: "digest" },
|
|
46
|
+
event_reminder: { inapp: true, push: true, email: false, emailMode: "none" },
|
|
47
|
+
stream_live: { inapp: true, push: true, email: false, emailMode: "none" },
|
|
48
|
+
mod: { inapp: true, push: false, email: true, emailMode: "digest" },
|
|
49
|
+
task_comment: { inapp: true, push: true, email: false, emailMode: "none" },
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function resolved(overrides: Record<string, boolean>): Record<string, boolean> {
|
|
53
|
+
const out: Record<string, boolean> = {};
|
|
54
|
+
for (const [type, def] of Object.entries(defaults)) {
|
|
55
|
+
for (const channel of ["inapp", "push", "email"] as const) {
|
|
56
|
+
const key = `${type}.${channel}`;
|
|
57
|
+
out[key] = key in overrides ? overrides[key]! : def[channel];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function render(overrides: Record<string, boolean> = {}, digestCadence: NotificationMatrix["digestCadence"] = "daily") {
|
|
64
|
+
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
65
|
+
const matrix: NotificationMatrix = {
|
|
66
|
+
id: "pref-1",
|
|
67
|
+
accountId: member.id,
|
|
68
|
+
profileId: PROFILE_ID,
|
|
69
|
+
digestCadence,
|
|
70
|
+
preferences: resolved(overrides),
|
|
71
|
+
overrides,
|
|
72
|
+
defaults,
|
|
73
|
+
};
|
|
74
|
+
queryClient.setQueryData(["public-notification-preferences", PROFILE_ID], matrix);
|
|
75
|
+
return renderToStaticMarkup(
|
|
76
|
+
<QueryClientProvider client={queryClient}>
|
|
77
|
+
<ForgeClientProvider apiUrl="http://api.test" profileId={PROFILE_ID}>
|
|
78
|
+
<PublicAuthContext.Provider
|
|
79
|
+
value={{ user: member, currencies: null, userSelectedCurrency: "USD", logout: () => {} } as never}
|
|
80
|
+
>
|
|
81
|
+
<ForgeThemeProvider
|
|
82
|
+
theme={{ colors: { text: "#111111", background: "#ffffff", primary: "#6d28d9" }, cornerRadius: 8 }}
|
|
83
|
+
>
|
|
84
|
+
<AccountSection section="notifications" />
|
|
85
|
+
</ForgeThemeProvider>
|
|
86
|
+
</PublicAuthContext.Provider>
|
|
87
|
+
</ForgeClientProvider>
|
|
88
|
+
</QueryClientProvider>,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const pressed = (html: string, key: string) => {
|
|
93
|
+
const match = html.match(new RegExp(`data-testid="notify-${key.replace(".", "\\.")}" aria-pressed="(true|false)"`));
|
|
94
|
+
return match ? match[1] === "true" : null;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
describe("Account notification settings", () => {
|
|
98
|
+
it("groups the member's notifications and leaves out work notifications, which never reach them", () => {
|
|
99
|
+
const html = render();
|
|
100
|
+
expect(html).toContain("Messages");
|
|
101
|
+
expect(html).toContain("Community");
|
|
102
|
+
expect(html).toContain("Events and live");
|
|
103
|
+
expect(html).toContain('data-testid="notify-row-dm"');
|
|
104
|
+
expect(html).not.toContain('data-testid="notify-row-task_comment"');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("offers only the channels a type can use", () => {
|
|
108
|
+
const html = render();
|
|
109
|
+
// A chat message writes no in-app row and never emails: push only.
|
|
110
|
+
expect(html).toContain('data-testid="notify-chat_message.push"');
|
|
111
|
+
expect(html).not.toContain('data-testid="notify-chat_message.inapp"');
|
|
112
|
+
expect(html).not.toContain('data-testid="notify-chat_message.email"');
|
|
113
|
+
// Reminders and "live now" never email.
|
|
114
|
+
expect(html).not.toContain('data-testid="notify-event_reminder.email"');
|
|
115
|
+
expect(html).not.toContain('data-testid="notify-stream_live.email"');
|
|
116
|
+
// A direct message can go anywhere.
|
|
117
|
+
for (const channel of ["inapp", "push", "email"]) {
|
|
118
|
+
expect(html).toContain(`data-testid="notify-dm.${channel}"`);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("shows the server's resolved state, including an opt-out the member saved", () => {
|
|
123
|
+
const html = render({ "new_post.email": false, "reaction.push": true });
|
|
124
|
+
expect(pressed(html, "new_post.email")).toBe(false);
|
|
125
|
+
expect(pressed(html, "new_post.push")).toBe(true);
|
|
126
|
+
// Off by default, on because the member turned it on.
|
|
127
|
+
expect(pressed(html, "reaction.push")).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("marks the member's email summary choice", () => {
|
|
131
|
+
const html = render({}, "hourly");
|
|
132
|
+
expect(html).toMatch(/data-testid="notify-digest-hourly"[^>]*aria-checked="true"|aria-checked="true"[^>]*data-testid="notify-digest-hourly"/);
|
|
133
|
+
expect(html).toMatch(/data-testid="notify-digest-daily"[^>]*aria-checked="false"|aria-checked="false"[^>]*data-testid="notify-digest-daily"/);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -129,7 +129,13 @@ describe("CourseCallsSection", () => {
|
|
|
129
129
|
id: "call-2",
|
|
130
130
|
title: "Week 0 orientation",
|
|
131
131
|
isOpen: false,
|
|
132
|
-
recording: {
|
|
132
|
+
recording: {
|
|
133
|
+
id: "rec-1",
|
|
134
|
+
status: "ready",
|
|
135
|
+
durationSeconds: 3600,
|
|
136
|
+
watchUrl: "https://cdn.test/rec.mp4",
|
|
137
|
+
transcript: null,
|
|
138
|
+
},
|
|
133
139
|
}),
|
|
134
140
|
);
|
|
135
141
|
const { getAllByText, container } = mount();
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
3
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
4
|
+
import { ForgeClientProvider } from "../../../provider/ForgeProvider";
|
|
5
|
+
import { PublicAuthContext } from "../../../contexts/PublicAuthContext";
|
|
6
|
+
import { ForgeThemeProvider } from "../../theme/ForgeThemeProvider";
|
|
7
|
+
import type { DownloadPageInfo } from "../../headless/downloads/useDownloadPage";
|
|
8
|
+
import { DownloadPage } from "../DownloadPage";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The download page, rendered through `react-dom/server` with the page read
|
|
12
|
+
* SEEDED into the query cache so the real `useDownloadPage` stays in the path.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const PROFILE_ID = "profile-1";
|
|
16
|
+
const TOKEN = "tok_abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG";
|
|
17
|
+
|
|
18
|
+
function render(info: DownloadPageInfo | undefined, user: { id: string } | null = null) {
|
|
19
|
+
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
20
|
+
if (info) queryClient.setQueryData(["download-page", PROFILE_ID, TOKEN, user?.id ?? null], info);
|
|
21
|
+
return renderToStaticMarkup(
|
|
22
|
+
<QueryClientProvider client={queryClient}>
|
|
23
|
+
<ForgeClientProvider apiUrl="http://api.test" profileId={PROFILE_ID}>
|
|
24
|
+
<PublicAuthContext.Provider value={{ user, currencies: null, userSelectedCurrency: "USD", logout: () => {} } as never}>
|
|
25
|
+
<ForgeThemeProvider theme={{ colors: { text: "#111111", background: "#ffffff", primary: "#6d28d9" }, cornerRadius: 8 }}>
|
|
26
|
+
<DownloadPage token={TOKEN} />
|
|
27
|
+
</ForgeThemeProvider>
|
|
28
|
+
</PublicAuthContext.Provider>
|
|
29
|
+
</ForgeClientProvider>
|
|
30
|
+
</QueryClientProvider>,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const info: DownloadPageInfo = {
|
|
35
|
+
items: [{ title: "Night Drive", quantity: 1, coverImage: "https://cdn.test/cover.jpg" }],
|
|
36
|
+
files: [
|
|
37
|
+
{ id: "archive", filename: "order-1.zip", size: 5 * 1024 * 1024 },
|
|
38
|
+
{ id: "file-0", filename: "Master.wav", size: 700 * 1024 * 1024 },
|
|
39
|
+
],
|
|
40
|
+
sentTo: "b***@example.com",
|
|
41
|
+
isGift: false,
|
|
42
|
+
signedInAsOwner: false,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
describe("DownloadPage", () => {
|
|
46
|
+
it("names what was bought and every file, locked, and asks for a code sent to the recipient", () => {
|
|
47
|
+
const html = render(info);
|
|
48
|
+
expect(html).toContain("Night Drive");
|
|
49
|
+
expect(html).toContain("order-1.zip");
|
|
50
|
+
expect(html).toContain("5.0 MB");
|
|
51
|
+
expect(html).toContain("Master.wav");
|
|
52
|
+
expect(html).toContain('data-testid="download-gate"');
|
|
53
|
+
expect(html).toContain("We'll email a 6-digit code to b***@example.com.");
|
|
54
|
+
expect(html).toContain("Email me a code");
|
|
55
|
+
// No download button and no address before the person proves who they are.
|
|
56
|
+
expect(html).not.toContain('data-testid="download-file-button"');
|
|
57
|
+
expect(html).not.toContain("http://api.test");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("offers signing in, and brings the person back to this page", () => {
|
|
61
|
+
const html = render(info);
|
|
62
|
+
expect(html).toContain(`href="/i/login?redirect=${encodeURIComponent(`/i/downloads/${TOKEN}`)}"`);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("says a gift is a gift", () => {
|
|
66
|
+
expect(render({ ...info, isGift: true })).toContain("A gift for you.");
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -8,6 +8,7 @@ import { PublicAuthContext } from "../../../contexts/PublicAuthContext";
|
|
|
8
8
|
import { ForgeThemeProvider } from "../../theme/ForgeThemeProvider";
|
|
9
9
|
import { Currency, type MembershipTier, type PublicAuthUser } from "../../../types/models";
|
|
10
10
|
import { MembershipCheckout } from "../MembershipCheckout";
|
|
11
|
+
import { InCommunity } from "../../../_tests/testCommunity";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Membership signup, end to end, over the REAL query/mutation layer.
|
|
@@ -66,6 +67,14 @@ const currencies = {
|
|
|
66
67
|
exchangeRates: { [Currency.USD]: { [Currency.NGN]: 1500 } },
|
|
67
68
|
};
|
|
68
69
|
|
|
70
|
+
/**
|
|
71
|
+
* The viewer's membership IN THIS COMMUNITY, as `GET /public/communities/:ref/me`
|
|
72
|
+
* reports it. Inside a community the checkout reads the current tier from
|
|
73
|
+
* here, never from `user.membership` (the newest membership on the whole
|
|
74
|
+
* profile, which can belong to another community).
|
|
75
|
+
*/
|
|
76
|
+
let viewerMembership: { tierId: string; status: string } | null = null;
|
|
77
|
+
|
|
69
78
|
function installAdapter(tiers: MembershipTier[]) {
|
|
70
79
|
sent = [];
|
|
71
80
|
const adapter: AxiosAdapter = async (config: InternalAxiosRequestConfig) => {
|
|
@@ -76,6 +85,15 @@ function installAdapter(tiers: MembershipTier[]) {
|
|
|
76
85
|
const reply = (data: unknown) =>
|
|
77
86
|
Promise.resolve({ data, status: 200, statusText: "OK", headers: {}, config } as never);
|
|
78
87
|
|
|
88
|
+
if (/\/public\/communities\/[^/]+\/me$/.test(url)) {
|
|
89
|
+
return reply({
|
|
90
|
+
state: viewerMembership ? "member" : "none",
|
|
91
|
+
membership: viewerMembership,
|
|
92
|
+
application: null,
|
|
93
|
+
upgradeTiers: [],
|
|
94
|
+
canInteract: true,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
79
97
|
if (url.includes("/public/membership-tiers")) return reply(tiers);
|
|
80
98
|
if (url.includes("/public/websites/site-config")) return reply({ currency: "USD" });
|
|
81
99
|
if (url.includes("/public/currencies")) return reply(currencies);
|
|
@@ -107,7 +125,13 @@ function installAdapter(tiers: MembershipTier[]) {
|
|
|
107
125
|
axios.defaults.adapter = adapter;
|
|
108
126
|
}
|
|
109
127
|
|
|
110
|
-
function mount(opts: {
|
|
128
|
+
function mount(opts: {
|
|
129
|
+
tiers: MembershipTier[];
|
|
130
|
+
user?: PublicAuthUser;
|
|
131
|
+
initialTierId?: string;
|
|
132
|
+
viewerMembership?: { tierId: string; status: string } | null;
|
|
133
|
+
}) {
|
|
134
|
+
viewerMembership = opts.viewerMembership ?? null;
|
|
111
135
|
installAdapter(opts.tiers);
|
|
112
136
|
// `"initialTierId" in opts` rather than `??`, so a test can ask for NO
|
|
113
137
|
// preselected tier (the grid) without the default quietly putting one back.
|
|
@@ -130,7 +154,9 @@ function mount(opts: { tiers: MembershipTier[]; user?: PublicAuthUser; initialTi
|
|
|
130
154
|
<ForgeThemeProvider
|
|
131
155
|
theme={{ colors: { text: "#111111", background: "#ffffff", primary: "#6d28d9" }, cornerRadius: 8 }}
|
|
132
156
|
>
|
|
133
|
-
<
|
|
157
|
+
<InCommunity>
|
|
158
|
+
<MembershipCheckout initialTierId={initialTierId} />
|
|
159
|
+
</InCommunity>
|
|
134
160
|
</ForgeThemeProvider>
|
|
135
161
|
</PublicAuthContext.Provider>
|
|
136
162
|
</ForgeClientProvider>
|
|
@@ -344,10 +370,9 @@ describe("billing cycles a tier actually sells", () => {
|
|
|
344
370
|
|
|
345
371
|
describe("the tier grid, when no tier was chosen up front", () => {
|
|
346
372
|
it("does not offer the tier the member is already on", async () => {
|
|
347
|
-
const onTier = { ...member, membership: { membershipTierId: "tier-1" } } as unknown as PublicAuthUser;
|
|
348
373
|
mount({
|
|
349
374
|
tiers: [tier({ id: "tier-1", name: "Inner Circle", priceMonthly: 10 }), tier({ id: "tier-2", name: "Backstage", priceMonthly: 20 })],
|
|
350
|
-
|
|
375
|
+
viewerMembership: { tierId: "tier-1", status: "active" },
|
|
351
376
|
initialTierId: undefined,
|
|
352
377
|
});
|
|
353
378
|
|
|
@@ -371,13 +396,31 @@ describe("the tier grid, when no tier was chosen up front", () => {
|
|
|
371
396
|
});
|
|
372
397
|
|
|
373
398
|
it("calls a change a change for a member with a live subscription", async () => {
|
|
374
|
-
|
|
399
|
+
mount({
|
|
400
|
+
tiers: [tier({ id: "tier-2", name: "Backstage", priceMonthly: 20 })],
|
|
401
|
+
viewerMembership: { tierId: "tier-1", status: "active" },
|
|
402
|
+
initialTierId: "tier-2",
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
expect(await screen.findByRole("button", { name: "Confirm change" })).toBeTruthy();
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it("REGRESSION: a membership in ANOTHER community does not turn a join into a change", async () => {
|
|
409
|
+
// `user.membership` is the newest membership on the profile. Here it lives
|
|
410
|
+
// in a different community, and this community's `/me` says: not a member.
|
|
411
|
+
const inOtherCommunity = {
|
|
375
412
|
...member,
|
|
376
|
-
membership: { membershipTierId: "tier-
|
|
413
|
+
membership: { membershipTierId: "tier-elsewhere", status: "active", paymentProviderSubscriptionId: "sub_other" },
|
|
377
414
|
} as unknown as PublicAuthUser;
|
|
378
|
-
mount({
|
|
415
|
+
mount({
|
|
416
|
+
tiers: [tier({ id: "tier-2", name: "Backstage", priceMonthly: 20 })],
|
|
417
|
+
user: inOtherCommunity,
|
|
418
|
+
viewerMembership: null,
|
|
419
|
+
initialTierId: "tier-2",
|
|
420
|
+
});
|
|
379
421
|
|
|
380
|
-
|
|
422
|
+
await waitFor(() => expect(sent.some((r) => /\/me$/.test(r.url))).toBe(true));
|
|
423
|
+
expect(screen.queryByRole("button", { name: "Confirm change" })).toBeNull();
|
|
381
424
|
});
|
|
382
425
|
});
|
|
383
426
|
|
|
@@ -452,7 +495,7 @@ describe("tier questionnaires", () => {
|
|
|
452
495
|
]);
|
|
453
496
|
});
|
|
454
497
|
|
|
455
|
-
it("after applying, links
|
|
498
|
+
it("after applying, links inside the community (where it shows pending), never to the account page", async () => {
|
|
456
499
|
mount({ tiers: [tier({ priceMonthly: 0, joinMode: "application", questionnaire: questions })] });
|
|
457
500
|
|
|
458
501
|
fireEvent.change(await screen.findByLabelText(/Why do you want to join\?/), { target: { value: "Big fan" } });
|
|
@@ -463,8 +506,8 @@ describe("tier questionnaires", () => {
|
|
|
463
506
|
expect(applicationPosts()[0].body).not.toHaveProperty("message");
|
|
464
507
|
expect(applicationPosts()[0].body?.questionnaire).toHaveLength(3);
|
|
465
508
|
expect(await screen.findByText("Application received")).toBeTruthy();
|
|
466
|
-
const link = screen.getByRole("link", { name: "Go to
|
|
467
|
-
expect(link.getAttribute("href")).toBe("/i/
|
|
509
|
+
const link = screen.getByRole("link", { name: "Go to the community" }) as HTMLAnchorElement;
|
|
510
|
+
expect(link.getAttribute("href")).toBe("/i/communities/fan-club/home");
|
|
468
511
|
expect(screen.queryByRole("link", { name: "View memberships" })).toBeNull();
|
|
469
512
|
});
|
|
470
513
|
});
|
|
@@ -11,7 +11,7 @@ import type { MembershipTier } from "../../../types/models";
|
|
|
11
11
|
/**
|
|
12
12
|
* What a fan sees after pressing Join while signed out.
|
|
13
13
|
*
|
|
14
|
-
* The auth guard sends them to `/i/login?redirect=/i/
|
|
14
|
+
* The auth guard sends them to `/i/login?redirect=/i/communities/<slug>?tier=…`,
|
|
15
15
|
* which up to now was a bare sign-in card: no mention of the tier they had just
|
|
16
16
|
* chosen, no sign that the wall was part of joining at all. The client app has
|
|
17
17
|
* always shown the tier on this screen, so this is code websites catching up.
|
|
@@ -121,7 +121,7 @@ describe("MembershipTiers: the card per join mode and application state", () =>
|
|
|
121
121
|
|
|
122
122
|
describe("the invite token reaches the tier list", () => {
|
|
123
123
|
it("forwards `?invite=` as inviteToken", async () => {
|
|
124
|
-
window.history.replaceState(null, "", "/i/
|
|
124
|
+
window.history.replaceState(null, "", "/i/communities/fan-club?invite=tok-123");
|
|
125
125
|
mount([tier("inv", { joinMode: "invite" })]);
|
|
126
126
|
await cardOf("Tier inv");
|
|
127
127
|
|