@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.
Files changed (255) hide show
  1. package/package.json +42 -0
  2. package/src/client/createForgeClient.ts +54 -0
  3. package/src/content/ForgeContentProvider.tsx +118 -0
  4. package/src/content/ForgeEditBridge.tsx +134 -0
  5. package/src/content/editFlag.ts +23 -0
  6. package/src/content/index.ts +28 -0
  7. package/src/content/resolve.ts +67 -0
  8. package/src/content/types.ts +97 -0
  9. package/src/content/useField.ts +64 -0
  10. package/src/contexts/AppAuthContext.tsx +185 -0
  11. package/src/contexts/AudioPlayerContext.tsx +389 -0
  12. package/src/contexts/CartContext.tsx +131 -0
  13. package/src/contexts/PublicAuthContext.tsx +333 -0
  14. package/src/contexts/useAppAdminGuard.ts +56 -0
  15. package/src/css.d.ts +5 -0
  16. package/src/data/collectionParams.ts +57 -0
  17. package/src/data/queries/useAccount.ts +71 -0
  18. package/src/data/queries/useAccountSettings.ts +82 -0
  19. package/src/data/queries/useAnalytics.ts +86 -0
  20. package/src/data/queries/useAuthActions.ts +28 -0
  21. package/src/data/queries/useBlog.ts +103 -0
  22. package/src/data/queries/useBlogComments.ts +96 -0
  23. package/src/data/queries/useBroadcasts.ts +31 -0
  24. package/src/data/queries/useCertificates.ts +58 -0
  25. package/src/data/queries/useChat.ts +267 -0
  26. package/src/data/queries/useCoachingAvailability.ts +69 -0
  27. package/src/data/queries/useCoachingProducts.ts +67 -0
  28. package/src/data/queries/useCohortPage.ts +17 -0
  29. package/src/data/queries/useCollections.ts +211 -0
  30. package/src/data/queries/useCommunity.ts +794 -0
  31. package/src/data/queries/useCoupons.ts +20 -0
  32. package/src/data/queries/useCourseAccess.ts +50 -0
  33. package/src/data/queries/useCourses.ts +91 -0
  34. package/src/data/queries/useCurrencies.ts +33 -0
  35. package/src/data/queries/useDocuments.ts +208 -0
  36. package/src/data/queries/useDonations.ts +29 -0
  37. package/src/data/queries/useEngagement.ts +115 -0
  38. package/src/data/queries/useEvents.ts +69 -0
  39. package/src/data/queries/useFinalize.ts +109 -0
  40. package/src/data/queries/useForms.ts +31 -0
  41. package/src/data/queries/useInvoice.ts +65 -0
  42. package/src/data/queries/useLeadMagnet.ts +96 -0
  43. package/src/data/queries/useMembership.ts +20 -0
  44. package/src/data/queries/useNotifications.ts +267 -0
  45. package/src/data/queries/useOffers.ts +56 -0
  46. package/src/data/queries/useOrders.ts +71 -0
  47. package/src/data/queries/usePWA.ts +101 -0
  48. package/src/data/queries/usePageActions.ts +36 -0
  49. package/src/data/queries/usePaymentFlow.ts +80 -0
  50. package/src/data/queries/usePaymentLink.ts +47 -0
  51. package/src/data/queries/usePodcast.ts +85 -0
  52. package/src/data/queries/usePostCollections.ts +48 -0
  53. package/src/data/queries/usePosts.ts +94 -0
  54. package/src/data/queries/useProducts.ts +108 -0
  55. package/src/data/queries/useReplay.ts +19 -0
  56. package/src/data/queries/useReviews.ts +122 -0
  57. package/src/data/queries/useShipping.ts +109 -0
  58. package/src/data/queries/useSubscriptions.ts +66 -0
  59. package/src/data/queries/useWebsite.ts +128 -0
  60. package/src/data/queries/useWebsiteAgent.ts +100 -0
  61. package/src/index.ts +98 -0
  62. package/src/provider/ForgeAppProvider.tsx +149 -0
  63. package/src/provider/ForgeProvider.tsx +91 -0
  64. package/src/provider/SiteConfigProvider.tsx +27 -0
  65. package/src/server/_tests/buildPwaHead.spec.ts +182 -0
  66. package/src/server/_tests/buildWebManifest.spec.ts +313 -0
  67. package/src/server/_tests/forgeServiceWorkerScript.spec.ts +251 -0
  68. package/src/server/index.ts +260 -0
  69. package/src/server/jobs.ts +104 -0
  70. package/src/server/pwa.ts +332 -0
  71. package/src/types/index.ts +74 -0
  72. package/src/types/models.ts +1313 -0
  73. package/src/ui/analytics/ForgeAnalytics.tsx +208 -0
  74. package/src/ui/analytics/PageMetaPixel.tsx +92 -0
  75. package/src/ui/content/Editable.tsx +90 -0
  76. package/src/ui/format/PriceDisplay.tsx +142 -0
  77. package/src/ui/format/useFormatCurrency.ts +92 -0
  78. package/src/ui/headless/agent/useAiAgent.ts +106 -0
  79. package/src/ui/headless/auth/useLoginFlow.ts +72 -0
  80. package/src/ui/headless/auth/useSignupForm.ts +85 -0
  81. package/src/ui/headless/broadcast/useBroadcastWatch.ts +59 -0
  82. package/src/ui/headless/chat/attachmentKind.ts +27 -0
  83. package/src/ui/headless/chat/useChatAttachmentUpload.ts +132 -0
  84. package/src/ui/headless/chat/useChatRoom.ts +223 -0
  85. package/src/ui/headless/chat/useMessageThread.ts +60 -0
  86. package/src/ui/headless/checkout/useCheckout.ts +438 -0
  87. package/src/ui/headless/coaching/useCoachingBooking.ts +223 -0
  88. package/src/ui/headless/community/index.ts +20 -0
  89. package/src/ui/headless/community/lexical.ts +131 -0
  90. package/src/ui/headless/community/replyTree.ts +51 -0
  91. package/src/ui/headless/community/richLexical.ts +314 -0
  92. package/src/ui/headless/community/useCommunityAttachmentUpload.ts +137 -0
  93. package/src/ui/headless/community/useCommunitySpaceFeed.ts +122 -0
  94. package/src/ui/headless/community/useCommunityThread.ts +95 -0
  95. package/src/ui/headless/community/useMentionComposer.ts +105 -0
  96. package/src/ui/headless/community/useRichMentionComposer.ts +193 -0
  97. package/src/ui/headless/consent/useCookieConsent.ts +81 -0
  98. package/src/ui/headless/course/useCourseCheckout.ts +99 -0
  99. package/src/ui/headless/dialog.tsx +14 -0
  100. package/src/ui/headless/document/index.ts +7 -0
  101. package/src/ui/headless/document/useContractSignature.ts +68 -0
  102. package/src/ui/headless/document/useDocumentSigningFlow.ts +92 -0
  103. package/src/ui/headless/document/useOtpCodeInput.ts +86 -0
  104. package/src/ui/headless/document/useSignaturePad.ts +180 -0
  105. package/src/ui/headless/donation/Donation.tsx +115 -0
  106. package/src/ui/headless/donation/DonationContext.tsx +223 -0
  107. package/src/ui/headless/donation/fees.ts +15 -0
  108. package/src/ui/headless/donation/index.ts +10 -0
  109. package/src/ui/headless/engagement/usePostEngagement.ts +119 -0
  110. package/src/ui/headless/event/useEventCheckout.ts +141 -0
  111. package/src/ui/headless/forms/useContactForm.ts +58 -0
  112. package/src/ui/headless/forms/useEmailListForm.ts +75 -0
  113. package/src/ui/headless/forms/useSectionedForm.ts +256 -0
  114. package/src/ui/headless/index.ts +60 -0
  115. package/src/ui/headless/invoice/useInvoicePayment.ts +48 -0
  116. package/src/ui/headless/memberHome/index.ts +7 -0
  117. package/src/ui/headless/memberHome/useMemberHomeNav.ts +82 -0
  118. package/src/ui/headless/membership/MembershipGate.tsx +39 -0
  119. package/src/ui/headless/membership/useMembershipCheckout.ts +121 -0
  120. package/src/ui/headless/offer/Offer.tsx +88 -0
  121. package/src/ui/headless/offer/OfferContext.tsx +162 -0
  122. package/src/ui/headless/offer/index.ts +8 -0
  123. package/src/ui/headless/paymentLink/usePaymentLinkPayment.ts +46 -0
  124. package/src/ui/headless/podcast/usePodcastPlayer.ts +96 -0
  125. package/src/ui/headless/posts/index.ts +13 -0
  126. package/src/ui/headless/posts/upsell.ts +41 -0
  127. package/src/ui/headless/posts/useMemberPostsFeed.ts +110 -0
  128. package/src/ui/headless/posts/usePostCollectionDetail.ts +56 -0
  129. package/src/ui/headless/posts/usePostCollectionsList.ts +90 -0
  130. package/src/ui/headless/posts/usePostItem.ts +81 -0
  131. package/src/ui/headless/reviews/index.ts +6 -0
  132. package/src/ui/headless/reviews/useHelpfulVote.ts +45 -0
  133. package/src/ui/headless/reviews/useReviewForm.ts +108 -0
  134. package/src/ui/headless/reviews/useReviewsList.ts +79 -0
  135. package/src/ui/headless/work/index.ts +25 -0
  136. package/src/ui/headless/work/useWorkPortal.ts +349 -0
  137. package/src/ui/index.ts +144 -0
  138. package/src/ui/payment/ForgePaymentProvider.tsx +45 -0
  139. package/src/ui/payment/ForgeStripePayment.tsx +171 -0
  140. package/src/ui/shell/TribeNestApp.tsx +121 -0
  141. package/src/ui/shell/shellGating.spec.ts +28 -0
  142. package/src/ui/shell/shellGating.ts +11 -0
  143. package/src/ui/styled/AccountDashboard.tsx +575 -0
  144. package/src/ui/styled/AiAgentWidget.tsx +152 -0
  145. package/src/ui/styled/AudioPlayer.tsx +123 -0
  146. package/src/ui/styled/BlogCategory.tsx +79 -0
  147. package/src/ui/styled/BlogComments.tsx +236 -0
  148. package/src/ui/styled/BlogList.tsx +101 -0
  149. package/src/ui/styled/BlogPost.tsx +160 -0
  150. package/src/ui/styled/BlogPostFooter.tsx +33 -0
  151. package/src/ui/styled/BotProtection.tsx +101 -0
  152. package/src/ui/styled/Button.tsx +88 -0
  153. package/src/ui/styled/Cart.tsx +276 -0
  154. package/src/ui/styled/ChatRoom.tsx +252 -0
  155. package/src/ui/styled/Checkout.tsx +988 -0
  156. package/src/ui/styled/CheckoutConfirmation.tsx +452 -0
  157. package/src/ui/styled/CoachingBooking.tsx +605 -0
  158. package/src/ui/styled/CoachingConfirmation.tsx +151 -0
  159. package/src/ui/styled/CoachingDetail.tsx +99 -0
  160. package/src/ui/styled/CohortPage.tsx +82 -0
  161. package/src/ui/styled/ConfirmSubscription.tsx +134 -0
  162. package/src/ui/styled/Confirmation.tsx +171 -0
  163. package/src/ui/styled/ContactForm.tsx +81 -0
  164. package/src/ui/styled/CookieConsent.tsx +239 -0
  165. package/src/ui/styled/CourseAccess.tsx +169 -0
  166. package/src/ui/styled/CourseCheckout.tsx +425 -0
  167. package/src/ui/styled/CourseConfirmation.tsx +191 -0
  168. package/src/ui/styled/CourseDetail.tsx +140 -0
  169. package/src/ui/styled/CoursesGrid.tsx +85 -0
  170. package/src/ui/styled/CurrencySwitcher.tsx +51 -0
  171. package/src/ui/styled/DonationButton.tsx +247 -0
  172. package/src/ui/styled/DonationPage.tsx +29 -0
  173. package/src/ui/styled/EmailListForm.tsx +88 -0
  174. package/src/ui/styled/EventConfirmation.tsx +228 -0
  175. package/src/ui/styled/EventCountdown.tsx +98 -0
  176. package/src/ui/styled/EventDetail.tsx +141 -0
  177. package/src/ui/styled/EventTickets.tsx +548 -0
  178. package/src/ui/styled/EventsList.tsx +83 -0
  179. package/src/ui/styled/ForgotPasswordForm.tsx +105 -0
  180. package/src/ui/styled/FormRenderer.tsx +249 -0
  181. package/src/ui/styled/InstallBanner.tsx +114 -0
  182. package/src/ui/styled/InvoiceConfirmation.tsx +50 -0
  183. package/src/ui/styled/InvoicePayment.tsx +141 -0
  184. package/src/ui/styled/LeadMagnet.tsx +93 -0
  185. package/src/ui/styled/Loading.tsx +56 -0
  186. package/src/ui/styled/LoginForm.tsx +134 -0
  187. package/src/ui/styled/Markdown.tsx +103 -0
  188. package/src/ui/styled/MembershipCheckout.tsx +197 -0
  189. package/src/ui/styled/MembershipTiers.tsx +115 -0
  190. package/src/ui/styled/OfferButton.tsx +181 -0
  191. package/src/ui/styled/PageActions.tsx +124 -0
  192. package/src/ui/styled/PaymentLinkConfirmation.tsx +50 -0
  193. package/src/ui/styled/PaymentLinkPayment.tsx +85 -0
  194. package/src/ui/styled/Paywall.tsx +54 -0
  195. package/src/ui/styled/PlayButton.tsx +48 -0
  196. package/src/ui/styled/PodcastEpisode.tsx +137 -0
  197. package/src/ui/styled/PodcastList.tsx +58 -0
  198. package/src/ui/styled/PodcastShow.tsx +164 -0
  199. package/src/ui/styled/PostsFeed.tsx +63 -0
  200. package/src/ui/styled/ProductDetail.tsx +616 -0
  201. package/src/ui/styled/ProductGrid.tsx +82 -0
  202. package/src/ui/styled/PushOptIn.tsx +65 -0
  203. package/src/ui/styled/PwaRegistration.tsx +58 -0
  204. package/src/ui/styled/ReactionBar.tsx +93 -0
  205. package/src/ui/styled/ReplayList.tsx +134 -0
  206. package/src/ui/styled/ResetPasswordForm.tsx +127 -0
  207. package/src/ui/styled/ReviewForm.tsx +292 -0
  208. package/src/ui/styled/ReviewsSection.tsx +260 -0
  209. package/src/ui/styled/SectionedFormRenderer.tsx +146 -0
  210. package/src/ui/styled/SignupForm.tsx +109 -0
  211. package/src/ui/styled/UserMenu.tsx +166 -0
  212. package/src/ui/styled/chat/ChatAttachments.tsx +172 -0
  213. package/src/ui/styled/chat/ChatComposer.tsx +153 -0
  214. package/src/ui/styled/chat/ChatThreadPanel.tsx +94 -0
  215. package/src/ui/styled/chat/MessageBubble.tsx +66 -0
  216. package/src/ui/styled/chat/NewDmDialog.tsx +78 -0
  217. package/src/ui/styled/community/CommunityComposer.tsx +358 -0
  218. package/src/ui/styled/community/CommunityFeed.tsx +483 -0
  219. package/src/ui/styled/community/CommunityLeaderboard.tsx +109 -0
  220. package/src/ui/styled/community/CommunityMediaGallery.tsx +116 -0
  221. package/src/ui/styled/community/CommunityMemberProfile.tsx +285 -0
  222. package/src/ui/styled/community/CommunityNotifications.tsx +222 -0
  223. package/src/ui/styled/community/CommunityPostCard.tsx +165 -0
  224. package/src/ui/styled/community/CommunityPostDetail.tsx +337 -0
  225. package/src/ui/styled/community/CommunityReactionBar.tsx +107 -0
  226. package/src/ui/styled/community/CommunityRichText.tsx +157 -0
  227. package/src/ui/styled/community/CommunitySpaces.tsx +174 -0
  228. package/src/ui/styled/community/MemberAvatar.tsx +74 -0
  229. package/src/ui/styled/community/index.ts +23 -0
  230. package/src/ui/styled/community/util.ts +15 -0
  231. package/src/ui/styled/members/MemberHomeLayout.tsx +98 -0
  232. package/src/ui/styled/members/MemberPostCard.tsx +392 -0
  233. package/src/ui/styled/members/MemberPostsFeed.tsx +219 -0
  234. package/src/ui/styled/members/PostCollectionDetail.tsx +227 -0
  235. package/src/ui/styled/members/PostCollectionsGrid.tsx +287 -0
  236. package/src/ui/styled/members/index.ts +7 -0
  237. package/src/ui/styled/rich-text.css +150 -0
  238. package/src/ui/styled/work/WorkInviteAccept.tsx +222 -0
  239. package/src/ui/styled/work/WorkPortalProjects.tsx +163 -0
  240. package/src/ui/styled/work/WorkProjectInvoices.tsx +113 -0
  241. package/src/ui/styled/work/WorkProjectReport.tsx +66 -0
  242. package/src/ui/styled/work/WorkReportBody.tsx +327 -0
  243. package/src/ui/styled/work/WorkTaskAttachments.tsx +67 -0
  244. package/src/ui/styled/work/WorkTaskComments.tsx +151 -0
  245. package/src/ui/styled/work/WorkTaskDetail.tsx +92 -0
  246. package/src/ui/styled/work/WorkTokenReport.tsx +49 -0
  247. package/src/ui/styled/work/index.ts +10 -0
  248. package/src/ui/theme/ForgeThemeProvider.tsx +121 -0
  249. package/src/ui/theme/contrast.ts +22 -0
  250. package/src/utils/analyticsBus.ts +31 -0
  251. package/src/utils/attribution.ts +68 -0
  252. package/src/utils/cookieConsent.ts +83 -0
  253. package/src/utils/landing.ts +153 -0
  254. package/src/utils/metaPixel.ts +122 -0
  255. 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&apos;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
+ }