@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,71 @@
1
+ import { useForge } from "../../provider/ForgeProvider";
2
+ import { useMutation } from "@tanstack/react-query";
3
+
4
+ /**
5
+ * The minimal cart-item shape the order endpoint needs. The full cart `CartItem`
6
+ * is a superset and assignable; lighter callers (e.g. a single-product offer)
7
+ * supply just these fields.
8
+ */
9
+ export type OrderCartItem = {
10
+ productId: string;
11
+ productVariantId: string;
12
+ title: string;
13
+ price: number;
14
+ quantity: number;
15
+ coverImage?: string;
16
+ isGift?: boolean;
17
+ recipientName?: string;
18
+ recipientEmail?: string;
19
+ recipientMessage?: string;
20
+ };
21
+
22
+ export type CreateOrderShippingRate = {
23
+ groupId: string;
24
+ type: "flat" | "dynamic" | "external";
25
+ productIds: string[];
26
+ rateId: string;
27
+ shipmentId: string;
28
+ amount: number;
29
+ currency?: string;
30
+ carrier: string;
31
+ service: string;
32
+ };
33
+
34
+ export type CreateOrderInput = {
35
+ amount: number;
36
+ email: string;
37
+ firstName?: string;
38
+ lastName?: string;
39
+ accountId?: string;
40
+ cartItems: OrderCartItem[];
41
+ shippingAddress?: {
42
+ address1: string;
43
+ city: string;
44
+ stateCode: string;
45
+ countryCode: string;
46
+ zip: string;
47
+ };
48
+ couponCode?: string;
49
+ selectedShippingRates?: CreateOrderShippingRate[];
50
+ joinEmailList?: boolean;
51
+ attributionRefId?: string;
52
+ };
53
+
54
+ export type CreateOrderResult = {
55
+ orderId: string;
56
+ totalAmount: number;
57
+ subTotal: number;
58
+ shippingCosts?: { deliveryGroupId: string; amount: number; currency: string }[];
59
+ };
60
+
61
+ /** Create a cart/product order (the step before start-payment). */
62
+ export function useCreateOrder() {
63
+ const { client, profileId } = useForge();
64
+
65
+ return useMutation<CreateOrderResult, unknown, CreateOrderInput>({
66
+ mutationFn: async (input) => {
67
+ const res = await client.post("/public/orders", { ...input, profileId });
68
+ return res.data;
69
+ },
70
+ });
71
+ }
@@ -0,0 +1,101 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+
3
+ /**
4
+ * Headless PWA install-prompt hook — the versioned Forge primitive that powers
5
+ * `<InstallBanner/>` (forge/ui) and any custom install UI on a code site. Ported
6
+ * from apps/client/src/lib/hooks/usePWA.ts, plus iOS + standalone detection
7
+ * (Safari never fires `beforeinstallprompt`, so iOS needs the manual
8
+ * "Share → Add to Home Screen" path).
9
+ *
10
+ * const { isInstallable, isInstalled, isIOS, isStandalone, promptInstall } = useInstallPrompt();
11
+ *
12
+ * SSR-safe: everything runs inside effects, so the initial server render is a
13
+ * stable "nothing installable/installed" state.
14
+ */
15
+ interface BeforeInstallPromptEvent extends Event {
16
+ prompt(): Promise<void>;
17
+ userChoice: Promise<{ outcome: "accepted" | "dismissed" }>;
18
+ }
19
+
20
+ export interface InstallPromptState {
21
+ /** A `beforeinstallprompt` event was captured (Android/desktop Chromium). */
22
+ isInstallable: boolean;
23
+ /** The app is running installed (standalone display-mode or iOS standalone). */
24
+ isInstalled: boolean;
25
+ /** iOS Safari — no `beforeinstallprompt`; show the Add-to-Home-Screen guide. */
26
+ isIOS: boolean;
27
+ /** Currently rendered in standalone (installed) display mode. */
28
+ isStandalone: boolean;
29
+ /** Fire the native install prompt; resolves true if the user accepted. */
30
+ promptInstall: () => Promise<boolean>;
31
+ }
32
+
33
+ function detectIOS(): boolean {
34
+ if (typeof navigator === "undefined") return false;
35
+ const ua = navigator.userAgent || "";
36
+ const iOSDevice = /iPad|iPhone|iPod/.test(ua);
37
+ // iPadOS 13+ reports as Mac; detect via touch points.
38
+ const iPadOS = navigator.platform === "MacIntel" && (navigator as { maxTouchPoints?: number }).maxTouchPoints! > 1;
39
+ return iOSDevice || iPadOS;
40
+ }
41
+
42
+ function detectStandalone(): boolean {
43
+ if (typeof window === "undefined") return false;
44
+ const mm = window.matchMedia && window.matchMedia("(display-mode: standalone)").matches;
45
+ // iOS Safari exposes navigator.standalone instead of the media query.
46
+ const iosStandalone = (window.navigator as { standalone?: boolean }).standalone === true;
47
+ return !!mm || iosStandalone;
48
+ }
49
+
50
+ export function useInstallPrompt(): InstallPromptState {
51
+ const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null);
52
+ const [isInstallable, setIsInstallable] = useState(false);
53
+ const [isInstalled, setIsInstalled] = useState(false);
54
+ const [isIOS, setIsIOS] = useState(false);
55
+ const [isStandalone, setIsStandalone] = useState(false);
56
+
57
+ useEffect(() => {
58
+ const ios = detectIOS();
59
+ const standalone = detectStandalone();
60
+ setIsIOS(ios);
61
+ setIsStandalone(standalone);
62
+ if (standalone) setIsInstalled(true);
63
+
64
+ const handleBeforeInstallPrompt = (e: Event) => {
65
+ e.preventDefault();
66
+ setDeferredPrompt(e as BeforeInstallPromptEvent);
67
+ setIsInstallable(true);
68
+ };
69
+ const handleAppInstalled = () => {
70
+ setIsInstalled(true);
71
+ setIsInstallable(false);
72
+ setDeferredPrompt(null);
73
+ };
74
+
75
+ window.addEventListener("beforeinstallprompt", handleBeforeInstallPrompt);
76
+ window.addEventListener("appinstalled", handleAppInstalled);
77
+ return () => {
78
+ window.removeEventListener("beforeinstallprompt", handleBeforeInstallPrompt);
79
+ window.removeEventListener("appinstalled", handleAppInstalled);
80
+ };
81
+ }, []);
82
+
83
+ const promptInstall = useCallback(async () => {
84
+ if (!deferredPrompt) return false;
85
+ try {
86
+ await deferredPrompt.prompt();
87
+ const { outcome } = await deferredPrompt.userChoice;
88
+ if (outcome === "accepted") {
89
+ setIsInstalled(true);
90
+ setIsInstallable(false);
91
+ setDeferredPrompt(null);
92
+ return true;
93
+ }
94
+ return false;
95
+ } catch {
96
+ return false;
97
+ }
98
+ }, [deferredPrompt]);
99
+
100
+ return { isInstallable, isInstalled, isIOS, isStandalone, promptInstall };
101
+ }
@@ -0,0 +1,36 @@
1
+ import { useForge } from "../../provider/ForgeProvider";
2
+ import { useQuery } from "@tanstack/react-query";
3
+
4
+ export type PageActionType =
5
+ | "newsletter"
6
+ | "lead_magnet"
7
+ | "offer"
8
+ | "product_cards"
9
+ | "donation"
10
+ | "membership"
11
+ | "button";
12
+
13
+ export interface PageActionDescriptor {
14
+ id: string;
15
+ type: PageActionType;
16
+ config: Record<string, unknown>;
17
+ }
18
+
19
+ /**
20
+ * Resolves the page-action descriptors for a page slot (server applies the
21
+ * per-entity-override-vs-default logic). The shared <PageActions> renderer maps
22
+ * each descriptor to its Forge primitive.
23
+ */
24
+ export function usePageActions(pageType: string, entityId?: string, placement = "after_content") {
25
+ const { client, profileId } = useForge();
26
+ return useQuery<{ actions: PageActionDescriptor[] }>({
27
+ queryKey: ["page-actions", profileId, pageType, placement, entityId],
28
+ queryFn: async () => {
29
+ const res = await client.get("/public/page-actions", {
30
+ params: { profileId, pageType, placement, entityId },
31
+ });
32
+ return res.data;
33
+ },
34
+ enabled: !!client && !!profileId && !!pageType,
35
+ });
36
+ }
@@ -0,0 +1,80 @@
1
+ import { useEffect, useRef } from "react";
2
+ import type { PaymentFlowResult, PaymentStartResponse } from "../../types/models";
3
+ import { PaymentProviderName } from "../../types/models";
4
+ import { useForge } from "../../provider/ForgeProvider";
5
+ import { useMutation } from "@tanstack/react-query";
6
+
7
+ export interface UsePaymentFlowOptions {
8
+ /** The `start-payment` endpoint for this domain (resource already created). */
9
+ path?: string;
10
+ /** Domain-specific body (orderId / bookingId / donation fields, etc.). */
11
+ body?: Record<string, unknown>;
12
+ /** Where the provider should return the buyer after payment. */
13
+ returnUrl?: string;
14
+ /** Fire automatically once path/returnUrl/profile are ready (default true). */
15
+ autoStart?: boolean;
16
+ /** Gate auto-start on extra readiness (default true). */
17
+ enabled?: boolean;
18
+ /** Redirect to the Paystack checkout URL on success (default true). */
19
+ paystackRedirect?: boolean;
20
+ }
21
+
22
+ /**
23
+ * Starts a payment against any `/public/<domain>/.../start-payment` endpoint and
24
+ * normalizes the result: resolves the provider (the field is named differently
25
+ * per domain) and, for Paystack, redirects to the hosted checkout. The Stripe
26
+ * UI (Elements + confirm) stays in the app — this hook only owns the data.
27
+ */
28
+ export function usePaymentFlow(opts: UsePaymentFlowOptions) {
29
+ const { client, profileId } = useForge();
30
+ const { path, body, returnUrl, autoStart = true, enabled = true, paystackRedirect = true } = opts;
31
+ const startedRef = useRef(false);
32
+
33
+ const mutation = useMutation<PaymentFlowResult, unknown, Record<string, unknown> | undefined>({
34
+ mutationFn: async (override) => {
35
+ // override wins — click-driven flows pass a just-created orderId/returnUrl.
36
+ const res = await client.post(path as string, { ...body, profileId, returnUrl, ...(override ?? {}) });
37
+ const data = res.data as PaymentStartResponse;
38
+ const provider = (data.paymentProvider ?? data.paymentProviderName) as PaymentProviderName | undefined;
39
+
40
+ if (
41
+ paystackRedirect &&
42
+ provider === PaymentProviderName.Paystack &&
43
+ data.paymentSecret &&
44
+ typeof window !== "undefined"
45
+ ) {
46
+ window.location.href = data.paymentSecret;
47
+ }
48
+
49
+ return { ...data, provider };
50
+ },
51
+ });
52
+
53
+ const { mutate, mutateAsync } = mutation;
54
+
55
+ useEffect(() => {
56
+ if (autoStart && enabled && path && returnUrl && profileId && !startedRef.current) {
57
+ startedRef.current = true;
58
+ mutate(undefined);
59
+ }
60
+ // eslint-disable-next-line react-hooks/exhaustive-deps
61
+ }, [autoStart, enabled, path, returnUrl, profileId]);
62
+
63
+ return {
64
+ result: mutation.data ?? null,
65
+ clientSecret: mutation.data?.paymentSecret,
66
+ paymentId: mutation.data?.paymentId,
67
+ provider: mutation.data?.provider,
68
+ isStarting: mutation.isPending,
69
+ error: mutation.error,
70
+ /**
71
+ * Manually start (or restart) the payment — for click-driven flows. Pass a
72
+ * body override (e.g. a freshly-created orderId/returnUrl) that wins over
73
+ * the hook's static options.
74
+ */
75
+ start: (override?: Record<string, unknown>) => {
76
+ startedRef.current = true;
77
+ return mutateAsync(override);
78
+ },
79
+ };
80
+ }
@@ -0,0 +1,47 @@
1
+ import type { PaymentLinkData } from "../../types/models";
2
+ import { useForge } from "../../provider/ForgeProvider";
3
+ import { useQuery } from "@tanstack/react-query";
4
+
5
+ /** A single public payment link by id. */
6
+ export function usePaymentLink(paymentLinkId?: string) {
7
+ const { client, profileId } = useForge();
8
+
9
+ return useQuery<PaymentLinkData>({
10
+ queryKey: ["payment-link", paymentLinkId, profileId],
11
+ queryFn: async () => {
12
+ const res = await client.get(`/public/payment-links/${paymentLinkId}`, {
13
+ params: { profileId },
14
+ });
15
+ return res.data;
16
+ },
17
+ enabled: !!profileId && !!client && !!paymentLinkId,
18
+ });
19
+ }
20
+
21
+ export type PaymentLinkFinalizeResult = {
22
+ id: string;
23
+ recipientName: string;
24
+ recipientEmail: string;
25
+ amount: string;
26
+ currency: string;
27
+ message: string | null;
28
+ status: string;
29
+ paidAt: string | null;
30
+ profileName: string;
31
+ };
32
+
33
+ /** Finalize a payment-link payment on the return page (idempotent reconcile). */
34
+ export function usePaymentLinkFinalize(paymentLinkId?: string) {
35
+ const { client, profileId } = useForge();
36
+
37
+ return useQuery<PaymentLinkFinalizeResult>({
38
+ queryKey: ["payment-link-finalize", paymentLinkId, profileId],
39
+ queryFn: async () => {
40
+ const res = await client.post(`/public/payment-links/${paymentLinkId}/finalize`, {
41
+ profileId,
42
+ });
43
+ return res.data;
44
+ },
45
+ enabled: !!profileId && !!client && !!paymentLinkId,
46
+ });
47
+ }
@@ -0,0 +1,85 @@
1
+ "use client";
2
+ import type { BlogPost, BlogCategory, PodcastShow } from "../../types/models";
3
+ import type { AudioTrack } from "../../contexts/AudioPlayerContext";
4
+ import { useForge } from "../../provider/ForgeProvider";
5
+ import { useBlogCategory, useBlogCategoryPosts } from "./useBlog";
6
+ import { useQuery } from "@tanstack/react-query";
7
+
8
+ // A podcast is a podcast-type blog category; an episode is one of its audio posts.
9
+ // These hooks compose the existing blog hooks so the whole thing rides the proven
10
+ // blog + audio harness.
11
+
12
+ /** The audio file of an episode (the first audio media), or undefined. */
13
+ export function episodeAudio(post: BlogPost): { url: string; durationSec?: number | null } | undefined {
14
+ const audio = post.media?.find((m) => (m.type || "").startsWith("audio"));
15
+ return audio ? { url: audio.url, durationSec: audio.durationSec ?? null } : undefined;
16
+ }
17
+
18
+ /** True when a post is a real episode (has an audio file). */
19
+ export function isEpisode(post: BlogPost): boolean {
20
+ return !!episodeAudio(post);
21
+ }
22
+
23
+ /** Map an episode to an AudioTrack for the shared AudioPlayer / queue. */
24
+ export function episodeToTrack(post: BlogPost, show?: { title?: string; author?: string | null; coverImage?: { url: string } | null } | null): AudioTrack | null {
25
+ const audio = episodeAudio(post);
26
+ if (!audio) return null;
27
+ return {
28
+ id: post.id,
29
+ title: post.title,
30
+ artist: show?.author ?? show?.title ?? undefined,
31
+ coverImage: show?.coverImage?.url ?? undefined,
32
+ url: audio.url,
33
+ duration: audio.durationSec ?? undefined,
34
+ };
35
+ }
36
+
37
+ /** All podcast shows for the profile (newest first). */
38
+ export function usePodcasts(options?: { initialData?: PodcastShow[] }) {
39
+ const { client, profileId } = useForge();
40
+ return useQuery<PodcastShow[]>({
41
+ queryKey: ["podcasts-public", profileId],
42
+ queryFn: async () => {
43
+ const res = await client.get(`/public/blog/posts/podcasts`, { params: { profileId } });
44
+ return res.data;
45
+ },
46
+ enabled: !!profileId && !!client,
47
+ initialData: options?.initialData,
48
+ });
49
+ }
50
+
51
+ export interface UsePodcastResult {
52
+ show: BlogCategory | null;
53
+ episodes: BlogPost[];
54
+ isLoading: boolean;
55
+ }
56
+
57
+ /** A single podcast show (by slug) + its episodes (audio posts), in feed order. */
58
+ export function usePodcast(
59
+ slug?: string,
60
+ options?: { initialShow?: BlogCategory | null; initialEpisodes?: BlogPost[] },
61
+ ): UsePodcastResult {
62
+ const showQ = useBlogCategory(slug, { initialData: options?.initialShow });
63
+ const postsQ = useBlogCategoryPosts(slug, { page: 1, limit: 100 });
64
+
65
+ const rawPosts = postsQ.data?.data ?? options?.initialEpisodes ?? [];
66
+ const episodes = rawPosts.filter(isEpisode);
67
+
68
+ return {
69
+ show: showQ.data ?? options?.initialShow ?? null,
70
+ episodes,
71
+ isLoading: showQ.isLoading || (postsQ.isLoading && !options?.initialEpisodes),
72
+ };
73
+ }
74
+
75
+ /** Locate one episode within a show by slug, with its neighbours for prev/next. */
76
+ export function findEpisode(episodes: BlogPost[], episodeSlug?: string) {
77
+ const index = episodeSlug ? episodes.findIndex((e) => e.slug === episodeSlug) : -1;
78
+ const episode = index >= 0 ? episodes[index] : null;
79
+ return {
80
+ episode,
81
+ index,
82
+ previous: index > 0 ? episodes[index - 1] : null,
83
+ next: index >= 0 && index < episodes.length - 1 ? episodes[index + 1] : null,
84
+ };
85
+ }
@@ -0,0 +1,48 @@
1
+ import type { IPublicPostCollection, PaginatedData } from "../../types/models";
2
+ import { useForge } from "../../provider/ForgeProvider";
3
+ import { useQuery } from "@tanstack/react-query";
4
+
5
+ export interface GetPostCollectionsParams {
6
+ query?: string;
7
+ page?: number;
8
+ }
9
+
10
+ export function useGetPostCollections(params?: GetPostCollectionsParams) {
11
+ const { client, profileId } = useForge();
12
+
13
+ return useQuery<PaginatedData<IPublicPostCollection>>({
14
+ queryKey: ["post-collections", profileId, params],
15
+ queryFn: async () => {
16
+ const res = await client.get("/public/post-collections", {
17
+ params: {
18
+ profileId: profileId,
19
+ page: params?.page || 1,
20
+ limit: 10,
21
+ filter: {
22
+ query: params?.query,
23
+ },
24
+ },
25
+ });
26
+ return res.data;
27
+ },
28
+ enabled: !!profileId && !!client,
29
+ });
30
+ }
31
+
32
+ export function useGetPostCollection(collectionId?: string) {
33
+ const { client, profileId } = useForge();
34
+
35
+ return useQuery<IPublicPostCollection>({
36
+ queryKey: ["post-collection", profileId, collectionId],
37
+ queryFn: async () => {
38
+ const res = await client.get(`/public/post-collections/${collectionId}`, {
39
+ params: {
40
+ profileId: profileId,
41
+ },
42
+ });
43
+ return res.data;
44
+ },
45
+ enabled: !!profileId && !!client && !!collectionId,
46
+ });
47
+ }
48
+
@@ -0,0 +1,94 @@
1
+ import type { IPublicPost, PaginatedData } from "../../types/models";
2
+ import { useForge } from "../../provider/ForgeProvider";
3
+ import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
4
+
5
+ export interface GetPostsParams {
6
+ query?: string;
7
+ type?: string;
8
+ membershipTierId?: string;
9
+ page?: number;
10
+ }
11
+
12
+ export function useGetPosts(params?: GetPostsParams) {
13
+ const { client, profileId } = useForge();
14
+
15
+ return useQuery<PaginatedData<IPublicPost>>({
16
+ queryKey: ["posts", profileId, params],
17
+ queryFn: async () => {
18
+ const res = await client.get("/public/posts", {
19
+ params: {
20
+ profileId: profileId,
21
+ page: params?.page || 1,
22
+ limit: 10,
23
+ filter: {
24
+ query: params?.query,
25
+ type: params?.type,
26
+ membershipTierId: params?.membershipTierId,
27
+ },
28
+ },
29
+ });
30
+ return res.data;
31
+ },
32
+ enabled: !!profileId && !!client,
33
+ });
34
+ }
35
+
36
+ export interface GetPostsInfiniteParams {
37
+ query?: string;
38
+ type?: string;
39
+ membershipTierId?: string;
40
+ pageSize?: number;
41
+ }
42
+
43
+ /**
44
+ * Infinite (load-more) variant of `useGetPosts` for the member posts feed.
45
+ * Same gated endpoint: the server computes per-post `hasAccess` and never
46
+ * ships media for locked posts, so paging is safe for any auth state.
47
+ */
48
+ export function useGetPostsInfinite(params?: GetPostsInfiniteParams) {
49
+ const { client, profileId } = useForge();
50
+ const limit = params?.pageSize ?? 10;
51
+
52
+ return useInfiniteQuery<PaginatedData<IPublicPost>>({
53
+ queryKey: [
54
+ "posts-infinite",
55
+ profileId,
56
+ { query: params?.query, type: params?.type, membershipTierId: params?.membershipTierId, limit },
57
+ ],
58
+ queryFn: async ({ pageParam = 1 }) => {
59
+ const res = await client.get("/public/posts", {
60
+ params: {
61
+ profileId: profileId,
62
+ page: pageParam,
63
+ limit,
64
+ filter: {
65
+ query: params?.query,
66
+ type: params?.type,
67
+ membershipTierId: params?.membershipTierId,
68
+ },
69
+ },
70
+ });
71
+ return res.data;
72
+ },
73
+ initialPageParam: 1,
74
+ getNextPageParam: (lastPage) => (lastPage.hasNextPage ? (lastPage.nextPage ?? lastPage.page + 1) : undefined),
75
+ enabled: !!profileId && !!client,
76
+ });
77
+ }
78
+
79
+ /**
80
+ * The member's saved posts (GET /public/posts/saved — requires auth). The
81
+ * endpoint returns a single `{ data, total }` page.
82
+ */
83
+ export function useGetSavedPosts(enabled = true) {
84
+ const { client, profileId } = useForge();
85
+
86
+ return useQuery<{ data: IPublicPost[]; total: number }>({
87
+ queryKey: ["posts-saved", profileId],
88
+ queryFn: async () => {
89
+ const res = await client.get("/public/posts/saved", { params: { profileId } });
90
+ return res.data;
91
+ },
92
+ enabled: !!profileId && !!client && enabled,
93
+ });
94
+ }
@@ -0,0 +1,108 @@
1
+ import type { IPublicProduct, PaginatedData, ProductCategory } from "../../types/models";
2
+ import { useForge } from "../../provider/ForgeProvider";
3
+ import { useQuery } from "@tanstack/react-query";
4
+
5
+ export interface GetProductsParams {
6
+ query?: string;
7
+ category?: ProductCategory[];
8
+ page?: number;
9
+ releaseType?: string;
10
+ }
11
+
12
+ /** Featured products for the profile, optionally filtered by category. */
13
+ export function useFeaturedProducts(category?: ProductCategory) {
14
+ const { client, profileId } = useForge();
15
+
16
+ return useQuery<IPublicProduct[]>({
17
+ queryKey: ["featured-products", profileId, category],
18
+ queryFn: async () => {
19
+ const res = await client.get("/public/products/featured", {
20
+ params: { profileId, category },
21
+ });
22
+ return res.data;
23
+ },
24
+ enabled: !!profileId && !!client,
25
+ });
26
+ }
27
+
28
+ export function useGetProducts(params?: GetProductsParams, enabled = true) {
29
+ const { client, profileId } = useForge();
30
+
31
+ return useQuery<PaginatedData<IPublicProduct>>({
32
+ queryKey: ["products", profileId, params],
33
+ queryFn: async () => {
34
+ const res = await client.get("/public/products", {
35
+ params: {
36
+ profileId: profileId,
37
+ category: params?.category,
38
+ page: params?.page || 1,
39
+ limit: 10,
40
+ filter: {
41
+ query: params?.query,
42
+ releaseType: params?.releaseType,
43
+ },
44
+ },
45
+ });
46
+ return res.data;
47
+ },
48
+ enabled: !!profileId && !!client && enabled,
49
+ });
50
+ }
51
+
52
+ export function useGetProduct(productId?: string, options?: { initialData?: IPublicProduct }) {
53
+ const { client, profileId } = useForge();
54
+
55
+ return useQuery<IPublicProduct>({
56
+ queryKey: ["products", profileId, { productId }],
57
+ queryFn: async () => {
58
+ const res = await client.get(`/public/products/${productId}`, {
59
+ params: {
60
+ profileId: profileId,
61
+ },
62
+ });
63
+ return res.data;
64
+ },
65
+ enabled: !!profileId && !!client && !!productId,
66
+ initialData: options?.initialData,
67
+ });
68
+ }
69
+
70
+ export function useGetProductsByIds(productIds: string[]) {
71
+ const { client, profileId } = useForge();
72
+ const idsKey = productIds.join(",");
73
+
74
+ return useQuery<IPublicProduct[]>({
75
+ queryKey: ["products", "batch", profileId, idsKey],
76
+ queryFn: async () => {
77
+ const res = await client.get("/public/products/batch", {
78
+ params: {
79
+ profileId: profileId,
80
+ ids: idsKey,
81
+ },
82
+ });
83
+ // Preserve the order of the requested IDs
84
+ const data = res.data as IPublicProduct[];
85
+ return productIds
86
+ .map((id) => data.find((p) => p.id === id))
87
+ .filter((p): p is IPublicProduct => p != null);
88
+ },
89
+ enabled: !!profileId && !!client && productIds.length > 0,
90
+ });
91
+ }
92
+
93
+ export function useGetProductCategories() {
94
+ const { client, profileId } = useForge();
95
+
96
+ return useQuery<{ title: ProductCategory; description: string }[]>({
97
+ queryKey: ["product-categories", profileId],
98
+ queryFn: async () => {
99
+ const res = await client.get("/public/products/categories", {
100
+ params: {
101
+ profileId: profileId,
102
+ },
103
+ });
104
+ return res.data;
105
+ },
106
+ enabled: !!profileId && !!client,
107
+ });
108
+ }
@@ -0,0 +1,19 @@
1
+ import type { ReplayData } from "../../types/models";
2
+ import { useForge } from "../../provider/ForgeProvider";
3
+ import { useQuery } from "@tanstack/react-query";
4
+
5
+ /** Replay broadcasts for an event ticket id. */
6
+ export function useReplayBroadcasts(ticketId?: string | null) {
7
+ const { client, profileId } = useForge();
8
+
9
+ return useQuery<ReplayData>({
10
+ queryKey: ["replay-broadcasts", ticketId],
11
+ queryFn: async () => {
12
+ const res = await client.get(`/public/broadcasts/replay/${ticketId}`, {
13
+ params: { profileId },
14
+ });
15
+ return res.data;
16
+ },
17
+ enabled: !!client && !!ticketId && !!profileId,
18
+ });
19
+ }