@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,149 @@
1
+ import { useState, type ReactNode } from "react";
2
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
3
+ import { ForgeClientProvider } from "./ForgeProvider";
4
+ import { SiteConfigProvider } from "./SiteConfigProvider";
5
+ import { ForgeContentProvider } from "../content/ForgeContentProvider";
6
+ import { useField } from "../content/useField";
7
+ import { emptyContentDocument, type ContentDocument } from "../content/types";
8
+ import { PublicAuthProvider } from "../contexts/PublicAuthContext";
9
+ import { AppAuthProvider } from "../contexts/AppAuthContext";
10
+ import { CartProvider } from "../contexts/CartContext";
11
+ import { AudioPlayerProvider } from "../contexts/AudioPlayerContext";
12
+ import { ForgeThemeProvider, type ForgeTheme } from "../ui/theme/ForgeThemeProvider";
13
+ import { readableTextOn } from "../ui/theme/contrast";
14
+ import type { SiteConfig } from "../data/queries/useWebsite";
15
+
16
+ /** Forge's baseline theme — shown until the creator edits the `theme.*` content fields. */
17
+ const BASE_THEME: ForgeTheme = {
18
+ colors: { text: "#0f0f10", background: "#ffffff", primary: "#6d28d9" },
19
+ cornerRadius: 8,
20
+ };
21
+
22
+ /**
23
+ * Reads the theme from `theme.*` content fields and feeds `ForgeThemeProvider`
24
+ * (which also publishes the `--forge-*` CSS vars). Lives inside
25
+ * `ForgeContentProvider` so the agent/creator can recolor the whole site via
26
+ * `set_content_fields`. `defaultTheme` supplies the brand defaults for fields
27
+ * the creator hasn't set.
28
+ */
29
+ function ThemeFromContent({ defaultTheme, children }: { defaultTheme: ForgeTheme; children: ReactNode }) {
30
+ const primary = useField("theme.primary", { type: "color", default: defaultTheme.colors.primary, label: "Primary color" });
31
+ const background = useField("theme.background", {
32
+ type: "color",
33
+ default: defaultTheme.colors.background,
34
+ label: "Background color",
35
+ });
36
+ const text = useField("theme.text", { type: "color", default: defaultTheme.colors.text, label: "Text color" });
37
+ // Text/icon color placed ON the primary (buttons etc.) — defaults to an
38
+ // auto-contrast color against the primary.
39
+ const textPrimary = useField("theme.textPrimary", {
40
+ type: "color",
41
+ default: readableTextOn(primary.value),
42
+ label: "Text on primary",
43
+ });
44
+ // Richer, optional brand tokens — each falls back to a sensible derived value.
45
+ const secondary = useField("theme.secondary", { type: "color", default: primary.value, label: "Secondary color" });
46
+ const surface = useField("theme.surface", { type: "color", default: background.value, label: "Surface / card color" });
47
+ const theme: ForgeTheme = {
48
+ colors: {
49
+ primary: primary.value,
50
+ secondary: secondary.value,
51
+ background: background.value,
52
+ surface: surface.value,
53
+ text: text.value,
54
+ textPrimary: textPrimary.value,
55
+ },
56
+ cornerRadius: defaultTheme.cornerRadius,
57
+ };
58
+ return <ForgeThemeProvider theme={theme}>{children}</ForgeThemeProvider>;
59
+ }
60
+
61
+ export interface ForgeProviderProps {
62
+ /** Base URL of the TribeNest public API. */
63
+ apiUrl: string;
64
+ /** The tenant this deploy is pinned to. */
65
+ profileId?: string;
66
+ /** The app this deploy is the frontend for (mini-apps only; baked at build). */
67
+ appId?: string;
68
+ /** The tenant subdomain (also used to fetch site config). */
69
+ subdomain?: string;
70
+ rootDomain?: string;
71
+ publishableKey?: string;
72
+ /** Optional initial member token (e.g. restored from storage). */
73
+ initialToken?: string | null;
74
+ /** Editing affordances (the in-place content editor). Off for a live site. */
75
+ editable?: boolean;
76
+ /** Content locale. */
77
+ locale?: string;
78
+ /** SSR content document (from `fetchContentDocument`). Defaults to empty. */
79
+ initialContentDoc?: ContentDocument;
80
+ /** SSR site config (from `fetchSiteConfig`) — seeds `useSiteConfig` so runtime
81
+ * settings (currency, pixel, Stripe) are present on first paint, no flash. */
82
+ initialSiteConfig?: SiteConfig | null;
83
+ /** Brand default theme, shown until the creator edits the `theme.*` fields. */
84
+ defaultTheme?: ForgeTheme;
85
+ /** Supply your own React Query client; otherwise one is created per provider. */
86
+ queryClient?: QueryClient;
87
+ children: ReactNode;
88
+ }
89
+
90
+ /**
91
+ * The all-in-one root provider for a Forge-powered site. Nests, in order:
92
+ * `ForgeClientProvider` (tenant/client) → React Query → `SiteConfigProvider`
93
+ * (SSR runtime config) → `ForgeContentProvider` → auth → cart → audio player →
94
+ * theme-from-content. Wrap your app once and every Forge hook/block works — the
95
+ * whole provider tree is owned here so it can be updated centrally without
96
+ * touching each site.
97
+ *
98
+ * <ForgeProvider apiUrl={API_URL} profileId={PROFILE_ID} subdomain={SUBDOMAIN}
99
+ * editable={FORGE_EDIT} initialContentDoc={contentDoc} initialSiteConfig={siteConfig}>
100
+ * <App />
101
+ * </ForgeProvider>
102
+ */
103
+ export function ForgeProvider({
104
+ apiUrl,
105
+ profileId,
106
+ appId,
107
+ subdomain,
108
+ rootDomain,
109
+ publishableKey,
110
+ initialToken = null,
111
+ editable = false,
112
+ locale,
113
+ initialContentDoc,
114
+ initialSiteConfig,
115
+ defaultTheme = BASE_THEME,
116
+ queryClient,
117
+ children,
118
+ }: ForgeProviderProps) {
119
+ // One client per provider instance (per SSR request), unless the host supplies one.
120
+ const [client] = useState(() => queryClient ?? new QueryClient());
121
+
122
+ return (
123
+ <ForgeClientProvider
124
+ apiUrl={apiUrl}
125
+ rootDomain={rootDomain}
126
+ profileId={profileId}
127
+ appId={appId}
128
+ subdomain={subdomain}
129
+ publishableKey={publishableKey}
130
+ initialToken={initialToken}
131
+ >
132
+ <QueryClientProvider client={client}>
133
+ <SiteConfigProvider initialConfig={initialSiteConfig}>
134
+ <ForgeContentProvider initialDoc={initialContentDoc ?? emptyContentDocument(locale)} locale={locale} editable={editable}>
135
+ <PublicAuthProvider>
136
+ <AppAuthProvider>
137
+ <CartProvider>
138
+ <AudioPlayerProvider>
139
+ <ThemeFromContent defaultTheme={defaultTheme}>{children}</ThemeFromContent>
140
+ </AudioPlayerProvider>
141
+ </CartProvider>
142
+ </AppAuthProvider>
143
+ </PublicAuthProvider>
144
+ </ForgeContentProvider>
145
+ </SiteConfigProvider>
146
+ </QueryClientProvider>
147
+ </ForgeClientProvider>
148
+ );
149
+ }
@@ -0,0 +1,91 @@
1
+ import { createContext, useCallback, useContext, useMemo, useRef, useState } from "react";
2
+ import type { AxiosInstance } from "axios";
3
+ import { createForgeClient } from "../client/createForgeClient";
4
+
5
+ export interface ForgeContextValue {
6
+ /** Base URL of the TribeNest public API. */
7
+ apiUrl: string;
8
+ /** Root domain (e.g. tribenest.co). Empty when not relevant. */
9
+ rootDomain: string;
10
+ /** The tenant this site is pinned to (one deploy = one profileId). */
11
+ profileId?: string;
12
+ /** The app this deploy is the frontend for (mini-apps only; baked at build). */
13
+ appId?: string;
14
+ /** The tenant subdomain, when resolved. */
15
+ subdomain?: string;
16
+ /** Per-tenant publishable key (`pk_site_…`), sent as `x-forge-key`. */
17
+ publishableKey?: string;
18
+ /** The configured Axios client, scoped to this tenant. */
19
+ client: AxiosInstance;
20
+ /** Current member bearer token, or null when anonymous. */
21
+ token: string | null;
22
+ /** Set/clear the member bearer token. */
23
+ setToken: (token: string | null) => void;
24
+ }
25
+
26
+ const ForgeContext = createContext<ForgeContextValue | null>(null);
27
+
28
+ export interface ForgeClientProviderProps {
29
+ apiUrl: string;
30
+ rootDomain?: string;
31
+ profileId?: string;
32
+ appId?: string;
33
+ subdomain?: string;
34
+ publishableKey?: string;
35
+ /** Optional initial token (e.g. restored from storage on mount). */
36
+ initialToken?: string | null;
37
+ children: React.ReactNode;
38
+ }
39
+
40
+ /**
41
+ * Low-level tenant/client provider — supplies the `useForge()` context (Axios
42
+ * client, tenant ids, token). Most sites use the all-in-one `<ForgeProvider>`
43
+ * (which wraps this plus query/content/auth/cart/audio/site-config/theme); reach
44
+ * for `ForgeClientProvider` only when composing the tree yourself.
45
+ *
46
+ * The Axios client is created once per `apiUrl` and reads the token from a ref,
47
+ * so changing the token never tears down/recreates the client.
48
+ */
49
+ export const ForgeClientProvider = ({
50
+ apiUrl,
51
+ rootDomain = "",
52
+ profileId,
53
+ appId,
54
+ subdomain,
55
+ publishableKey,
56
+ initialToken = null,
57
+ children,
58
+ }: ForgeClientProviderProps) => {
59
+ const [token, setTokenState] = useState<string | null>(initialToken);
60
+
61
+ // The client reads the token from this ref per-request. `setToken` updates the
62
+ // ref *synchronously* (so a request fired immediately after setToken uses the
63
+ // new token) and the state (to re-render consumers of `token`).
64
+ const tokenRef = useRef<string | null>(initialToken);
65
+
66
+ const setToken = useCallback((next: string | null) => {
67
+ tokenRef.current = next;
68
+ setTokenState(next);
69
+ }, []);
70
+
71
+ const client = useMemo(
72
+ () => createForgeClient({ baseURL: apiUrl, publishableKey, getToken: () => tokenRef.current }),
73
+ [apiUrl, publishableKey],
74
+ );
75
+
76
+ const value = useMemo<ForgeContextValue>(
77
+ () => ({ apiUrl, rootDomain, profileId, appId, subdomain, publishableKey, client, token, setToken }),
78
+ [apiUrl, rootDomain, profileId, appId, subdomain, publishableKey, client, token, setToken],
79
+ );
80
+
81
+ return <ForgeContext.Provider value={value}>{children}</ForgeContext.Provider>;
82
+ };
83
+
84
+ /** Access the Forge context. Throws if used outside a `<ForgeProvider>`. */
85
+ export const useForge = (): ForgeContextValue => {
86
+ const ctx = useContext(ForgeContext);
87
+ if (!ctx) {
88
+ throw new Error("useForge must be used within a <ForgeProvider>");
89
+ }
90
+ return ctx;
91
+ };
@@ -0,0 +1,27 @@
1
+ import { createContext, useContext, type ReactNode } from "react";
2
+ import type { SiteConfig } from "../data/queries/useWebsite";
3
+
4
+ /**
5
+ * Holds an SSR-fetched site config so `useSiteConfig()` can seed React Query
6
+ * with it (as `initialData`) and have the value on the very first render — no
7
+ * client-fetch flash, no hydration mismatch. `undefined` when the site config
8
+ * wasn't fetched on the server, in which case `useSiteConfig` just fetches on
9
+ * the client as before.
10
+ */
11
+ const SiteConfigContext = createContext<SiteConfig | undefined>(undefined);
12
+
13
+ /** Seed the SSR-fetched site config (see `fetchSiteConfig` in `@tribe-nest/forge/server`). */
14
+ export function SiteConfigProvider({
15
+ initialConfig,
16
+ children,
17
+ }: {
18
+ initialConfig?: SiteConfig | null;
19
+ children: ReactNode;
20
+ }) {
21
+ return <SiteConfigContext.Provider value={initialConfig ?? undefined}>{children}</SiteConfigContext.Provider>;
22
+ }
23
+
24
+ /** The SSR-seeded site config, if a `SiteConfigProvider` supplied one. */
25
+ export function useInitialSiteConfig(): SiteConfig | undefined {
26
+ return useContext(SiteConfigContext);
27
+ }
@@ -0,0 +1,182 @@
1
+ // Unit tests for buildPwaHead() — the React-free Forge server builder that emits
2
+ // the manifest <link>, apple-touch-icon, theme-color, and iOS standalone metas
3
+ // for a code site's route head(). Pure function, no DB, no network.
4
+ //
5
+ // Coverage:
6
+ // - happy path: full link/meta set with deep assertions
7
+ // - apple-touch-icon derive (icon192 preferred, icon512 fallback, absent → omit)
8
+ // - theme-color derivation + default fallback (bare + localized + no doc)
9
+ // - manifestHref override / default
10
+ // - apple-mobile-web-app-title fallback chain (shortName → name → siteName → "App")
11
+ // - null/undefined siteConfig degradation (never throws)
12
+ // - the known client bug this fixes: apple-touch-icon is always emitted when an icon exists
13
+ //
14
+ // Source: packages/forge/src/server/pwa.ts. Spec: docs/pwa-custom-code-initiative.md.
15
+
16
+ import { describe, it, expect } from "vitest";
17
+ import { buildPwaHead } from "../pwa";
18
+ import type { SiteConfig, PwaConfig } from "../../data/queries/useWebsite";
19
+ import type { ContentDocument } from "../../content/types";
20
+ import type { HeadMeta as HeadMetaT } from "../pwa";
21
+
22
+ const DEFAULT_PRIMARY = "#6d28d9";
23
+
24
+ function fullPwa(overrides: Partial<PwaConfig> = {}): PwaConfig {
25
+ return {
26
+ name: "Nova Sound",
27
+ shortName: "Nova",
28
+ description: "The Nova Sound fan app",
29
+ icon96: "https://cdn.tribenest.co/p/nova/icon-96.png",
30
+ icon192: "https://cdn.tribenest.co/p/nova/icon-192.png",
31
+ icon512: "https://cdn.tribenest.co/p/nova/icon-512.png",
32
+ ...overrides,
33
+ };
34
+ }
35
+
36
+ function siteConfig(overrides: Partial<SiteConfig> = {}): SiteConfig {
37
+ return {
38
+ currency: "USD",
39
+ metaPixelId: null,
40
+ stripeAccountId: null,
41
+ paystackAccountId: null,
42
+ paymentProviderName: null,
43
+ paymentProviderPublicKey: null,
44
+ stripePublishableKey: null,
45
+ siteName: "Nova Sound",
46
+ pwa: fullPwa(),
47
+ ...overrides,
48
+ };
49
+ }
50
+
51
+ function themedDoc(primary?: string): ContentDocument {
52
+ const fields: ContentDocument["fields"] = {};
53
+ if (primary !== undefined) fields["theme.primary"] = { type: "color", localized: false, value: primary };
54
+ return { version: 1, defaultLocale: "en", locales: ["en"], fields };
55
+ }
56
+
57
+ function metaFor(metas: HeadMetaT[], name: string): HeadMetaT | undefined {
58
+ return metas.find((m) => m.name === name);
59
+ }
60
+
61
+ describe("buildPwaHead — happy path (full pwaConfig)", () => {
62
+ const head = buildPwaHead({ siteConfig: siteConfig(), contentDoc: themedDoc("#0a0a0a") });
63
+
64
+ it("emits the manifest link + apple-touch-icon with deep assertions", () => {
65
+ expect(head.links).toEqual([
66
+ { rel: "manifest", href: "/manifest.webmanifest" },
67
+ { rel: "apple-touch-icon", href: "https://cdn.tribenest.co/p/nova/icon-192.png", sizes: "192x192" },
68
+ ]);
69
+ });
70
+
71
+ it("emits theme-color from content theme.primary + the iOS standalone metas", () => {
72
+ expect(head.metas).toEqual([
73
+ { name: "theme-color", content: "#0a0a0a" },
74
+ { name: "mobile-web-app-capable", content: "yes" },
75
+ { name: "apple-mobile-web-app-capable", content: "yes" },
76
+ { name: "apple-mobile-web-app-status-bar-style", content: "default" },
77
+ { name: "apple-mobile-web-app-title", content: "Nova" },
78
+ ]);
79
+ });
80
+ });
81
+
82
+ describe("buildPwaHead — apple-touch-icon derivation", () => {
83
+ it("prefers icon192", () => {
84
+ const head = buildPwaHead({ siteConfig: siteConfig() });
85
+ const touch = head.links.find((l) => l.rel === "apple-touch-icon");
86
+ expect(touch?.href).toBe("https://cdn.tribenest.co/p/nova/icon-192.png");
87
+ });
88
+
89
+ it("falls back to icon512 when icon192 is empty", () => {
90
+ const head = buildPwaHead({ siteConfig: siteConfig({ pwa: fullPwa({ icon192: "" as unknown as string }) }) });
91
+ const touch = head.links.find((l) => l.rel === "apple-touch-icon");
92
+ expect(touch?.href).toBe("https://cdn.tribenest.co/p/nova/icon-512.png");
93
+ });
94
+
95
+ it("omits apple-touch-icon entirely when the profile has no pwaConfig", () => {
96
+ const head = buildPwaHead({ siteConfig: siteConfig({ pwa: null }) });
97
+ expect(head.links.some((l) => l.rel === "apple-touch-icon")).toBe(false);
98
+ // the manifest link is always present (installability nudge)
99
+ expect(head.links).toContainEqual({ rel: "manifest", href: "/manifest.webmanifest" });
100
+ });
101
+
102
+ it("omits apple-touch-icon when both icons are empty", () => {
103
+ const head = buildPwaHead({
104
+ siteConfig: siteConfig({ pwa: fullPwa({ icon192: "" as unknown as string, icon512: "" as unknown as string }) }),
105
+ });
106
+ expect(head.links.some((l) => l.rel === "apple-touch-icon")).toBe(false);
107
+ });
108
+ });
109
+
110
+ describe("buildPwaHead — theme-color derivation + fallback", () => {
111
+ it("falls back to the Forge default when the content doc has no theme.primary", () => {
112
+ const head = buildPwaHead({ siteConfig: siteConfig(), contentDoc: themedDoc() });
113
+ expect(metaFor(head.metas, "theme-color")?.content).toBe(DEFAULT_PRIMARY);
114
+ });
115
+
116
+ it("falls back to default when contentDoc is null / omitted", () => {
117
+ expect(metaFor(buildPwaHead({ siteConfig: siteConfig(), contentDoc: null }).metas, "theme-color")?.content).toBe(
118
+ DEFAULT_PRIMARY,
119
+ );
120
+ expect(metaFor(buildPwaHead({ siteConfig: siteConfig() }).metas, "theme-color")?.content).toBe(DEFAULT_PRIMARY);
121
+ });
122
+
123
+ it("resolves a localized theme.primary via defaultLocale", () => {
124
+ const doc: ContentDocument = {
125
+ version: 1,
126
+ defaultLocale: "de",
127
+ locales: ["de", "en"],
128
+ fields: { "theme.primary": { type: "color", localized: true, value: { en: "#111", de: "#999" } } },
129
+ };
130
+ expect(metaFor(buildPwaHead({ siteConfig: siteConfig(), contentDoc: doc }).metas, "theme-color")?.content).toBe(
131
+ "#999",
132
+ );
133
+ });
134
+ });
135
+
136
+ describe("buildPwaHead — manifestHref override", () => {
137
+ it("defaults the manifest link href to /manifest.webmanifest", () => {
138
+ const head = buildPwaHead({ siteConfig: siteConfig() });
139
+ expect(head.links[0]).toEqual({ rel: "manifest", href: "/manifest.webmanifest" });
140
+ });
141
+
142
+ it("honors an explicit manifestHref", () => {
143
+ const head = buildPwaHead({ siteConfig: siteConfig(), manifestHref: "/app.webmanifest" });
144
+ expect(head.links[0]).toEqual({ rel: "manifest", href: "/app.webmanifest" });
145
+ });
146
+ });
147
+
148
+ describe("buildPwaHead — apple-mobile-web-app-title fallback chain", () => {
149
+ it("uses shortName first", () => {
150
+ const head = buildPwaHead({ siteConfig: siteConfig({ pwa: fullPwa({ shortName: "Nv" }) }) });
151
+ expect(metaFor(head.metas, "apple-mobile-web-app-title")?.content).toBe("Nv");
152
+ });
153
+
154
+ it("falls back to pwa.name when shortName is empty", () => {
155
+ const head = buildPwaHead({ siteConfig: siteConfig({ pwa: fullPwa({ shortName: "" }) }) });
156
+ expect(metaFor(head.metas, "apple-mobile-web-app-title")?.content).toBe("Nova Sound");
157
+ });
158
+
159
+ it("falls back to siteName when there is no pwaConfig", () => {
160
+ const head = buildPwaHead({ siteConfig: siteConfig({ pwa: null, siteName: "Fallback Site" }) });
161
+ expect(metaFor(head.metas, "apple-mobile-web-app-title")?.content).toBe("Fallback Site");
162
+ });
163
+
164
+ it("falls back to 'App' when nothing is set", () => {
165
+ const head = buildPwaHead({ siteConfig: siteConfig({ pwa: null, siteName: null }) });
166
+ expect(metaFor(head.metas, "apple-mobile-web-app-title")?.content).toBe("App");
167
+ });
168
+ });
169
+
170
+ describe("buildPwaHead — degradation (never throws)", () => {
171
+ it("returns manifest link + default metas when siteConfig is null", () => {
172
+ const head = buildPwaHead({ siteConfig: null });
173
+ expect(head.links).toEqual([{ rel: "manifest", href: "/manifest.webmanifest" }]);
174
+ expect(metaFor(head.metas, "theme-color")?.content).toBe(DEFAULT_PRIMARY);
175
+ expect(metaFor(head.metas, "apple-mobile-web-app-title")?.content).toBe("App");
176
+ });
177
+
178
+ it("returns identical output for identical input (determinism)", () => {
179
+ const opts = { siteConfig: siteConfig(), contentDoc: themedDoc("#424242") };
180
+ expect(buildPwaHead(opts)).toEqual(buildPwaHead(opts));
181
+ });
182
+ });