@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
|
@@ -13,6 +13,7 @@ import { EventCountdown } from "./EventCountdown";
|
|
|
13
13
|
import { EventWaitlist } from "./EventWaitlist";
|
|
14
14
|
import { AddToCalendar } from "./AddToCalendar";
|
|
15
15
|
import { PresaleCodeField } from "./PresaleCode";
|
|
16
|
+
import { useMetaViewContent } from "../analytics/useMetaEvents";
|
|
16
17
|
|
|
17
18
|
export interface EventDetailProps {
|
|
18
19
|
/** Event slug (resolves the event). */
|
|
@@ -67,6 +68,15 @@ export function EventDetail({
|
|
|
67
68
|
// contains the tiers that code unlocked.
|
|
68
69
|
const presale = usePresaleCode(slug, { initialEvent });
|
|
69
70
|
const { event, isLoading } = presale;
|
|
71
|
+
useMetaViewContent(
|
|
72
|
+
event
|
|
73
|
+
? {
|
|
74
|
+
id: event.id,
|
|
75
|
+
name: event.title,
|
|
76
|
+
value: event.tickets?.length ? Math.min(...event.tickets.map((ticket) => Number(ticket.price))) : undefined,
|
|
77
|
+
}
|
|
78
|
+
: null,
|
|
79
|
+
);
|
|
70
80
|
|
|
71
81
|
if (isLoading) return <Loading fullPage />;
|
|
72
82
|
if (!event) return <p>{tr("forge.event_detail.not_found")}</p>;
|
|
@@ -16,6 +16,11 @@ export interface FilmRentalListProps {
|
|
|
16
16
|
basePath?: string;
|
|
17
17
|
/** SPA navigation adapter. Defaults to a full-page visit. */
|
|
18
18
|
navigate?: (href: string) => void;
|
|
19
|
+
/**
|
|
20
|
+
* `cards` (the default) boxes each rental. `rows` divides them with hairlines
|
|
21
|
+
* instead, the client portal's list style.
|
|
22
|
+
*/
|
|
23
|
+
layout?: "cards" | "rows";
|
|
19
24
|
className?: string;
|
|
20
25
|
style?: CSSProperties;
|
|
21
26
|
}
|
|
@@ -36,7 +41,7 @@ export type FilmLibraryProps = FilmRentalListProps;
|
|
|
36
41
|
* that already read it (the dashboard does, to decide whether to draw the tab at
|
|
37
42
|
* all) shares one request through react-query rather than making a second.
|
|
38
43
|
*/
|
|
39
|
-
export function FilmRentalList({ basePath = "/i/films", navigate, className, style }: FilmRentalListProps) {
|
|
44
|
+
export function FilmRentalList({ basePath = "/i/films", navigate, layout = "cards", className, style }: FilmRentalListProps) {
|
|
40
45
|
const t = useForgeT();
|
|
41
46
|
const tokens = useThemeTokens();
|
|
42
47
|
const { user, isInitialized } = usePublicAuth();
|
|
@@ -47,11 +52,12 @@ export function FilmRentalList({ basePath = "/i/films", navigate, className, sty
|
|
|
47
52
|
|
|
48
53
|
const rentals = data?.data ?? [];
|
|
49
54
|
|
|
55
|
+
const rows = layout === "rows";
|
|
50
56
|
const notice: CSSProperties = {
|
|
51
57
|
padding: "28px 20px",
|
|
52
58
|
borderRadius: tokens.cornerRadius,
|
|
53
|
-
border: `1px solid ${tokens.border}`,
|
|
54
|
-
background: tokens.surface,
|
|
59
|
+
border: `1px ${rows ? "dashed" : "solid"} ${tokens.border}`,
|
|
60
|
+
background: rows ? "transparent" : tokens.surface,
|
|
55
61
|
color: tokens.muted,
|
|
56
62
|
textAlign: "center",
|
|
57
63
|
fontSize: 14,
|
|
@@ -63,7 +69,8 @@ export function FilmRentalList({ basePath = "/i/films", navigate, className, sty
|
|
|
63
69
|
style={{
|
|
64
70
|
display: "flex",
|
|
65
71
|
flexDirection: "column",
|
|
66
|
-
gap: 16,
|
|
72
|
+
gap: rows ? 0 : 16,
|
|
73
|
+
...(rows && rentals.length > 0 ? { borderTop: `1px solid ${tokens.border}` } : {}),
|
|
67
74
|
color: tokens.text,
|
|
68
75
|
fontFamily: tokens.fontFamily,
|
|
69
76
|
...style,
|
|
@@ -92,7 +99,7 @@ export function FilmRentalList({ basePath = "/i/films", navigate, className, sty
|
|
|
92
99
|
)}
|
|
93
100
|
|
|
94
101
|
{rentals.map((rental) => (
|
|
95
|
-
<RentalRow key={rental.id} rental={rental} t={t} basePath={basePath} go={go} />
|
|
102
|
+
<RentalRow key={rental.id} rental={rental} t={t} basePath={basePath} go={go} rows={rows} />
|
|
96
103
|
))}
|
|
97
104
|
</div>
|
|
98
105
|
);
|
|
@@ -161,11 +168,13 @@ function RentalRow({
|
|
|
161
168
|
t,
|
|
162
169
|
basePath,
|
|
163
170
|
go,
|
|
171
|
+
rows,
|
|
164
172
|
}: {
|
|
165
173
|
rental: MyFilmRental;
|
|
166
174
|
t: ForgeT;
|
|
167
175
|
basePath: string;
|
|
168
176
|
go: (href: string) => void;
|
|
177
|
+
rows?: boolean;
|
|
169
178
|
}) {
|
|
170
179
|
const tokens = useThemeTokens();
|
|
171
180
|
const remaining = splitRemainingMs(rental.msRemaining);
|
|
@@ -195,10 +204,14 @@ function RentalRow({
|
|
|
195
204
|
gap: 14,
|
|
196
205
|
alignItems: "center",
|
|
197
206
|
flexWrap: "wrap",
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
207
|
+
...(rows
|
|
208
|
+
? { padding: "16px 0", borderBottom: `1px solid ${tokens.border}` }
|
|
209
|
+
: {
|
|
210
|
+
padding: 14,
|
|
211
|
+
borderRadius: tokens.cornerRadius,
|
|
212
|
+
border: `1px solid ${tokens.border}`,
|
|
213
|
+
background: tokens.surface,
|
|
214
|
+
}),
|
|
202
215
|
opacity: rental.isLive ? 1 : 0.85,
|
|
203
216
|
}}
|
|
204
217
|
>
|
|
@@ -16,6 +16,9 @@ import { formatTrialDate, tierTrialDays } from "../headless/membership/membershi
|
|
|
16
16
|
import { readableTextOn } from "../theme/contrast";
|
|
17
17
|
import { cycleFloor, cycleIsFree, offeredCycles, type BillingCycle } from "../format/membershipPwyw";
|
|
18
18
|
import type { MembershipTier } from "../../types/models";
|
|
19
|
+
import { useOptionalCommunityContext } from "../../contexts/CommunityContext";
|
|
20
|
+
import { useCommunityViewerState } from "../../data/queries/useCommunities";
|
|
21
|
+
import { communitiesListPath, communityAboutPath, communityHomePath } from "../headless/communities/paths";
|
|
19
22
|
import { Loading } from "./Loading";
|
|
20
23
|
import { PaystackPayButton } from "./PaystackPayButton";
|
|
21
24
|
|
|
@@ -25,23 +28,24 @@ export interface MembershipCheckoutProps {
|
|
|
25
28
|
/**
|
|
26
29
|
* Called when the subscription is finished in-app: a free activation, a tier
|
|
27
30
|
* change settled by proration, or a paid card payment that succeeded. The
|
|
28
|
-
* host navigates however it wants
|
|
29
|
-
*
|
|
31
|
+
* host navigates however it wants. Omit to fall back to Forge's default
|
|
32
|
+
* `window.location` redirect, which lands inside the community.
|
|
30
33
|
*
|
|
31
34
|
* `requiresConfirmation` is true only for a real provider payment, which is
|
|
32
35
|
* the only case the account page has anything to reconcile.
|
|
33
36
|
*/
|
|
34
37
|
onComplete?: (result: { requiresConfirmation: boolean }) => void;
|
|
35
38
|
/** The URL Stripe returns to for a PAID subscription (its `return_url`, and the
|
|
36
|
-
* free/paid fallback redirect). Default
|
|
39
|
+
* free/paid fallback redirect). Default: the community home with
|
|
40
|
+
* `confirmSubscription=1` (the community comes from `<CommunityProvider>`). */
|
|
37
41
|
successPath?: string;
|
|
38
|
-
/** Where the "View memberships" link points when
|
|
42
|
+
/** Where the "View memberships" link points when there are no tiers. Default: the community's About page. */
|
|
39
43
|
membershipsPath?: string;
|
|
40
44
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
45
|
+
* Where the link after applying points: the community home, which shows the
|
|
46
|
+
* pending application. Default: the surrounding community's home.
|
|
43
47
|
*/
|
|
44
|
-
|
|
48
|
+
homePath?: string;
|
|
45
49
|
/** Format a numeric amount for display. Defaults to the runtime currency formatter. */
|
|
46
50
|
formatAmount?: (amount: number) => string;
|
|
47
51
|
/**
|
|
@@ -83,9 +87,9 @@ export interface MembershipCheckoutProps {
|
|
|
83
87
|
export function MembershipCheckout({
|
|
84
88
|
initialTierId,
|
|
85
89
|
onComplete,
|
|
86
|
-
successPath
|
|
87
|
-
membershipsPath
|
|
88
|
-
|
|
90
|
+
successPath,
|
|
91
|
+
membershipsPath: membershipsPathProp,
|
|
92
|
+
homePath: homePathProp,
|
|
89
93
|
formatAmount,
|
|
90
94
|
inviteToken,
|
|
91
95
|
className,
|
|
@@ -140,12 +144,28 @@ export function MembershipCheckout({
|
|
|
140
144
|
const taxCaption = priceTaxCaption({ pricesIncludeTax: usePricesIncludeTax() });
|
|
141
145
|
const renderPayment = usePaymentRenderer();
|
|
142
146
|
|
|
147
|
+
const communityContext = useOptionalCommunityContext();
|
|
148
|
+
const slug = communityContext?.slug;
|
|
149
|
+
// Inside a community, the fan's membership IN IT (not `user.membership`, the
|
|
150
|
+
// newest one on the profile) decides "current plan" and join vs change.
|
|
151
|
+
const viewer = useCommunityViewerState(communityContext?.communityId ?? slug, {
|
|
152
|
+
enabled: !!(communityContext?.communityId ?? slug),
|
|
153
|
+
});
|
|
154
|
+
const current =
|
|
155
|
+
communityContext && viewer.data
|
|
156
|
+
? viewer.data.membership
|
|
157
|
+
? { tierId: viewer.data.membership.tierId, status: viewer.data.membership.status }
|
|
158
|
+
: null
|
|
159
|
+
: undefined;
|
|
160
|
+
const membershipsPath = membershipsPathProp ?? (slug ? communityAboutPath(slug) : communitiesListPath());
|
|
161
|
+
const homePath = homePathProp ?? (slug ? communityHomePath(slug) : communitiesListPath());
|
|
143
162
|
const checkout = useMembershipCheckout({
|
|
144
163
|
initialTierId,
|
|
145
164
|
returnPath: successPath,
|
|
146
165
|
freeReturnPath: successPath,
|
|
147
166
|
onComplete,
|
|
148
167
|
inviteToken,
|
|
168
|
+
current,
|
|
149
169
|
});
|
|
150
170
|
const inviteInfo = useMembershipInviteInfo(checkout.inviteToken);
|
|
151
171
|
|
|
@@ -269,16 +289,16 @@ export function MembershipCheckout({
|
|
|
269
289
|
: t(APPLICATION_OUTCOME_BODY[checkout.applicationOutcome], { tier_name: tier.name })}
|
|
270
290
|
</p>
|
|
271
291
|
</div>
|
|
272
|
-
{/* A lodged or approved application is followed
|
|
273
|
-
One that closed leaves the fan choosing
|
|
274
|
-
to the tiers. */}
|
|
292
|
+
{/* A lodged or approved application is followed inside the community,
|
|
293
|
+
which shows it pending. One that closed leaves the fan choosing
|
|
294
|
+
again, so that goes back to the tiers. */}
|
|
275
295
|
{checkout.applicationOutcome === "received" || checkout.applicationOutcome === "approved" ? (
|
|
276
296
|
<a
|
|
277
|
-
href={
|
|
278
|
-
data-testid="membership-application-
|
|
297
|
+
href={homePath}
|
|
298
|
+
data-testid="membership-application-home-link"
|
|
279
299
|
style={{ ...ghostButton, display: "block", textAlign: "center", textDecoration: "none", boxSizing: "border-box" }}
|
|
280
300
|
>
|
|
281
|
-
{t("forge.membership_checkout.
|
|
301
|
+
{t("forge.membership_checkout.go_to_community")}
|
|
282
302
|
</a>
|
|
283
303
|
) : (
|
|
284
304
|
<a
|
|
@@ -445,7 +465,7 @@ export function MembershipCheckout({
|
|
|
445
465
|
returnUrl:
|
|
446
466
|
checkout.setupPurpose === "application" && checkout.applicationReturnUrl
|
|
447
467
|
? checkout.applicationReturnUrl
|
|
448
|
-
: `${typeof window !== "undefined" ? window.location.origin : ""}${
|
|
468
|
+
: `${typeof window !== "undefined" ? window.location.origin : ""}${checkout.returnPath}`,
|
|
449
469
|
amount: checkout.amount,
|
|
450
470
|
currency: settlementCurrency,
|
|
451
471
|
mode: "manual",
|
|
@@ -4,6 +4,8 @@ import { useForgeT } from "../../i18n";
|
|
|
4
4
|
import { useThemeTokens } from "../theme/ForgeThemeProvider";
|
|
5
5
|
import { useAmountFormatter } from "../format/useFormatCurrency";
|
|
6
6
|
import { membershipPriceLabel } from "../format/membershipPriceLabel";
|
|
7
|
+
import { usePublicCommunities } from "../../data/queries/useCommunities";
|
|
8
|
+
import { communityAboutPath } from "../headless/communities/paths";
|
|
7
9
|
|
|
8
10
|
export interface MembershipTierCalloutProps {
|
|
9
11
|
/** The tier the visitor is on their way to join. Nothing is drawn without it. */
|
|
@@ -30,6 +32,9 @@ export interface MembershipTierCalloutProps {
|
|
|
30
32
|
* site remembers too. The client app has always shown this; code websites did
|
|
31
33
|
* not, so this is the same reassurance in a Forge component.
|
|
32
34
|
*
|
|
35
|
+
* A tier belongs to exactly one community, so the callout names it and links
|
|
36
|
+
* to its About page with the tier preselected, where the join happens in place.
|
|
37
|
+
*
|
|
33
38
|
* Renders NOTHING when there is no tier id, when the tiers have not loaded, or
|
|
34
39
|
* when the id matches nothing the artist sells. A form that pushes itself down
|
|
35
40
|
* the page a beat after it appeared is worse than one that never moved, and an
|
|
@@ -50,6 +55,9 @@ export function MembershipTierCallout({
|
|
|
50
55
|
const { data: tiers } = useGetMembershipTiers(inviteToken ? { inviteToken } : {});
|
|
51
56
|
const { data: invite } = useMembershipInviteInfo(inviteToken);
|
|
52
57
|
const tier = membershipTierId ? tiers?.find((candidate) => candidate.id === membershipTierId) : undefined;
|
|
58
|
+
const tierCommunityId = (tier as { communityId?: string } | undefined)?.communityId;
|
|
59
|
+
const { data: communities } = usePublicCommunities({ enabled: !!tierCommunityId });
|
|
60
|
+
const community = tierCommunityId ? communities?.find((candidate) => candidate.id === tierCommunityId) : undefined;
|
|
53
61
|
|
|
54
62
|
if (!tier) return null;
|
|
55
63
|
|
|
@@ -76,7 +84,18 @@ export function MembershipTierCallout({
|
|
|
76
84
|
<h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 8, fontFamily: tokens.headingFontFamily }}>
|
|
77
85
|
{t("forge.membership_tier_callout.join_title", { tier_name: tier.name })}
|
|
78
86
|
</h2>
|
|
79
|
-
<p style={{ fontSize: 14, marginBottom: shown.length || invite ? 12 : 0 }}>{membershipPriceLabel(tier, t, fmt)}</p>
|
|
87
|
+
<p style={{ fontSize: 14, marginBottom: shown.length || invite || community ? 12 : 0 }}>{membershipPriceLabel(tier, t, fmt)}</p>
|
|
88
|
+
{community && (
|
|
89
|
+
<p style={{ fontSize: 14, marginBottom: shown.length || invite ? 12 : 0 }}>
|
|
90
|
+
<a
|
|
91
|
+
href={communityAboutPath(community.slug, { tier: tier.id })}
|
|
92
|
+
data-testid="membership-tier-callout-community"
|
|
93
|
+
style={{ color: tokens.primary, fontWeight: 600 }}
|
|
94
|
+
>
|
|
95
|
+
{t("forge.membership_tier_callout.view_community", { community_name: community.name })}
|
|
96
|
+
</a>
|
|
97
|
+
</p>
|
|
98
|
+
)}
|
|
80
99
|
{invite && invite.membershipTierId === tier.id && (
|
|
81
100
|
<p style={{ fontSize: 14, fontWeight: 600, marginBottom: shown.length ? 12 : 0 }}>
|
|
82
101
|
{t("forge.membership_tier_callout.invite_for_email", { email: invite.email })}
|
|
@@ -14,6 +14,9 @@ import { usePricesIncludeTax } from "../../data/queries/useWebsite";
|
|
|
14
14
|
import { useForgeT } from "../../i18n";
|
|
15
15
|
import { PublicAuthContext } from "../../contexts/PublicAuthContext";
|
|
16
16
|
import { membershipPriceLabel } from "../format/membershipPriceLabel";
|
|
17
|
+
import { CommunityProvider, useOptionalCommunityContext } from "../../contexts/CommunityContext";
|
|
18
|
+
import { usePublicCommunities } from "../../data/queries/useCommunities";
|
|
19
|
+
import { communityAboutPath, communitiesListPath } from "../headless/communities/paths";
|
|
17
20
|
import { Button } from "./Button";
|
|
18
21
|
import { Loading } from "./Loading";
|
|
19
22
|
|
|
@@ -21,8 +24,16 @@ export interface MembershipTiersProps {
|
|
|
21
24
|
/** Format a numeric amount for display. Defaults to `$X.XX`. */
|
|
22
25
|
formatAmount?: (amount: number) => string;
|
|
23
26
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
27
|
+
* The community whose tiers to show (slug or id). Omitted, the surrounding
|
|
28
|
+
* `<CommunityProvider>` is used; outside one, every tier on the profile is
|
|
29
|
+
* listed and each links to its own community.
|
|
30
|
+
*/
|
|
31
|
+
community?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Override where "Select" sends the buyer; the tier id is appended as the
|
|
34
|
+
* `membershipTierId` query param. Default: the tier's community About page
|
|
35
|
+
* with the tier preselected (`/i/communities/<slug>?tier=<id>`), which opens
|
|
36
|
+
* the join sheet in place.
|
|
26
37
|
*/
|
|
27
38
|
checkoutPath?: string;
|
|
28
39
|
/**
|
|
@@ -39,7 +50,11 @@ export interface MembershipTiersProps {
|
|
|
39
50
|
}
|
|
40
51
|
|
|
41
52
|
/**
|
|
42
|
-
* Drop-in
|
|
53
|
+
* Drop-in tier cards. `community` scopes them to one community:
|
|
54
|
+
*
|
|
55
|
+
* <MembershipTiers community="fan-club" />
|
|
56
|
+
*
|
|
57
|
+
* Renders every membership tier as a centered,
|
|
43
58
|
* responsive card row (a single tier sits at a natural card width rather than
|
|
44
59
|
* stretching full-screen). "Select" routes to `checkoutPath` with the tier id,
|
|
45
60
|
* or calls `onSelect` when provided.
|
|
@@ -53,14 +68,25 @@ export interface MembershipTiersProps {
|
|
|
53
68
|
* "{days}-day free trial" badge, and an open tier's button reads "Start free
|
|
54
69
|
* trial". An application tier keeps "Apply to join" (approval starts the trial).
|
|
55
70
|
*/
|
|
56
|
-
export function MembershipTiers({
|
|
71
|
+
export function MembershipTiers({ community, ...props }: MembershipTiersProps) {
|
|
72
|
+
if (community) {
|
|
73
|
+
return (
|
|
74
|
+
<CommunityProvider community={community}>
|
|
75
|
+
<MembershipTierCards {...props} />
|
|
76
|
+
</CommunityProvider>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return <MembershipTierCards {...props} />;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function MembershipTierCards({
|
|
57
83
|
formatAmount,
|
|
58
|
-
checkoutPath
|
|
84
|
+
checkoutPath,
|
|
59
85
|
onSelect,
|
|
60
86
|
heading,
|
|
61
87
|
className,
|
|
62
88
|
style,
|
|
63
|
-
}: MembershipTiersProps) {
|
|
89
|
+
}: Omit<MembershipTiersProps, "community">) {
|
|
64
90
|
const tokens = useThemeTokens();
|
|
65
91
|
const t = useForgeT();
|
|
66
92
|
const fmt = useAmountFormatter(formatAmount);
|
|
@@ -68,6 +94,9 @@ export function MembershipTiers({
|
|
|
68
94
|
const pricesIncludeTax = usePricesIncludeTax();
|
|
69
95
|
const taxCaption = priceTaxCaption({ pricesIncludeTax });
|
|
70
96
|
const { data: tiers, isLoading } = useGetMembershipTiers();
|
|
97
|
+
const context = useOptionalCommunityContext();
|
|
98
|
+
// Only needed to link a tier to its community when there is no provider.
|
|
99
|
+
const { data: communities } = usePublicCommunities({ enabled: !context && !checkoutPath && !onSelect });
|
|
71
100
|
const withdraw = useWithdrawMembershipApplication();
|
|
72
101
|
const [withdrawError, setWithdrawError] = useState<string | null>(null);
|
|
73
102
|
// `null` still means "no heading"; anything else falls back to the default.
|
|
@@ -82,15 +111,24 @@ export function MembershipTiers({
|
|
|
82
111
|
// the same sentence this page does.
|
|
83
112
|
const priceLabel = (tier: MembershipTier) => membershipPriceLabel(tier, t, fmt);
|
|
84
113
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
114
|
+
/** A tier belongs to exactly one community; its About page is where it is joined. */
|
|
115
|
+
const tierHref = (tier: MembershipTier): string => {
|
|
116
|
+
const invite = readMembershipInviteFromUrl();
|
|
117
|
+
if (checkoutPath) {
|
|
88
118
|
const sep = checkoutPath.includes("?") ? "&" : "?";
|
|
89
|
-
|
|
90
|
-
window.location.href = `${checkoutPath}${sep}membershipTierId=${encodeURIComponent(tier.id)}${
|
|
119
|
+
return `${checkoutPath}${sep}membershipTierId=${encodeURIComponent(tier.id)}${
|
|
91
120
|
invite ? `&invite=${encodeURIComponent(invite)}` : ""
|
|
92
121
|
}`;
|
|
93
122
|
}
|
|
123
|
+
const tierCommunityId = (tier as MembershipTier & { communityId?: string }).communityId;
|
|
124
|
+
const slug =
|
|
125
|
+
context?.slug ?? communities?.find((candidate) => candidate.id === tierCommunityId)?.slug ?? undefined;
|
|
126
|
+
return slug ? communityAboutPath(slug, { tier: tier.id, ...(invite ? { invite } : {}) }) : communitiesListPath();
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const select = (tier: MembershipTier) => {
|
|
130
|
+
if (onSelect) return onSelect(tier);
|
|
131
|
+
if (typeof window !== "undefined") window.location.href = tierHref(tier);
|
|
94
132
|
};
|
|
95
133
|
|
|
96
134
|
return (
|
|
@@ -214,13 +214,13 @@ export function MusicLinkPage({
|
|
|
214
214
|
const destinations = useMemo(() => link?.destinations ?? [], [link]);
|
|
215
215
|
|
|
216
216
|
const onDestinationClick = (platform: string, url: string) => {
|
|
217
|
-
// ONE id, shared by the
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
217
|
+
// ONE id, the click's own, shared by the campaign pixel's Lead, its server
|
|
218
|
+
// copy and the first-party click. Meta deduplicates on (event_id,
|
|
219
|
+
// event_name), so without a shared id the same tap is counted as two
|
|
220
|
+
// conversions and the campaign optimises against inflated numbers.
|
|
221
221
|
const eventId = newEventId();
|
|
222
222
|
|
|
223
|
-
fire("Lead", { content_name: serviceMeta(platform).label, content_category: "music_link" });
|
|
223
|
+
fire("Lead", { content_name: serviceMeta(platform).label, content_category: "music_link" }, eventId);
|
|
224
224
|
|
|
225
225
|
trackBeacon("click", {
|
|
226
226
|
pathname: typeof window !== "undefined" ? window.location.pathname : undefined,
|
|
@@ -8,7 +8,7 @@ export interface PaywallProps {
|
|
|
8
8
|
onAction?: () => void;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
/** A themed "members only" card
|
|
11
|
+
/** A themed "members only" card. Pair with `<CommunityGate fallback={...}>`. */
|
|
12
12
|
export function Paywall({
|
|
13
13
|
title = "Members only",
|
|
14
14
|
message = "This content is available to members. Join to unlock it.",
|
|
@@ -16,6 +16,7 @@ import { usePricesIncludeTax } from "../../data/queries/useWebsite";
|
|
|
16
16
|
import { Button } from "./Button";
|
|
17
17
|
import { Loading } from "./Loading";
|
|
18
18
|
import { ReviewsSection } from "./ReviewsSection";
|
|
19
|
+
import { useMetaViewContent } from "../analytics/useMetaEvents";
|
|
19
20
|
|
|
20
21
|
export interface ProductDetailProps {
|
|
21
22
|
/** Product slug (resolves the product). */
|
|
@@ -68,6 +69,11 @@ export function ProductDetail({
|
|
|
68
69
|
const tr = useForgeT();
|
|
69
70
|
const { data: product, isLoading } = useGetProduct(slug, { initialData: initialProduct });
|
|
70
71
|
const fmt = useAmountFormatter(formatAmount);
|
|
72
|
+
useMetaViewContent(
|
|
73
|
+
product
|
|
74
|
+
? { id: product.id, name: product.title, value: lowestPrice(product.variants?.map((v) => v.price)) }
|
|
75
|
+
: null,
|
|
76
|
+
);
|
|
71
77
|
|
|
72
78
|
if (isLoading) return <Loading fullPage />;
|
|
73
79
|
if (!product) return <p style={{ color: t.text }}>{tr("forge.product_detail.not_found")}</p>;
|
|
@@ -101,6 +107,12 @@ export function ProductDetail({
|
|
|
101
107
|
);
|
|
102
108
|
}
|
|
103
109
|
|
|
110
|
+
/** The lowest numeric price, or undefined when none is a number. */
|
|
111
|
+
function lowestPrice(prices: (number | string | null | undefined)[] | undefined): number | undefined {
|
|
112
|
+
const numbers = (prices ?? []).map((p) => Number(p)).filter((n) => Number.isFinite(n));
|
|
113
|
+
return numbers.length ? Math.min(...numbers) : undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
104
116
|
// ── Rich description (responsive media) ───────────────────────────────────────
|
|
105
117
|
const RICH_CSS =
|
|
106
118
|
".forge-rich{overflow-wrap:break-word;line-height:1.6}" +
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEffect } from "react";
|
|
2
|
+
import { captureInstallPrompt } from "../../data/queries/usePWA";
|
|
2
3
|
|
|
3
4
|
export interface PwaRegistrationProps {
|
|
4
5
|
/**
|
|
@@ -17,7 +18,7 @@ export interface PwaRegistrationProps {
|
|
|
17
18
|
* Registers the Forge service worker (`/sw.js`) on the published site. Renders
|
|
18
19
|
* nothing. The SW source lives in `@tribe-nest/forge/server`
|
|
19
20
|
* (`forgeServiceWorkerScript()`) and is served by the starter's `/sw.js` route,
|
|
20
|
-
* so a Forge version bump propagates SW fixes to every deployed site
|
|
21
|
+
* so a Forge version bump propagates SW fixes to every deployed site, never
|
|
21
22
|
* frozen into per-site files.
|
|
22
23
|
*
|
|
23
24
|
* Gating (locked decision): registration is skipped unless `enabled` is true,
|
|
@@ -28,6 +29,9 @@ export interface PwaRegistrationProps {
|
|
|
28
29
|
export function PwaRegistration({ enabled = false, swUrl = "/sw.js" }: PwaRegistrationProps) {
|
|
29
30
|
useEffect(() => {
|
|
30
31
|
if (!enabled) return;
|
|
32
|
+
// Hold the browser's one install prompt from the first page, so an app
|
|
33
|
+
// prompt shown later (after joining, after a purchase) can still use it.
|
|
34
|
+
captureInstallPrompt();
|
|
31
35
|
if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) return;
|
|
32
36
|
let cancelled = false;
|
|
33
37
|
const register = () => {
|
|
@@ -18,13 +18,22 @@ export interface SignupFormProps {
|
|
|
18
18
|
/** The membership invite token riding on the same redirect, if any. See `MembershipTierCallout`. */
|
|
19
19
|
membershipInviteToken?: string;
|
|
20
20
|
couponCode?: string;
|
|
21
|
+
/** Start with this email filled in (a client-portal invite's address, for one). */
|
|
22
|
+
defaultEmail?: string;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
/** Themed account signup form, built on `useSignupForm`. */
|
|
24
|
-
export function SignupForm({
|
|
26
|
+
export function SignupForm({
|
|
27
|
+
onSuccess,
|
|
28
|
+
loginHref,
|
|
29
|
+
membershipTierId,
|
|
30
|
+
membershipInviteToken,
|
|
31
|
+
couponCode,
|
|
32
|
+
defaultEmail,
|
|
33
|
+
}: SignupFormProps) {
|
|
25
34
|
const t = useForgeT();
|
|
26
35
|
const theme = useForgeTheme();
|
|
27
|
-
const form = useSignupForm({ onSuccess, membershipTierId, couponCode });
|
|
36
|
+
const form = useSignupForm({ onSuccess, membershipTierId, couponCode, defaultEmail });
|
|
28
37
|
|
|
29
38
|
// One key for the whole sentence, split on its own `{email}` slot so the
|
|
30
39
|
// address keeps its <strong> without the sentence being assembled from two
|
|
@@ -104,6 +113,7 @@ export function SignupForm({ onSuccess, loginHref, membershipTierId, membershipI
|
|
|
104
113
|
value={form.code}
|
|
105
114
|
onChange={(e) => form.setCode(e.target.value)}
|
|
106
115
|
style={input}
|
|
116
|
+
data-testid="signup-code"
|
|
107
117
|
placeholder="123456"
|
|
108
118
|
/>,
|
|
109
119
|
)}
|
|
@@ -117,6 +127,7 @@ export function SignupForm({ onSuccess, loginHref, membershipTierId, membershipI
|
|
|
117
127
|
value={form.firstName}
|
|
118
128
|
onChange={(e) => form.setFirstName(e.target.value)}
|
|
119
129
|
style={input}
|
|
130
|
+
data-testid="signup-first-name"
|
|
120
131
|
placeholder={t("forge.signup_form.first_name_placeholder")}
|
|
121
132
|
/>,
|
|
122
133
|
)}
|
|
@@ -126,6 +137,7 @@ export function SignupForm({ onSuccess, loginHref, membershipTierId, membershipI
|
|
|
126
137
|
value={form.lastName}
|
|
127
138
|
onChange={(e) => form.setLastName(e.target.value)}
|
|
128
139
|
style={input}
|
|
140
|
+
data-testid="signup-last-name"
|
|
129
141
|
placeholder={t("forge.signup_form.last_name_placeholder")}
|
|
130
142
|
/>,
|
|
131
143
|
)}
|
|
@@ -137,6 +149,7 @@ export function SignupForm({ onSuccess, loginHref, membershipTierId, membershipI
|
|
|
137
149
|
value={form.email}
|
|
138
150
|
onChange={(e) => form.setEmail(e.target.value)}
|
|
139
151
|
style={input}
|
|
152
|
+
data-testid="signup-email"
|
|
140
153
|
placeholder={t("forge.signup_form.email_placeholder")}
|
|
141
154
|
/>,
|
|
142
155
|
)}
|
|
@@ -147,6 +160,7 @@ export function SignupForm({ onSuccess, loginHref, membershipTierId, membershipI
|
|
|
147
160
|
value={form.password}
|
|
148
161
|
onChange={(e) => form.setPassword(e.target.value)}
|
|
149
162
|
style={input}
|
|
163
|
+
data-testid="signup-password"
|
|
150
164
|
placeholder={t("forge.signup_form.password_placeholder")}
|
|
151
165
|
/>,
|
|
152
166
|
)}
|
|
@@ -155,13 +169,14 @@ export function SignupForm({ onSuccess, loginHref, membershipTierId, membershipI
|
|
|
155
169
|
type="checkbox"
|
|
156
170
|
checked={form.acceptedTerms}
|
|
157
171
|
onChange={(e) => form.setAcceptedTerms(e.target.checked)}
|
|
172
|
+
data-testid="signup-accept-terms"
|
|
158
173
|
style={{ accentColor: theme.colors.primary }}
|
|
159
174
|
/>
|
|
160
175
|
{t("forge.signup_form.accept_terms")}
|
|
161
176
|
</label>
|
|
162
177
|
</>
|
|
163
178
|
)}
|
|
164
|
-
<button type="submit" disabled={form.isSubmitting} style={button}>
|
|
179
|
+
<button type="submit" disabled={form.isSubmitting} style={button} data-testid="signup-submit">
|
|
165
180
|
{form.isSubmitting
|
|
166
181
|
? t("forge.signup_form.submitting")
|
|
167
182
|
: form.awaitingCode
|
|
@@ -90,11 +90,11 @@ const booking = (overrides: Partial<MyBooking> = {}): MyBooking => {
|
|
|
90
90
|
|
|
91
91
|
function render(rows: MyBooking[]): string {
|
|
92
92
|
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
page: 1,
|
|
97
|
-
|
|
93
|
+
// The list loads a page at a time (`useMyBookingsPaged`), so the cache holds
|
|
94
|
+
// infinite-query data: one page here.
|
|
95
|
+
queryClient.setQueryData(["my-bookings", ACCOUNT_ID, PROFILE_ID, "paged", 20], {
|
|
96
|
+
pages: [{ data: rows, total: rows.length, page: 1, limit: 20 }],
|
|
97
|
+
pageParams: [1],
|
|
98
98
|
});
|
|
99
99
|
|
|
100
100
|
return renderToStaticMarkup(
|
|
@@ -106,7 +106,7 @@ function render(rows: MyBooking[]): string {
|
|
|
106
106
|
<ForgeThemeProvider
|
|
107
107
|
theme={{ colors: { text: "#111111", background: "#ffffff", primary: "#6d28d9" }, cornerRadius: 8 }}
|
|
108
108
|
>
|
|
109
|
-
<AccountDashboard tab="bookings"
|
|
109
|
+
<AccountDashboard tab="bookings" />
|
|
110
110
|
</ForgeThemeProvider>
|
|
111
111
|
</PublicAuthContext.Provider>
|
|
112
112
|
</ForgeClientProvider>
|