@tribe-nest/forge 1.13.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 +42 -0
- package/src/client/createForgeClient.ts +54 -0
- package/src/content/ForgeContentProvider.tsx +118 -0
- package/src/content/ForgeEditBridge.tsx +134 -0
- package/src/content/editFlag.ts +23 -0
- package/src/content/index.ts +28 -0
- package/src/content/resolve.ts +67 -0
- package/src/content/types.ts +97 -0
- package/src/content/useField.ts +64 -0
- package/src/contexts/AppAuthContext.tsx +185 -0
- package/src/contexts/AudioPlayerContext.tsx +389 -0
- package/src/contexts/CartContext.tsx +131 -0
- package/src/contexts/PublicAuthContext.tsx +333 -0
- package/src/contexts/useAppAdminGuard.ts +56 -0
- package/src/css.d.ts +5 -0
- package/src/data/collectionParams.ts +57 -0
- package/src/data/queries/useAccount.ts +71 -0
- package/src/data/queries/useAccountSettings.ts +82 -0
- package/src/data/queries/useAnalytics.ts +86 -0
- package/src/data/queries/useAuthActions.ts +28 -0
- package/src/data/queries/useBlog.ts +103 -0
- package/src/data/queries/useBlogComments.ts +96 -0
- package/src/data/queries/useBroadcasts.ts +31 -0
- package/src/data/queries/useCertificates.ts +58 -0
- package/src/data/queries/useChat.ts +267 -0
- package/src/data/queries/useCoachingAvailability.ts +69 -0
- package/src/data/queries/useCoachingProducts.ts +67 -0
- package/src/data/queries/useCohortPage.ts +17 -0
- package/src/data/queries/useCollections.ts +211 -0
- package/src/data/queries/useCommunity.ts +794 -0
- package/src/data/queries/useCoupons.ts +20 -0
- package/src/data/queries/useCourseAccess.ts +50 -0
- package/src/data/queries/useCourses.ts +91 -0
- package/src/data/queries/useCurrencies.ts +33 -0
- package/src/data/queries/useDocuments.ts +208 -0
- package/src/data/queries/useDonations.ts +29 -0
- package/src/data/queries/useEngagement.ts +115 -0
- package/src/data/queries/useEvents.ts +69 -0
- package/src/data/queries/useFinalize.ts +109 -0
- package/src/data/queries/useForms.ts +31 -0
- package/src/data/queries/useInvoice.ts +65 -0
- package/src/data/queries/useLeadMagnet.ts +96 -0
- package/src/data/queries/useMembership.ts +20 -0
- package/src/data/queries/useNotifications.ts +267 -0
- package/src/data/queries/useOffers.ts +56 -0
- package/src/data/queries/useOrders.ts +71 -0
- package/src/data/queries/usePWA.ts +101 -0
- package/src/data/queries/usePageActions.ts +36 -0
- package/src/data/queries/usePaymentFlow.ts +80 -0
- package/src/data/queries/usePaymentLink.ts +47 -0
- package/src/data/queries/usePodcast.ts +85 -0
- package/src/data/queries/usePostCollections.ts +48 -0
- package/src/data/queries/usePosts.ts +94 -0
- package/src/data/queries/useProducts.ts +108 -0
- package/src/data/queries/useReplay.ts +19 -0
- package/src/data/queries/useReviews.ts +122 -0
- package/src/data/queries/useShipping.ts +109 -0
- package/src/data/queries/useSubscriptions.ts +66 -0
- package/src/data/queries/useWebsite.ts +128 -0
- package/src/data/queries/useWebsiteAgent.ts +100 -0
- package/src/index.ts +98 -0
- package/src/provider/ForgeAppProvider.tsx +149 -0
- package/src/provider/ForgeProvider.tsx +91 -0
- package/src/provider/SiteConfigProvider.tsx +27 -0
- package/src/server/_tests/buildPwaHead.spec.ts +182 -0
- package/src/server/_tests/buildWebManifest.spec.ts +313 -0
- package/src/server/_tests/forgeServiceWorkerScript.spec.ts +251 -0
- package/src/server/index.ts +260 -0
- package/src/server/jobs.ts +104 -0
- package/src/server/pwa.ts +332 -0
- package/src/types/index.ts +74 -0
- package/src/types/models.ts +1313 -0
- package/src/ui/analytics/ForgeAnalytics.tsx +208 -0
- package/src/ui/analytics/PageMetaPixel.tsx +92 -0
- package/src/ui/content/Editable.tsx +90 -0
- package/src/ui/format/PriceDisplay.tsx +142 -0
- package/src/ui/format/useFormatCurrency.ts +92 -0
- package/src/ui/headless/agent/useAiAgent.ts +106 -0
- package/src/ui/headless/auth/useLoginFlow.ts +72 -0
- package/src/ui/headless/auth/useSignupForm.ts +85 -0
- package/src/ui/headless/broadcast/useBroadcastWatch.ts +59 -0
- package/src/ui/headless/chat/attachmentKind.ts +27 -0
- package/src/ui/headless/chat/useChatAttachmentUpload.ts +132 -0
- package/src/ui/headless/chat/useChatRoom.ts +223 -0
- package/src/ui/headless/chat/useMessageThread.ts +60 -0
- package/src/ui/headless/checkout/useCheckout.ts +438 -0
- package/src/ui/headless/coaching/useCoachingBooking.ts +223 -0
- package/src/ui/headless/community/index.ts +20 -0
- package/src/ui/headless/community/lexical.ts +131 -0
- package/src/ui/headless/community/replyTree.ts +51 -0
- package/src/ui/headless/community/richLexical.ts +314 -0
- package/src/ui/headless/community/useCommunityAttachmentUpload.ts +137 -0
- package/src/ui/headless/community/useCommunitySpaceFeed.ts +122 -0
- package/src/ui/headless/community/useCommunityThread.ts +95 -0
- package/src/ui/headless/community/useMentionComposer.ts +105 -0
- package/src/ui/headless/community/useRichMentionComposer.ts +193 -0
- package/src/ui/headless/consent/useCookieConsent.ts +81 -0
- package/src/ui/headless/course/useCourseCheckout.ts +99 -0
- package/src/ui/headless/dialog.tsx +14 -0
- package/src/ui/headless/document/index.ts +7 -0
- package/src/ui/headless/document/useContractSignature.ts +68 -0
- package/src/ui/headless/document/useDocumentSigningFlow.ts +92 -0
- package/src/ui/headless/document/useOtpCodeInput.ts +86 -0
- package/src/ui/headless/document/useSignaturePad.ts +180 -0
- package/src/ui/headless/donation/Donation.tsx +115 -0
- package/src/ui/headless/donation/DonationContext.tsx +223 -0
- package/src/ui/headless/donation/fees.ts +15 -0
- package/src/ui/headless/donation/index.ts +10 -0
- package/src/ui/headless/engagement/usePostEngagement.ts +119 -0
- package/src/ui/headless/event/useEventCheckout.ts +141 -0
- package/src/ui/headless/forms/useContactForm.ts +58 -0
- package/src/ui/headless/forms/useEmailListForm.ts +75 -0
- package/src/ui/headless/forms/useSectionedForm.ts +256 -0
- package/src/ui/headless/index.ts +60 -0
- package/src/ui/headless/invoice/useInvoicePayment.ts +48 -0
- package/src/ui/headless/memberHome/index.ts +7 -0
- package/src/ui/headless/memberHome/useMemberHomeNav.ts +82 -0
- package/src/ui/headless/membership/MembershipGate.tsx +39 -0
- package/src/ui/headless/membership/useMembershipCheckout.ts +121 -0
- package/src/ui/headless/offer/Offer.tsx +88 -0
- package/src/ui/headless/offer/OfferContext.tsx +162 -0
- package/src/ui/headless/offer/index.ts +8 -0
- package/src/ui/headless/paymentLink/usePaymentLinkPayment.ts +46 -0
- package/src/ui/headless/podcast/usePodcastPlayer.ts +96 -0
- package/src/ui/headless/posts/index.ts +13 -0
- package/src/ui/headless/posts/upsell.ts +41 -0
- package/src/ui/headless/posts/useMemberPostsFeed.ts +110 -0
- package/src/ui/headless/posts/usePostCollectionDetail.ts +56 -0
- package/src/ui/headless/posts/usePostCollectionsList.ts +90 -0
- package/src/ui/headless/posts/usePostItem.ts +81 -0
- package/src/ui/headless/reviews/index.ts +6 -0
- package/src/ui/headless/reviews/useHelpfulVote.ts +45 -0
- package/src/ui/headless/reviews/useReviewForm.ts +108 -0
- package/src/ui/headless/reviews/useReviewsList.ts +79 -0
- package/src/ui/headless/work/index.ts +25 -0
- package/src/ui/headless/work/useWorkPortal.ts +349 -0
- package/src/ui/index.ts +144 -0
- package/src/ui/payment/ForgePaymentProvider.tsx +45 -0
- package/src/ui/payment/ForgeStripePayment.tsx +171 -0
- package/src/ui/shell/TribeNestApp.tsx +121 -0
- package/src/ui/shell/shellGating.spec.ts +28 -0
- package/src/ui/shell/shellGating.ts +11 -0
- package/src/ui/styled/AccountDashboard.tsx +575 -0
- package/src/ui/styled/AiAgentWidget.tsx +152 -0
- package/src/ui/styled/AudioPlayer.tsx +123 -0
- package/src/ui/styled/BlogCategory.tsx +79 -0
- package/src/ui/styled/BlogComments.tsx +236 -0
- package/src/ui/styled/BlogList.tsx +101 -0
- package/src/ui/styled/BlogPost.tsx +160 -0
- package/src/ui/styled/BlogPostFooter.tsx +33 -0
- package/src/ui/styled/BotProtection.tsx +101 -0
- package/src/ui/styled/Button.tsx +88 -0
- package/src/ui/styled/Cart.tsx +276 -0
- package/src/ui/styled/ChatRoom.tsx +252 -0
- package/src/ui/styled/Checkout.tsx +988 -0
- package/src/ui/styled/CheckoutConfirmation.tsx +452 -0
- package/src/ui/styled/CoachingBooking.tsx +605 -0
- package/src/ui/styled/CoachingConfirmation.tsx +151 -0
- package/src/ui/styled/CoachingDetail.tsx +99 -0
- package/src/ui/styled/CohortPage.tsx +82 -0
- package/src/ui/styled/ConfirmSubscription.tsx +134 -0
- package/src/ui/styled/Confirmation.tsx +171 -0
- package/src/ui/styled/ContactForm.tsx +81 -0
- package/src/ui/styled/CookieConsent.tsx +239 -0
- package/src/ui/styled/CourseAccess.tsx +169 -0
- package/src/ui/styled/CourseCheckout.tsx +425 -0
- package/src/ui/styled/CourseConfirmation.tsx +191 -0
- package/src/ui/styled/CourseDetail.tsx +140 -0
- package/src/ui/styled/CoursesGrid.tsx +85 -0
- package/src/ui/styled/CurrencySwitcher.tsx +51 -0
- package/src/ui/styled/DonationButton.tsx +247 -0
- package/src/ui/styled/DonationPage.tsx +29 -0
- package/src/ui/styled/EmailListForm.tsx +88 -0
- package/src/ui/styled/EventConfirmation.tsx +228 -0
- package/src/ui/styled/EventCountdown.tsx +98 -0
- package/src/ui/styled/EventDetail.tsx +141 -0
- package/src/ui/styled/EventTickets.tsx +548 -0
- package/src/ui/styled/EventsList.tsx +83 -0
- package/src/ui/styled/ForgotPasswordForm.tsx +105 -0
- package/src/ui/styled/FormRenderer.tsx +249 -0
- package/src/ui/styled/InstallBanner.tsx +114 -0
- package/src/ui/styled/InvoiceConfirmation.tsx +50 -0
- package/src/ui/styled/InvoicePayment.tsx +141 -0
- package/src/ui/styled/LeadMagnet.tsx +93 -0
- package/src/ui/styled/Loading.tsx +56 -0
- package/src/ui/styled/LoginForm.tsx +134 -0
- package/src/ui/styled/Markdown.tsx +103 -0
- package/src/ui/styled/MembershipCheckout.tsx +197 -0
- package/src/ui/styled/MembershipTiers.tsx +115 -0
- package/src/ui/styled/OfferButton.tsx +181 -0
- package/src/ui/styled/PageActions.tsx +124 -0
- package/src/ui/styled/PaymentLinkConfirmation.tsx +50 -0
- package/src/ui/styled/PaymentLinkPayment.tsx +85 -0
- package/src/ui/styled/Paywall.tsx +54 -0
- package/src/ui/styled/PlayButton.tsx +48 -0
- package/src/ui/styled/PodcastEpisode.tsx +137 -0
- package/src/ui/styled/PodcastList.tsx +58 -0
- package/src/ui/styled/PodcastShow.tsx +164 -0
- package/src/ui/styled/PostsFeed.tsx +63 -0
- package/src/ui/styled/ProductDetail.tsx +616 -0
- package/src/ui/styled/ProductGrid.tsx +82 -0
- package/src/ui/styled/PushOptIn.tsx +65 -0
- package/src/ui/styled/PwaRegistration.tsx +58 -0
- package/src/ui/styled/ReactionBar.tsx +93 -0
- package/src/ui/styled/ReplayList.tsx +134 -0
- package/src/ui/styled/ResetPasswordForm.tsx +127 -0
- package/src/ui/styled/ReviewForm.tsx +292 -0
- package/src/ui/styled/ReviewsSection.tsx +260 -0
- package/src/ui/styled/SectionedFormRenderer.tsx +146 -0
- package/src/ui/styled/SignupForm.tsx +109 -0
- package/src/ui/styled/UserMenu.tsx +166 -0
- package/src/ui/styled/chat/ChatAttachments.tsx +172 -0
- package/src/ui/styled/chat/ChatComposer.tsx +153 -0
- package/src/ui/styled/chat/ChatThreadPanel.tsx +94 -0
- package/src/ui/styled/chat/MessageBubble.tsx +66 -0
- package/src/ui/styled/chat/NewDmDialog.tsx +78 -0
- package/src/ui/styled/community/CommunityComposer.tsx +358 -0
- package/src/ui/styled/community/CommunityFeed.tsx +483 -0
- package/src/ui/styled/community/CommunityLeaderboard.tsx +109 -0
- package/src/ui/styled/community/CommunityMediaGallery.tsx +116 -0
- package/src/ui/styled/community/CommunityMemberProfile.tsx +285 -0
- package/src/ui/styled/community/CommunityNotifications.tsx +222 -0
- package/src/ui/styled/community/CommunityPostCard.tsx +165 -0
- package/src/ui/styled/community/CommunityPostDetail.tsx +337 -0
- package/src/ui/styled/community/CommunityReactionBar.tsx +107 -0
- package/src/ui/styled/community/CommunityRichText.tsx +157 -0
- package/src/ui/styled/community/CommunitySpaces.tsx +174 -0
- package/src/ui/styled/community/MemberAvatar.tsx +74 -0
- package/src/ui/styled/community/index.ts +23 -0
- package/src/ui/styled/community/util.ts +15 -0
- package/src/ui/styled/members/MemberHomeLayout.tsx +98 -0
- package/src/ui/styled/members/MemberPostCard.tsx +392 -0
- package/src/ui/styled/members/MemberPostsFeed.tsx +219 -0
- package/src/ui/styled/members/PostCollectionDetail.tsx +227 -0
- package/src/ui/styled/members/PostCollectionsGrid.tsx +287 -0
- package/src/ui/styled/members/index.ts +7 -0
- package/src/ui/styled/rich-text.css +150 -0
- package/src/ui/styled/work/WorkInviteAccept.tsx +222 -0
- package/src/ui/styled/work/WorkPortalProjects.tsx +163 -0
- package/src/ui/styled/work/WorkProjectInvoices.tsx +113 -0
- package/src/ui/styled/work/WorkProjectReport.tsx +66 -0
- package/src/ui/styled/work/WorkReportBody.tsx +327 -0
- package/src/ui/styled/work/WorkTaskAttachments.tsx +67 -0
- package/src/ui/styled/work/WorkTaskComments.tsx +151 -0
- package/src/ui/styled/work/WorkTaskDetail.tsx +92 -0
- package/src/ui/styled/work/WorkTokenReport.tsx +49 -0
- package/src/ui/styled/work/index.ts +10 -0
- package/src/ui/theme/ForgeThemeProvider.tsx +121 -0
- package/src/ui/theme/contrast.ts +22 -0
- package/src/utils/analyticsBus.ts +31 -0
- package/src/utils/attribution.ts +68 -0
- package/src/utils/cookieConsent.ts +83 -0
- package/src/utils/landing.ts +153 -0
- package/src/utils/metaPixel.ts +122 -0
- package/src/utils/structuredData.ts +126 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { useLeadMagnetConfirm, useTrackLeadMagnetDownload } from "../../data/queries/useLeadMagnet";
|
|
3
|
+
import { useForgeTheme } from "../theme/ForgeThemeProvider";
|
|
4
|
+
import { Loading } from "./Loading";
|
|
5
|
+
|
|
6
|
+
export interface LeadMagnetProps {
|
|
7
|
+
/** Lead magnet id (route param). */
|
|
8
|
+
leadMagnetId: string;
|
|
9
|
+
/** Subscriber id from the delivery link (URL search param). */
|
|
10
|
+
subscriberId?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Lead magnet delivery / download screen. Built on `useLeadMagnetConfirm` + `useTrackLeadMagnetDownload`. */
|
|
14
|
+
export function LeadMagnet({ leadMagnetId, subscriberId }: LeadMagnetProps) {
|
|
15
|
+
const theme = useForgeTheme();
|
|
16
|
+
const trackDownload = useTrackLeadMagnetDownload();
|
|
17
|
+
const [isDownloading, setIsDownloading] = useState(false);
|
|
18
|
+
|
|
19
|
+
const { data, isLoading, isError } = useLeadMagnetConfirm(leadMagnetId, subscriberId);
|
|
20
|
+
const leadMagnet = data?.leadMagnet;
|
|
21
|
+
const profile = data?.profile;
|
|
22
|
+
|
|
23
|
+
const state: "loading" | "success" | "error" =
|
|
24
|
+
!leadMagnetId || !subscriberId || isError ? "error" : isLoading ? "loading" : "success";
|
|
25
|
+
|
|
26
|
+
const handleDownload = async () => {
|
|
27
|
+
if (!leadMagnet) return;
|
|
28
|
+
setIsDownloading(true);
|
|
29
|
+
try {
|
|
30
|
+
await trackDownload.mutateAsync({ leadMagnetId, subscriberId });
|
|
31
|
+
const fileUrl = leadMagnet.media?.[0]?.url;
|
|
32
|
+
if (fileUrl && typeof document !== "undefined") {
|
|
33
|
+
const link = document.createElement("a");
|
|
34
|
+
link.href = fileUrl;
|
|
35
|
+
link.download = leadMagnet.media?.[0]?.filename || "download";
|
|
36
|
+
link.target = "_blank";
|
|
37
|
+
document.body.appendChild(link);
|
|
38
|
+
link.click();
|
|
39
|
+
document.body.removeChild(link);
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
// download tracking failed but still allow download
|
|
43
|
+
} finally {
|
|
44
|
+
setIsDownloading(false);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const wrap: React.CSSProperties = {
|
|
49
|
+
maxWidth: 480,
|
|
50
|
+
margin: "0 auto",
|
|
51
|
+
textAlign: "center",
|
|
52
|
+
color: theme.colors.text,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (state === "loading") return <Loading fullPage />;
|
|
56
|
+
|
|
57
|
+
if (state === "error") {
|
|
58
|
+
return (
|
|
59
|
+
<div style={wrap}>
|
|
60
|
+
<h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 12 }}>Oops!</h1>
|
|
61
|
+
<p>{!leadMagnetId || !subscriberId ? "Invalid link" : "This link is invalid or has expired."}</p>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div style={wrap}>
|
|
68
|
+
{profile && <p style={{ fontSize: 14, opacity: 0.7, marginBottom: 8 }}>{profile.name}</p>}
|
|
69
|
+
<h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 12 }}>
|
|
70
|
+
{leadMagnet?.thankYouMessage || "Thank you for subscribing!"}
|
|
71
|
+
</h1>
|
|
72
|
+
<p style={{ opacity: 0.8, marginBottom: 32 }}>
|
|
73
|
+
Your download is ready. Click the button below to get your file.
|
|
74
|
+
</p>
|
|
75
|
+
<button
|
|
76
|
+
onClick={handleDownload}
|
|
77
|
+
disabled={isDownloading}
|
|
78
|
+
style={{
|
|
79
|
+
padding: "12px 32px",
|
|
80
|
+
borderRadius: theme.cornerRadius,
|
|
81
|
+
border: "none",
|
|
82
|
+
fontWeight: 600,
|
|
83
|
+
cursor: isDownloading ? "default" : "pointer",
|
|
84
|
+
opacity: isDownloading ? 0.5 : 1,
|
|
85
|
+
background: theme.colors.primary,
|
|
86
|
+
color: theme.colors.background,
|
|
87
|
+
}}
|
|
88
|
+
>
|
|
89
|
+
{isDownloading ? "Preparing…" : "Download Now"}
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useForgeTheme } from "../theme/ForgeThemeProvider";
|
|
2
|
+
|
|
3
|
+
export interface LoadingProps {
|
|
4
|
+
/** Spinner diameter in px. */
|
|
5
|
+
size?: number;
|
|
6
|
+
/** Optional caption under the spinner. */
|
|
7
|
+
label?: string;
|
|
8
|
+
/** Center it in a tall block (for full-page/route loading states). */
|
|
9
|
+
fullPage?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const alpha = (hex: string, a: number) => hex + Math.round(a * 255).toString(16).padStart(2, "0");
|
|
13
|
+
|
|
14
|
+
/** Themed loading spinner — drop-in from `@tribe-nest/forge/ui`. */
|
|
15
|
+
export function Loading({ size = 28, label, fullPage }: LoadingProps) {
|
|
16
|
+
const theme = useForgeTheme();
|
|
17
|
+
const border = Math.max(2, Math.round(size / 9));
|
|
18
|
+
|
|
19
|
+
const content = (
|
|
20
|
+
<div
|
|
21
|
+
role="status"
|
|
22
|
+
aria-live="polite"
|
|
23
|
+
aria-busy="true"
|
|
24
|
+
style={{
|
|
25
|
+
display: "flex",
|
|
26
|
+
flexDirection: "column",
|
|
27
|
+
alignItems: "center",
|
|
28
|
+
justifyContent: "center",
|
|
29
|
+
gap: 12,
|
|
30
|
+
color: theme.colors.text,
|
|
31
|
+
fontFamily: theme.fontFamily,
|
|
32
|
+
}}
|
|
33
|
+
>
|
|
34
|
+
<style>{"@keyframes forge-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.forge-spin{animation-duration:1.6s}}"}</style>
|
|
35
|
+
<span
|
|
36
|
+
className="forge-spin"
|
|
37
|
+
style={{
|
|
38
|
+
display: "inline-block",
|
|
39
|
+
width: size,
|
|
40
|
+
height: size,
|
|
41
|
+
borderRadius: "50%",
|
|
42
|
+
border: `${border}px solid ${alpha(theme.colors.primary, 0.2)}`,
|
|
43
|
+
borderTopColor: theme.colors.primary,
|
|
44
|
+
animation: "forge-spin .7s linear infinite",
|
|
45
|
+
}}
|
|
46
|
+
/>
|
|
47
|
+
{label && <span style={{ fontSize: 14, opacity: 0.7 }}>{label}</span>}
|
|
48
|
+
{!label && <span style={{ position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" }}>Loading</span>}
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
if (fullPage) {
|
|
53
|
+
return <div style={{ minHeight: "40vh", display: "grid", placeItems: "center" }}>{content}</div>;
|
|
54
|
+
}
|
|
55
|
+
return content;
|
|
56
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { useLoginFlow } from "../headless/auth/useLoginFlow";
|
|
2
|
+
import { useForgeTheme } from "../theme/ForgeThemeProvider";
|
|
3
|
+
|
|
4
|
+
export interface LoginFormProps {
|
|
5
|
+
/** Called after a successful login. The shell typically redirects here. */
|
|
6
|
+
onSuccess?: (smartLinkPath?: string) => void;
|
|
7
|
+
/** Href for the "Sign up" link. */
|
|
8
|
+
signupHref: string;
|
|
9
|
+
/** Href for the "Forgot password?" link. */
|
|
10
|
+
forgotHref: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Themed 2FA login form (credentials → emailed code), built on `useLoginFlow`. */
|
|
14
|
+
export function LoginForm({ onSuccess, signupHref, forgotHref }: LoginFormProps) {
|
|
15
|
+
const theme = useForgeTheme();
|
|
16
|
+
const flow = useLoginFlow({ onSuccess });
|
|
17
|
+
|
|
18
|
+
const card: React.CSSProperties = {
|
|
19
|
+
maxWidth: 420,
|
|
20
|
+
margin: "40px auto 0",
|
|
21
|
+
padding: 24,
|
|
22
|
+
border: `1px solid ${theme.colors.primary}40`,
|
|
23
|
+
borderRadius: theme.cornerRadius,
|
|
24
|
+
color: theme.colors.text,
|
|
25
|
+
};
|
|
26
|
+
const input: React.CSSProperties = {
|
|
27
|
+
width: "100%",
|
|
28
|
+
height: 40,
|
|
29
|
+
padding: "8px 12px",
|
|
30
|
+
borderRadius: theme.cornerRadius,
|
|
31
|
+
border: `1px solid ${theme.colors.primary}40`,
|
|
32
|
+
background: "transparent",
|
|
33
|
+
color: theme.colors.text,
|
|
34
|
+
outline: "none",
|
|
35
|
+
};
|
|
36
|
+
const button: React.CSSProperties = {
|
|
37
|
+
width: "100%",
|
|
38
|
+
padding: "10px 14px",
|
|
39
|
+
borderRadius: theme.cornerRadius,
|
|
40
|
+
background: theme.colors.primary,
|
|
41
|
+
color: theme.colors.background,
|
|
42
|
+
border: "none",
|
|
43
|
+
fontWeight: 600,
|
|
44
|
+
cursor: "pointer",
|
|
45
|
+
};
|
|
46
|
+
const link: React.CSSProperties = { color: theme.colors.primary, textDecoration: "underline" };
|
|
47
|
+
|
|
48
|
+
if (flow.step === "code") {
|
|
49
|
+
return (
|
|
50
|
+
<div style={card}>
|
|
51
|
+
<h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 8 }}>Verify your email</h1>
|
|
52
|
+
<p style={{ fontSize: 14, opacity: 0.7, marginBottom: 24 }}>
|
|
53
|
+
We sent a 6-digit code to <strong>{flow.email}</strong>
|
|
54
|
+
</p>
|
|
55
|
+
{flow.error && <p style={{ color: "#ef4444", marginBottom: 16 }}>{flow.error}</p>}
|
|
56
|
+
<form
|
|
57
|
+
onSubmit={(e) => {
|
|
58
|
+
e.preventDefault();
|
|
59
|
+
flow.submitCode();
|
|
60
|
+
}}
|
|
61
|
+
style={{ display: "flex", flexDirection: "column", gap: 16 }}
|
|
62
|
+
>
|
|
63
|
+
<input
|
|
64
|
+
type="text"
|
|
65
|
+
inputMode="numeric"
|
|
66
|
+
maxLength={6}
|
|
67
|
+
placeholder="000000"
|
|
68
|
+
autoFocus
|
|
69
|
+
value={flow.code}
|
|
70
|
+
onChange={(e) => flow.setCode(e.target.value.replace(/\D/g, "").slice(0, 6))}
|
|
71
|
+
style={{ ...input, textAlign: "center", fontSize: 24, letterSpacing: "0.4em" }}
|
|
72
|
+
/>
|
|
73
|
+
<button type="submit" disabled={flow.isSubmitting || flow.code.length !== 6} style={button}>
|
|
74
|
+
{flow.isSubmitting ? "Verifying…" : "Verify & Sign In"}
|
|
75
|
+
</button>
|
|
76
|
+
</form>
|
|
77
|
+
<div style={{ display: "flex", justifyContent: "space-between", marginTop: 16, fontSize: 14 }}>
|
|
78
|
+
<button type="button" onClick={flow.back} style={{ ...link, background: "none", border: "none", cursor: "pointer" }}>
|
|
79
|
+
Back to login
|
|
80
|
+
</button>
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
onClick={flow.resend}
|
|
84
|
+
disabled={flow.isSubmitting}
|
|
85
|
+
style={{ ...link, background: "none", border: "none", cursor: "pointer" }}
|
|
86
|
+
>
|
|
87
|
+
Resend code
|
|
88
|
+
</button>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<div style={card}>
|
|
96
|
+
<h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 24 }}>Login</h1>
|
|
97
|
+
{flow.error && <p style={{ color: "#ef4444", marginBottom: 16 }}>{flow.error}</p>}
|
|
98
|
+
<form
|
|
99
|
+
onSubmit={(e) => {
|
|
100
|
+
e.preventDefault();
|
|
101
|
+
flow.submitCredentials();
|
|
102
|
+
}}
|
|
103
|
+
style={{ display: "flex", flexDirection: "column", gap: 16 }}
|
|
104
|
+
>
|
|
105
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
|
106
|
+
<label style={{ fontSize: 14 }}>Email</label>
|
|
107
|
+
<input type="email" placeholder="Email" value={flow.email} onChange={(e) => flow.setEmail(e.target.value)} style={input} />
|
|
108
|
+
</div>
|
|
109
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
|
110
|
+
<label style={{ fontSize: 14 }}>Password</label>
|
|
111
|
+
<input
|
|
112
|
+
type="password"
|
|
113
|
+
placeholder="Password"
|
|
114
|
+
value={flow.password}
|
|
115
|
+
onChange={(e) => flow.setPassword(e.target.value)}
|
|
116
|
+
style={input}
|
|
117
|
+
/>
|
|
118
|
+
<a href={forgotHref} style={{ ...link, fontSize: 13 }}>
|
|
119
|
+
Forgot password?
|
|
120
|
+
</a>
|
|
121
|
+
</div>
|
|
122
|
+
<button type="submit" disabled={flow.isSubmitting} style={button}>
|
|
123
|
+
{flow.isSubmitting ? "Sending code…" : "Login"}
|
|
124
|
+
</button>
|
|
125
|
+
</form>
|
|
126
|
+
<p style={{ marginTop: 16, fontSize: 14, textAlign: "center" }}>
|
|
127
|
+
Don't have an account?{" "}
|
|
128
|
+
<a href={signupHref} style={link}>
|
|
129
|
+
Sign up
|
|
130
|
+
</a>
|
|
131
|
+
</p>
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import ReactMarkdown from "react-markdown";
|
|
2
|
+
import remarkGfm from "remark-gfm";
|
|
3
|
+
import type { CSSProperties } from "react";
|
|
4
|
+
import { useForgeTheme } from "../theme/ForgeThemeProvider";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Renders assistant text as Markdown. The agent produces lists, bullets, code,
|
|
8
|
+
* links, and the occasional table — formatting those is the difference between
|
|
9
|
+
* a chat surface and a wall of plaintext.
|
|
10
|
+
*
|
|
11
|
+
* Forge is a standalone SDK (no Tailwind on code-generated sites), so unlike the
|
|
12
|
+
* admin Mira renderer this styles every element with inline styles driven by the
|
|
13
|
+
* Forge theme. GFM gives us tables + task lists.
|
|
14
|
+
*/
|
|
15
|
+
export function Markdown({ children }: { children: string }) {
|
|
16
|
+
const theme = useForgeTheme();
|
|
17
|
+
const text = theme.colors.text;
|
|
18
|
+
const accent = theme.colors.primary;
|
|
19
|
+
const subtle = `${text}14`; // ~8% overlay for code/quote backgrounds
|
|
20
|
+
const border = `${text}26`;
|
|
21
|
+
|
|
22
|
+
const codeBase: CSSProperties = {
|
|
23
|
+
fontFamily:
|
|
24
|
+
'ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace',
|
|
25
|
+
background: subtle,
|
|
26
|
+
borderRadius: 4,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div style={{ fontSize: 14, lineHeight: 1.55, wordBreak: "break-word" }}>
|
|
31
|
+
<ReactMarkdown
|
|
32
|
+
remarkPlugins={[remarkGfm]}
|
|
33
|
+
components={{
|
|
34
|
+
p: ({ children }) => <p style={{ margin: "6px 0" }}>{children}</p>,
|
|
35
|
+
ul: ({ children }) => (
|
|
36
|
+
<ul style={{ margin: "6px 0", paddingLeft: 20, display: "flex", flexDirection: "column", gap: 2 }}>
|
|
37
|
+
{children}
|
|
38
|
+
</ul>
|
|
39
|
+
),
|
|
40
|
+
ol: ({ children }) => (
|
|
41
|
+
<ol style={{ margin: "6px 0", paddingLeft: 20, display: "flex", flexDirection: "column", gap: 2 }}>
|
|
42
|
+
{children}
|
|
43
|
+
</ol>
|
|
44
|
+
),
|
|
45
|
+
li: ({ children }) => <li style={{ lineHeight: 1.5 }}>{children}</li>,
|
|
46
|
+
h1: ({ children }) => <h1 style={{ fontSize: 18, fontWeight: 600, margin: "8px 0 4px" }}>{children}</h1>,
|
|
47
|
+
h2: ({ children }) => <h2 style={{ fontSize: 16, fontWeight: 600, margin: "8px 0 4px" }}>{children}</h2>,
|
|
48
|
+
h3: ({ children }) => <h3 style={{ fontSize: 14, fontWeight: 600, margin: "6px 0 4px" }}>{children}</h3>,
|
|
49
|
+
h4: ({ children }) => <h4 style={{ fontSize: 14, fontWeight: 600, margin: "6px 0 4px" }}>{children}</h4>,
|
|
50
|
+
strong: ({ children }) => <strong style={{ fontWeight: 600 }}>{children}</strong>,
|
|
51
|
+
em: ({ children }) => <em style={{ fontStyle: "italic" }}>{children}</em>,
|
|
52
|
+
a: ({ href, children }) => (
|
|
53
|
+
<a
|
|
54
|
+
href={href}
|
|
55
|
+
target="_blank"
|
|
56
|
+
rel="noreferrer noopener"
|
|
57
|
+
style={{ color: accent, textDecoration: "underline", textUnderlineOffset: 2 }}
|
|
58
|
+
>
|
|
59
|
+
{children}
|
|
60
|
+
</a>
|
|
61
|
+
),
|
|
62
|
+
code: ({ children, ...props }) => {
|
|
63
|
+
const inline = !(props as { className?: string }).className;
|
|
64
|
+
if (inline) {
|
|
65
|
+
return <code style={{ ...codeBase, padding: "1px 5px", fontSize: "0.85em" }}>{children}</code>;
|
|
66
|
+
}
|
|
67
|
+
return (
|
|
68
|
+
<code style={{ ...codeBase, display: "block", padding: "8px 10px", fontSize: 12, overflowX: "auto" }}>
|
|
69
|
+
{children}
|
|
70
|
+
</code>
|
|
71
|
+
);
|
|
72
|
+
},
|
|
73
|
+
pre: ({ children }) => (
|
|
74
|
+
<pre style={{ ...codeBase, padding: 10, fontSize: 12, overflowX: "auto", margin: "6px 0" }}>{children}</pre>
|
|
75
|
+
),
|
|
76
|
+
blockquote: ({ children }) => (
|
|
77
|
+
<blockquote
|
|
78
|
+
style={{ borderLeft: `3px solid ${border}`, paddingLeft: 10, margin: "6px 0", opacity: 0.9 }}
|
|
79
|
+
>
|
|
80
|
+
{children}
|
|
81
|
+
</blockquote>
|
|
82
|
+
),
|
|
83
|
+
hr: () => <hr style={{ border: 0, borderTop: `1px solid ${border}`, margin: "10px 0" }} />,
|
|
84
|
+
table: ({ children }) => (
|
|
85
|
+
<div style={{ overflowX: "auto", margin: "6px 0" }}>
|
|
86
|
+
<table style={{ fontSize: 12, borderCollapse: "collapse", width: "100%" }}>{children}</table>
|
|
87
|
+
</div>
|
|
88
|
+
),
|
|
89
|
+
th: ({ children }) => (
|
|
90
|
+
<th style={{ border: `1px solid ${border}`, background: subtle, padding: "4px 8px", textAlign: "left", fontWeight: 600 }}>
|
|
91
|
+
{children}
|
|
92
|
+
</th>
|
|
93
|
+
),
|
|
94
|
+
td: ({ children }) => (
|
|
95
|
+
<td style={{ border: `1px solid ${border}`, padding: "4px 8px", verticalAlign: "top" }}>{children}</td>
|
|
96
|
+
),
|
|
97
|
+
}}
|
|
98
|
+
>
|
|
99
|
+
{children}
|
|
100
|
+
</ReactMarkdown>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
import { useMembershipCheckout } from "../headless/membership/useMembershipCheckout";
|
|
3
|
+
import { useForgeTheme } from "../theme/ForgeThemeProvider";
|
|
4
|
+
import { useAmountFormatter } from "../format/useFormatCurrency";
|
|
5
|
+
import { priceTaxCaption } from "../format/PriceDisplay";
|
|
6
|
+
import { usePricesIncludeTax } from "../../data/queries/useWebsite";
|
|
7
|
+
import { usePaymentRenderer } from "../payment/ForgePaymentProvider";
|
|
8
|
+
import { readableTextOn } from "../theme/contrast";
|
|
9
|
+
import { Loading } from "./Loading";
|
|
10
|
+
|
|
11
|
+
export interface MembershipCheckoutProps {
|
|
12
|
+
/** Pre-selected tier (e.g. from a `?membershipTierId=` param). */
|
|
13
|
+
initialTierId?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Called on a successful subscription (free activation, or after a paid
|
|
16
|
+
* payment succeeds) — the host navigates however it wants (e.g. to the account
|
|
17
|
+
* page). Omit to fall back to Forge's default `window.location` redirect.
|
|
18
|
+
*/
|
|
19
|
+
onComplete?: () => void;
|
|
20
|
+
/** The URL Stripe returns to for a PAID subscription (its `return_url`, and the
|
|
21
|
+
* free/paid fallback redirect). Default `/i/account?tab=membership&confirmSubscription=true`. */
|
|
22
|
+
successPath?: string;
|
|
23
|
+
/** Where the "View memberships" link points when no tier is selected. Default `/i/membership`. */
|
|
24
|
+
membershipsPath?: string;
|
|
25
|
+
/** Format a numeric amount for display. Defaults to the runtime currency formatter. */
|
|
26
|
+
formatAmount?: (amount: number) => string;
|
|
27
|
+
className?: string;
|
|
28
|
+
style?: CSSProperties;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Themed membership subscribe — the full flow (tier summary → billing cycle /
|
|
33
|
+
* pay-what-you-want → payment) on `useMembershipCheckout`. Free tiers activate
|
|
34
|
+
* immediately; paid tiers render the registered payment UI (Stripe, manual mode).
|
|
35
|
+
* A single drop-in reused by every site so the flow is fixed in one place.
|
|
36
|
+
*/
|
|
37
|
+
export function MembershipCheckout({
|
|
38
|
+
initialTierId,
|
|
39
|
+
onComplete,
|
|
40
|
+
successPath = "/i/account?tab=membership&confirmSubscription=true",
|
|
41
|
+
membershipsPath = "/i/membership",
|
|
42
|
+
formatAmount,
|
|
43
|
+
className,
|
|
44
|
+
style,
|
|
45
|
+
}: MembershipCheckoutProps) {
|
|
46
|
+
const theme = useForgeTheme();
|
|
47
|
+
const fmt = useAmountFormatter(formatAmount);
|
|
48
|
+
// Memberships have no checkout tax quote (subscription rail) — inclusive
|
|
49
|
+
// stores still caption the displayed price as tax-inclusive (display only).
|
|
50
|
+
const taxCaption = priceTaxCaption({ pricesIncludeTax: usePricesIncludeTax() });
|
|
51
|
+
const renderPayment = usePaymentRenderer();
|
|
52
|
+
const checkout = useMembershipCheckout({
|
|
53
|
+
initialTierId,
|
|
54
|
+
returnPath: successPath,
|
|
55
|
+
freeReturnPath: successPath,
|
|
56
|
+
onComplete,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const card: CSSProperties = { maxWidth: 560, margin: "0 auto", color: theme.colors.text, ...style };
|
|
60
|
+
const box: CSSProperties = {
|
|
61
|
+
border: `1px solid ${theme.colors.primary}30`,
|
|
62
|
+
borderRadius: theme.cornerRadius,
|
|
63
|
+
padding: 24,
|
|
64
|
+
marginBottom: 24,
|
|
65
|
+
};
|
|
66
|
+
const button: CSSProperties = {
|
|
67
|
+
width: "100%",
|
|
68
|
+
padding: "10px 14px",
|
|
69
|
+
borderRadius: theme.cornerRadius,
|
|
70
|
+
background: theme.colors.primary,
|
|
71
|
+
color: readableTextOn(theme.colors.primary),
|
|
72
|
+
border: "none",
|
|
73
|
+
fontWeight: 600,
|
|
74
|
+
cursor: "pointer",
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
if (checkout.isLoading) return <Loading fullPage />;
|
|
78
|
+
|
|
79
|
+
const tier = checkout.selectedTier;
|
|
80
|
+
if (!tier) {
|
|
81
|
+
return (
|
|
82
|
+
<div className={className} style={card}>
|
|
83
|
+
<p>Select a membership tier to continue.</p>
|
|
84
|
+
<a href={membershipsPath} style={{ ...button, marginTop: 16, display: "inline-block", textAlign: "center", textDecoration: "none" }}>
|
|
85
|
+
View memberships
|
|
86
|
+
</a>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Payment step — render the registered payment UI (Stripe) in manual mode.
|
|
92
|
+
if (checkout.step === "payment" && checkout.clientSecret) {
|
|
93
|
+
return (
|
|
94
|
+
<div className={className} style={card}>
|
|
95
|
+
<h1 style={{ fontSize: 26, fontWeight: 800, marginBottom: 8 }}>Payment</h1>
|
|
96
|
+
<p style={{ opacity: 0.75, marginBottom: 24 }}>
|
|
97
|
+
{tier.name} — {fmt(checkout.amount)} / {checkout.billingCycle}
|
|
98
|
+
</p>
|
|
99
|
+
<div style={box}>
|
|
100
|
+
{renderPayment({
|
|
101
|
+
clientSecret: checkout.clientSecret,
|
|
102
|
+
returnUrl: `${typeof window !== "undefined" ? window.location.origin : ""}${successPath}`,
|
|
103
|
+
amount: checkout.amount,
|
|
104
|
+
mode: "manual",
|
|
105
|
+
onSucceeded: () => onComplete?.(),
|
|
106
|
+
onFailed: () => {},
|
|
107
|
+
})}
|
|
108
|
+
</div>
|
|
109
|
+
<button
|
|
110
|
+
onClick={() => checkout.setStep("select")}
|
|
111
|
+
style={{ ...button, background: "transparent", color: theme.colors.text, border: `1px solid ${theme.colors.primary}40` }}
|
|
112
|
+
>
|
|
113
|
+
Back
|
|
114
|
+
</button>
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const showBillingToggle = !tier.payWhatYouWant && !!tier.priceMonthly && !!tier.priceYearly;
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<div className={className} style={card}>
|
|
123
|
+
<h1 style={{ fontSize: 26, fontWeight: 800, marginBottom: 24 }}>Join {tier.name}</h1>
|
|
124
|
+
|
|
125
|
+
<div style={box}>
|
|
126
|
+
{tier.description && <p style={{ marginBottom: 16 }}>{tier.description}</p>}
|
|
127
|
+
{!!tier.benefits?.length && (
|
|
128
|
+
<ul style={{ listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 8 }}>
|
|
129
|
+
{tier.benefits.map((b) => (
|
|
130
|
+
<li key={b.id} style={{ display: "flex", gap: 8 }}>
|
|
131
|
+
<span style={{ color: theme.colors.primary }}>✓</span>
|
|
132
|
+
{b.title}
|
|
133
|
+
</li>
|
|
134
|
+
))}
|
|
135
|
+
</ul>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<div style={box}>
|
|
140
|
+
{showBillingToggle && (
|
|
141
|
+
<div style={{ marginBottom: 16 }}>
|
|
142
|
+
<p style={{ fontSize: 14, marginBottom: 8 }}>Billing cycle</p>
|
|
143
|
+
<div style={{ display: "flex", gap: 16 }}>
|
|
144
|
+
{(["month", "year"] as const).map((cycle) => (
|
|
145
|
+
<label key={cycle} style={{ display: "flex", alignItems: "center", gap: 6, cursor: "pointer" }}>
|
|
146
|
+
<input
|
|
147
|
+
type="radio"
|
|
148
|
+
name="billingCycle"
|
|
149
|
+
checked={checkout.billingCycle === cycle}
|
|
150
|
+
onChange={() => checkout.setBillingCycle(cycle)}
|
|
151
|
+
style={{ accentColor: theme.colors.primary }}
|
|
152
|
+
/>
|
|
153
|
+
{cycle === "month" ? "Monthly" : "Yearly"}
|
|
154
|
+
</label>
|
|
155
|
+
))}
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
)}
|
|
159
|
+
|
|
160
|
+
{tier.payWhatYouWant && (
|
|
161
|
+
<div style={{ marginBottom: 16 }}>
|
|
162
|
+
<label style={{ fontSize: 14, display: "block", marginBottom: 8 }}>How much would you like to pay?</label>
|
|
163
|
+
<input
|
|
164
|
+
type="number"
|
|
165
|
+
min={tier.payWhatYouWantMinimum ?? 0}
|
|
166
|
+
value={checkout.customAmount || ""}
|
|
167
|
+
onChange={(e) => checkout.setCustomAmount(Number(e.target.value))}
|
|
168
|
+
style={{
|
|
169
|
+
width: "100%",
|
|
170
|
+
height: 40,
|
|
171
|
+
padding: "8px 12px",
|
|
172
|
+
borderRadius: theme.cornerRadius,
|
|
173
|
+
border: `1px solid ${theme.colors.primary}40`,
|
|
174
|
+
background: "transparent",
|
|
175
|
+
color: theme.colors.text,
|
|
176
|
+
outline: "none",
|
|
177
|
+
}}
|
|
178
|
+
/>
|
|
179
|
+
</div>
|
|
180
|
+
)}
|
|
181
|
+
|
|
182
|
+
<div style={{ textAlign: "center", paddingTop: 16, borderTop: `1px solid ${theme.colors.primary}20` }}>
|
|
183
|
+
<p style={{ fontSize: 14, opacity: 0.75 }}>Total</p>
|
|
184
|
+
<p style={{ fontSize: 28, fontWeight: 800 }}>{fmt(checkout.amount)}</p>
|
|
185
|
+
<p style={{ fontSize: 14, opacity: 0.75 }}>{checkout.billingCycle === "month" ? "per month" : "per year"}</p>
|
|
186
|
+
{taxCaption && <p style={{ fontSize: 12, opacity: 0.65, marginTop: 2 }}>{taxCaption}</p>}
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
{checkout.error && <p style={{ color: "#ef4444", marginBottom: 16 }}>{checkout.error}</p>}
|
|
191
|
+
|
|
192
|
+
<button onClick={() => checkout.subscribe()} disabled={checkout.isProcessing} style={button}>
|
|
193
|
+
{checkout.isProcessing ? "Processing…" : "Subscribe"}
|
|
194
|
+
</button>
|
|
195
|
+
</div>
|
|
196
|
+
);
|
|
197
|
+
}
|