@tribe-nest/forge 3.58.0 → 3.60.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 +1 -1
- package/src/_tests/testCommunity.tsx +28 -0
- package/src/contexts/CommunityContext.tsx +192 -0
- package/src/data/queries/useChat.ts +42 -39
- package/src/data/queries/useCommunities.ts +343 -0
- package/src/data/queries/useCommunity.ts +93 -89
- package/src/data/queries/useMembership.ts +32 -7
- package/src/data/queries/useMembershipCalls.ts +116 -7
- package/src/data/queries/usePostCollections.ts +28 -16
- package/src/data/queries/usePosts.ts +16 -14
- package/src/data/queries/useSubscriptions.ts +22 -8
- package/src/i18n/de.json +145 -1
- package/src/i18n/en.json +145 -1
- package/src/index.ts +19 -7
- package/src/runtime/pages.spec.ts +31 -0
- package/src/runtime/pages.ts +21 -22
- package/src/server/_tests/buildWebManifest.spec.ts +1 -1
- package/src/server/_tests/forgeServiceWorkerScript.spec.ts +4 -4
- package/src/ui/format/_tests/membershipGate.spec.ts +6 -6
- package/src/ui/format/membershipGate.ts +19 -15
- package/src/ui/headless/_tests/communityGate.spec.tsx +80 -0
- package/src/ui/headless/_tests/communityHarness.tsx +205 -0
- package/src/ui/headless/_tests/communityProvider.spec.tsx +151 -0
- package/src/ui/headless/_tests/useCommunityInvite.spec.tsx +104 -0
- package/src/ui/headless/_tests/useCommunityJoin.spec.tsx +382 -0
- package/src/ui/headless/_tests/useCommunityMembership.spec.tsx +151 -0
- package/src/ui/headless/_tests/useCommunityReads.spec.tsx +286 -0
- package/src/ui/headless/_tests/useCommunityUpgrade.spec.tsx +157 -0
- package/src/ui/headless/auth/useSignupForm.ts +10 -2
- package/src/ui/headless/communities/CommunityGate.tsx +67 -0
- package/src/ui/headless/communities/index.ts +55 -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/useCommunityHome.ts +88 -0
- package/src/ui/headless/communities/useCommunityInvite.ts +116 -0
- package/src/ui/headless/communities/useCommunityJoin.ts +444 -0
- package/src/ui/headless/communities/useCommunityMembership.ts +136 -0
- package/src/ui/headless/communities/useCommunityUpgrade.ts +213 -0
- package/src/ui/headless/communities/useMyMemberships.ts +59 -0
- package/src/ui/headless/community/useCommunityAttachmentUpload.ts +7 -3
- package/src/ui/headless/index.ts +8 -4
- package/src/ui/headless/memberHome/index.ts +1 -1
- package/src/ui/headless/memberHome/useMemberHomeNav.ts +44 -42
- 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/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/index.ts +17 -12
- package/src/ui/styled/AccountDashboard.tsx +48 -256
- 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/Paywall.tsx +1 -1
- package/src/ui/styled/_tests/AccountDashboardBookingCall.spec.tsx +1 -1
- 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/CourseCallsSection.spec.tsx +7 -1
- 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/communities/CommunitiesList.tsx +88 -0
- package/src/ui/styled/communities/CommunityAbout.tsx +140 -0
- package/src/ui/styled/communities/CommunityHome.tsx +175 -0
- package/src/ui/styled/communities/CommunityJoinSheet.tsx +444 -0
- package/src/ui/styled/communities/CommunityMembershipMenu.tsx +133 -0
- package/src/ui/styled/communities/CommunityPending.tsx +49 -0
- package/src/ui/styled/communities/CommunityUpgradeSheet.tsx +137 -0
- package/src/ui/styled/communities/MyMembershipsList.tsx +89 -0
- package/src/ui/styled/communities/_tests/CommunityPages.spec.tsx +102 -0
- package/src/ui/styled/communities/index.ts +10 -0
- package/src/ui/styled/communities/shared.tsx +149 -0
- package/src/ui/styled/community/_tests/CommunityComposer.spec.tsx +4 -1
- package/src/ui/styled/community/_tests/CommunityPostDetail.spec.tsx +4 -1
- package/src/ui/styled/forge-utilities.css +19 -0
- package/src/ui/styled/members/CallTranscriptViewer.tsx +424 -0
- package/src/ui/styled/members/MemberCallsCalendar.tsx +142 -10
- package/src/ui/styled/members/MemberHomeLayout.tsx +35 -17
- package/src/ui/styled/members/MemberPostCard.tsx +9 -5
- 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/utils/_tests/safeRedirect.spec.ts +2 -2
- 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
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from "react";
|
|
2
|
-
import { usePublicAuth } from "../../../contexts/PublicAuthContext";
|
|
3
|
-
import { getMembershipAccess } from "../../../utils/membershipAccess";
|
|
4
|
-
|
|
5
|
-
export interface MembershipGateState {
|
|
6
|
-
hasAccess: boolean;
|
|
7
|
-
isAuthenticated: boolean;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface MembershipGateProps {
|
|
11
|
-
/**
|
|
12
|
-
* Tier ids that unlock the content. Omitted/empty → any active membership
|
|
13
|
-
* unlocks. (For per-item gates where the server already returns `hasAccess`,
|
|
14
|
-
* pass it via `access`.)
|
|
15
|
-
*/
|
|
16
|
-
tiers?: string[];
|
|
17
|
-
/** Override the access decision (e.g. server-provided `post.hasAccess`). */
|
|
18
|
-
access?: boolean;
|
|
19
|
-
children: ReactNode;
|
|
20
|
-
/** Rendered when locked — static node or a render-fn given the gate state. */
|
|
21
|
-
fallback?: ReactNode | ((state: MembershipGateState) => ReactNode);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Headless access gate. Renders children when the member has access, otherwise
|
|
26
|
-
* the fallback. Behavior (who has access) is fixed by the server/membership;
|
|
27
|
-
* the locked UI is entirely yours.
|
|
28
|
-
*/
|
|
29
|
-
export function MembershipGate({ tiers, access, children, fallback }: MembershipGateProps) {
|
|
30
|
-
const { user, isAuthenticated } = usePublicAuth();
|
|
31
|
-
const membership = user?.membership;
|
|
32
|
-
// The SERVER's access decision, not a `status === "active"` compare. That
|
|
33
|
-
// compare locked out members in `past_due`/`grace` — states the backend
|
|
34
|
-
// deliberately keeps access through — so a card that bounced cost them the
|
|
35
|
-
// content they were still paying for.
|
|
36
|
-
const membershipGrantsAccess = getMembershipAccess(membership).hasAccess;
|
|
37
|
-
const tierAllows = !tiers?.length || (membership ? tiers.includes(membership.membershipTierId) : false);
|
|
38
|
-
|
|
39
|
-
const hasAccess = access ?? (membershipGrantsAccess && tierAllows);
|
|
40
|
-
|
|
41
|
-
if (hasAccess) return <>{children}</>;
|
|
42
|
-
if (typeof fallback === "function") return <>{fallback({ hasAccess, isAuthenticated })}</>;
|
|
43
|
-
return <>{fallback ?? null}</>;
|
|
44
|
-
}
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } 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 { communityKeys, type CommunitySpace } from "../../../data/queries/useCommunity";
|
|
8
|
-
import { AccountDashboard } from "../AccountDashboard";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* The account page's way into the member area.
|
|
12
|
-
*
|
|
13
|
-
* Before this, a signed-in member landed on the membership card and the only
|
|
14
|
-
* doors on it led back to billing: change the plan, cancel it, fix the card.
|
|
15
|
-
* The member area they were paying for was reachable only by typing the URL.
|
|
16
|
-
*
|
|
17
|
-
* The first version gated the link on `/public/community/spaces` returning a
|
|
18
|
-
* space the caller could open, which hid it from precisely the fans who had not
|
|
19
|
-
* joined yet. Being signed in is now the whole gate: the member home shows the
|
|
20
|
-
* locked posts and the way to buy access, so it is never a dead end.
|
|
21
|
-
*
|
|
22
|
-
* Rendered through `react-dom/server`: no DOM, no effects, so the spaces query
|
|
23
|
-
* is SEEDED into the react-query cache rather than mocked, which keeps the real
|
|
24
|
-
* data path in place for the rest of the card.
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
const PROFILE_ID = "profile-1";
|
|
28
|
-
|
|
29
|
-
const space = (overrides: Partial<CommunitySpace> = {}): CommunitySpace => ({
|
|
30
|
-
id: "space-1",
|
|
31
|
-
profileId: PROFILE_ID,
|
|
32
|
-
name: "Backstage",
|
|
33
|
-
slug: "backstage",
|
|
34
|
-
description: null,
|
|
35
|
-
type: "feed",
|
|
36
|
-
courseId: null,
|
|
37
|
-
order: 0,
|
|
38
|
-
archivedAt: null,
|
|
39
|
-
createdAt: "2026-01-01T00:00:00.000Z",
|
|
40
|
-
tiers: [],
|
|
41
|
-
...overrides,
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
/** A signed-in member holding a paid tier that renews. */
|
|
45
|
-
const member = {
|
|
46
|
-
id: "account-1",
|
|
47
|
-
email: "fan@example.com",
|
|
48
|
-
firstName: "Fan",
|
|
49
|
-
lastName: "Person",
|
|
50
|
-
kind: "fan",
|
|
51
|
-
status: "active",
|
|
52
|
-
createdAt: "2026-01-01T00:00:00.000Z",
|
|
53
|
-
updatedAt: "2026-01-01T00:00:00.000Z",
|
|
54
|
-
membership: {
|
|
55
|
-
id: "membership-1",
|
|
56
|
-
membershipTierId: "tier-1",
|
|
57
|
-
endDate: "2026-12-01T00:00:00.000Z",
|
|
58
|
-
status: "active",
|
|
59
|
-
startDate: "2026-01-01T00:00:00.000Z",
|
|
60
|
-
subscriptionAmount: 10,
|
|
61
|
-
subscriptionCurrency: "USD",
|
|
62
|
-
billingCycle: "month",
|
|
63
|
-
membershipTier: { id: "tier-1", name: "Inner Circle", benefits: [] },
|
|
64
|
-
},
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
/** The same signed-in fan, with no membership of any kind. */
|
|
68
|
-
const nonMember = { ...member, membership: undefined };
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* The membership tab as a Forge code website draws it.
|
|
72
|
-
*
|
|
73
|
-
* `spaces === null` stands for the query never having answered, which is the
|
|
74
|
-
* state every first paint sits in.
|
|
75
|
-
*/
|
|
76
|
-
function render(opts: {
|
|
77
|
-
spaces: CommunitySpace[] | null;
|
|
78
|
-
user?: typeof member | typeof nonMember | null;
|
|
79
|
-
onNavigateCommunity?: () => void;
|
|
80
|
-
}): string {
|
|
81
|
-
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
82
|
-
if (opts.spaces) queryClient.setQueryData(communityKeys.spaces(PROFILE_ID), opts.spaces);
|
|
83
|
-
const user = opts.user === undefined ? member : opts.user;
|
|
84
|
-
|
|
85
|
-
return renderToStaticMarkup(
|
|
86
|
-
<QueryClientProvider client={queryClient}>
|
|
87
|
-
<ForgeClientProvider apiUrl="http://api.test" profileId={PROFILE_ID}>
|
|
88
|
-
<PublicAuthContext.Provider
|
|
89
|
-
value={{ user, currencies: null, userSelectedCurrency: "USD", logout: () => {} } as never}
|
|
90
|
-
>
|
|
91
|
-
<ForgeThemeProvider
|
|
92
|
-
theme={{ colors: { text: "#111111", background: "#ffffff", primary: "#6d28d9" }, cornerRadius: 8 }}
|
|
93
|
-
>
|
|
94
|
-
<AccountDashboard
|
|
95
|
-
tab="membership"
|
|
96
|
-
onNavigateMembership={() => {}}
|
|
97
|
-
onNavigateCommunity={opts.onNavigateCommunity}
|
|
98
|
-
/>
|
|
99
|
-
</ForgeThemeProvider>
|
|
100
|
-
</PublicAuthContext.Provider>
|
|
101
|
-
</ForgeClientProvider>
|
|
102
|
-
</QueryClientProvider>,
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
describe("AccountDashboard: the way into the member area", () => {
|
|
107
|
-
it("REGRESSION: offers the member area to a member who has a space to open", () => {
|
|
108
|
-
// The original gap this closes. Without it the membership card's only exits
|
|
109
|
-
// are Change and Cancel, and the member area is unreachable from here.
|
|
110
|
-
const html = render({ spaces: [space()], onNavigateCommunity: () => {} });
|
|
111
|
-
|
|
112
|
-
expect(html).toContain("Member Area");
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("REGRESSION: offers the member area to a signed-in fan with NO membership", () => {
|
|
116
|
-
// The second gap. The link used to require an openable space, so a fan who
|
|
117
|
-
// had not joined saw no way in, which is the fan the member home is most
|
|
118
|
-
// useful to: that page shows the locked posts and the offer to join.
|
|
119
|
-
const html = render({ spaces: [], user: nonMember, onNavigateCommunity: () => {} });
|
|
120
|
-
|
|
121
|
-
expect(html).toContain("Member Area");
|
|
122
|
-
// Still the non-member card and not some other state.
|
|
123
|
-
expect(html).toContain("You don't have an active membership.");
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it("REGRESSION: offers it before the spaces query has answered", () => {
|
|
127
|
-
// First paint has no cached answer. The link no longer waits on that read,
|
|
128
|
-
// so it does not appear a beat late.
|
|
129
|
-
const html = render({ spaces: null, onNavigateCommunity: () => {} });
|
|
130
|
-
|
|
131
|
-
expect(html).toContain("Member Area");
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it("draws nothing when the host site has no member-area route to send anyone to", () => {
|
|
135
|
-
// `onNavigateCommunity` is optional. A site that leaves it out gets no
|
|
136
|
-
// button rather than a dead one.
|
|
137
|
-
const html = render({ spaces: [space()] });
|
|
138
|
-
|
|
139
|
-
expect(html).not.toContain("Member Area");
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it("draws nothing for a visitor who is not signed in", () => {
|
|
143
|
-
// The member area is behind a login. Offering it to a signed-out visitor
|
|
144
|
-
// sends them at a guard, not at the content.
|
|
145
|
-
const html = render({ spaces: [space()], user: null, onNavigateCommunity: () => {} });
|
|
146
|
-
|
|
147
|
-
expect(html).not.toContain("Member Area");
|
|
148
|
-
});
|
|
149
|
-
});
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import { describe, it, expect, afterEach } from "vitest";
|
|
3
|
-
import { render, screen, fireEvent, cleanup } 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 during a free trial: the date the trial ends and what is
|
|
13
|
-
* charged after it replace "Renews on", and cancelling says access lasts to the
|
|
14
|
-
* trial end with nothing charged.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
const DAY = 24 * 60 * 60 * 1000;
|
|
18
|
-
const inDays = (n: number) => new Date(Date.now() + n * DAY).toISOString();
|
|
19
|
-
const fmt = (iso: string) =>
|
|
20
|
-
new Date(iso).toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" });
|
|
21
|
-
|
|
22
|
-
const memberWith = (membership: Record<string, unknown>) => ({
|
|
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
|
-
membership: {
|
|
32
|
-
id: "membership-1",
|
|
33
|
-
membershipTierId: "tier-1",
|
|
34
|
-
status: "active",
|
|
35
|
-
startDate: "2026-09-17T00:00:00.000Z",
|
|
36
|
-
subscriptionAmount: 10,
|
|
37
|
-
subscriptionCurrency: "USD",
|
|
38
|
-
billingCycle: "month",
|
|
39
|
-
membershipTier: { id: "tier-1", name: "Inner Circle", benefits: [] },
|
|
40
|
-
...membership,
|
|
41
|
-
},
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
let realAdapter: AxiosAdapter | undefined;
|
|
45
|
-
|
|
46
|
-
function mount(user: unknown) {
|
|
47
|
-
// Every request answers empty, so the tab's side queries never reach a network.
|
|
48
|
-
realAdapter = axios.defaults.adapter as AxiosAdapter | undefined;
|
|
49
|
-
axios.defaults.adapter = (async (config: InternalAxiosRequestConfig) =>
|
|
50
|
-
({ data: [], status: 200, statusText: "OK", headers: {}, config }) as never) as AxiosAdapter;
|
|
51
|
-
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
52
|
-
return render(
|
|
53
|
-
<QueryClientProvider client={queryClient}>
|
|
54
|
-
<ForgeClientProvider apiUrl="http://api.test" profileId="profile-1">
|
|
55
|
-
<PublicAuthContext.Provider
|
|
56
|
-
value={{ user, currencies: null, userSelectedCurrency: "USD", logout: () => {} } as never}
|
|
57
|
-
>
|
|
58
|
-
<ForgeThemeProvider theme={{ colors: { text: "#111111", background: "#ffffff", primary: "#6d28d9" }, cornerRadius: 8 }}>
|
|
59
|
-
<AccountDashboard tab="membership" onNavigateMembership={() => {}} />
|
|
60
|
-
</ForgeThemeProvider>
|
|
61
|
-
</PublicAuthContext.Provider>
|
|
62
|
-
</ForgeClientProvider>
|
|
63
|
-
</QueryClientProvider>,
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
afterEach(() => {
|
|
68
|
-
cleanup();
|
|
69
|
-
if (realAdapter) axios.defaults.adapter = realAdapter;
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
describe("AccountDashboard: a membership in its free trial", () => {
|
|
73
|
-
it("shows the trial end and what is charged after it, instead of Renews on", () => {
|
|
74
|
-
const ends = inDays(10);
|
|
75
|
-
mount(memberWith({ trialEndsAt: ends, endDate: ends }));
|
|
76
|
-
|
|
77
|
-
const state = screen.getByTestId("membership-trial-state");
|
|
78
|
-
expect(state.textContent).toContain(`Free trial until ${fmt(ends)}`);
|
|
79
|
-
expect(state.textContent).toMatch(new RegExp(`Then .*10.* per month from ${fmt(ends)}\\.`));
|
|
80
|
-
expect(document.body.textContent).not.toContain("Renews on");
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("says access lasts to the trial end with nothing charged when cancelling", () => {
|
|
84
|
-
const ends = inDays(10);
|
|
85
|
-
mount(memberWith({ trialEndsAt: ends, endDate: ends }));
|
|
86
|
-
|
|
87
|
-
fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
|
|
88
|
-
const dialog = screen.getByRole("alertdialog");
|
|
89
|
-
expect(dialog.textContent).toContain(
|
|
90
|
-
`Your free trial will not turn into a paid membership. You keep your benefits until ${fmt(ends)}, and nothing is charged.`,
|
|
91
|
-
);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("says nothing will be charged once a trial is cancelled", () => {
|
|
95
|
-
const ends = inDays(10);
|
|
96
|
-
mount(memberWith({ trialEndsAt: ends, endDate: ends, cancelAtPeriodEnd: true }));
|
|
97
|
-
expect(screen.getByTestId("membership-trial-state").textContent).toContain("Cancelled. Nothing will be charged.");
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("shows no trial line once the trial has ended", () => {
|
|
101
|
-
mount(memberWith({ trialEndsAt: inDays(-5), endDate: inDays(25) }));
|
|
102
|
-
expect(screen.queryByTestId("membership-trial-state")).toBeNull();
|
|
103
|
-
expect(document.body.textContent).toContain("Renews on");
|
|
104
|
-
});
|
|
105
|
-
});
|