@tribe-nest/forge 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +42 -0
- package/src/client/createForgeClient.ts +54 -0
- package/src/content/ForgeContentProvider.tsx +118 -0
- package/src/content/ForgeEditBridge.tsx +134 -0
- package/src/content/editFlag.ts +23 -0
- package/src/content/index.ts +28 -0
- package/src/content/resolve.ts +67 -0
- package/src/content/types.ts +97 -0
- package/src/content/useField.ts +64 -0
- package/src/contexts/AppAuthContext.tsx +185 -0
- package/src/contexts/AudioPlayerContext.tsx +389 -0
- package/src/contexts/CartContext.tsx +131 -0
- package/src/contexts/PublicAuthContext.tsx +333 -0
- package/src/contexts/useAppAdminGuard.ts +56 -0
- package/src/css.d.ts +5 -0
- package/src/data/collectionParams.ts +57 -0
- package/src/data/queries/useAccount.ts +71 -0
- package/src/data/queries/useAccountSettings.ts +82 -0
- package/src/data/queries/useAnalytics.ts +86 -0
- package/src/data/queries/useAuthActions.ts +28 -0
- package/src/data/queries/useBlog.ts +103 -0
- package/src/data/queries/useBlogComments.ts +96 -0
- package/src/data/queries/useBroadcasts.ts +31 -0
- package/src/data/queries/useCertificates.ts +58 -0
- package/src/data/queries/useChat.ts +267 -0
- package/src/data/queries/useCoachingAvailability.ts +69 -0
- package/src/data/queries/useCoachingProducts.ts +67 -0
- package/src/data/queries/useCohortPage.ts +17 -0
- package/src/data/queries/useCollections.ts +211 -0
- package/src/data/queries/useCommunity.ts +794 -0
- package/src/data/queries/useCoupons.ts +20 -0
- package/src/data/queries/useCourseAccess.ts +50 -0
- package/src/data/queries/useCourses.ts +91 -0
- package/src/data/queries/useCurrencies.ts +33 -0
- package/src/data/queries/useDocuments.ts +208 -0
- package/src/data/queries/useDonations.ts +29 -0
- package/src/data/queries/useEngagement.ts +115 -0
- package/src/data/queries/useEvents.ts +69 -0
- package/src/data/queries/useFinalize.ts +109 -0
- package/src/data/queries/useForms.ts +31 -0
- package/src/data/queries/useInvoice.ts +65 -0
- package/src/data/queries/useLeadMagnet.ts +96 -0
- package/src/data/queries/useMembership.ts +20 -0
- package/src/data/queries/useNotifications.ts +267 -0
- package/src/data/queries/useOffers.ts +56 -0
- package/src/data/queries/useOrders.ts +71 -0
- package/src/data/queries/usePWA.ts +101 -0
- package/src/data/queries/usePageActions.ts +36 -0
- package/src/data/queries/usePaymentFlow.ts +80 -0
- package/src/data/queries/usePaymentLink.ts +47 -0
- package/src/data/queries/usePodcast.ts +85 -0
- package/src/data/queries/usePostCollections.ts +48 -0
- package/src/data/queries/usePosts.ts +94 -0
- package/src/data/queries/useProducts.ts +108 -0
- package/src/data/queries/useReplay.ts +19 -0
- package/src/data/queries/useReviews.ts +122 -0
- package/src/data/queries/useShipping.ts +109 -0
- package/src/data/queries/useSubscriptions.ts +66 -0
- package/src/data/queries/useWebsite.ts +128 -0
- package/src/data/queries/useWebsiteAgent.ts +100 -0
- package/src/index.ts +98 -0
- package/src/provider/ForgeAppProvider.tsx +149 -0
- package/src/provider/ForgeProvider.tsx +91 -0
- package/src/provider/SiteConfigProvider.tsx +27 -0
- package/src/server/_tests/buildPwaHead.spec.ts +182 -0
- package/src/server/_tests/buildWebManifest.spec.ts +313 -0
- package/src/server/_tests/forgeServiceWorkerScript.spec.ts +251 -0
- package/src/server/index.ts +260 -0
- package/src/server/jobs.ts +104 -0
- package/src/server/pwa.ts +332 -0
- package/src/types/index.ts +74 -0
- package/src/types/models.ts +1313 -0
- package/src/ui/analytics/ForgeAnalytics.tsx +208 -0
- package/src/ui/analytics/PageMetaPixel.tsx +92 -0
- package/src/ui/content/Editable.tsx +90 -0
- package/src/ui/format/PriceDisplay.tsx +142 -0
- package/src/ui/format/useFormatCurrency.ts +92 -0
- package/src/ui/headless/agent/useAiAgent.ts +106 -0
- package/src/ui/headless/auth/useLoginFlow.ts +72 -0
- package/src/ui/headless/auth/useSignupForm.ts +85 -0
- package/src/ui/headless/broadcast/useBroadcastWatch.ts +59 -0
- package/src/ui/headless/chat/attachmentKind.ts +27 -0
- package/src/ui/headless/chat/useChatAttachmentUpload.ts +132 -0
- package/src/ui/headless/chat/useChatRoom.ts +223 -0
- package/src/ui/headless/chat/useMessageThread.ts +60 -0
- package/src/ui/headless/checkout/useCheckout.ts +438 -0
- package/src/ui/headless/coaching/useCoachingBooking.ts +223 -0
- package/src/ui/headless/community/index.ts +20 -0
- package/src/ui/headless/community/lexical.ts +131 -0
- package/src/ui/headless/community/replyTree.ts +51 -0
- package/src/ui/headless/community/richLexical.ts +314 -0
- package/src/ui/headless/community/useCommunityAttachmentUpload.ts +137 -0
- package/src/ui/headless/community/useCommunitySpaceFeed.ts +122 -0
- package/src/ui/headless/community/useCommunityThread.ts +95 -0
- package/src/ui/headless/community/useMentionComposer.ts +105 -0
- package/src/ui/headless/community/useRichMentionComposer.ts +193 -0
- package/src/ui/headless/consent/useCookieConsent.ts +81 -0
- package/src/ui/headless/course/useCourseCheckout.ts +99 -0
- package/src/ui/headless/dialog.tsx +14 -0
- package/src/ui/headless/document/index.ts +7 -0
- package/src/ui/headless/document/useContractSignature.ts +68 -0
- package/src/ui/headless/document/useDocumentSigningFlow.ts +92 -0
- package/src/ui/headless/document/useOtpCodeInput.ts +86 -0
- package/src/ui/headless/document/useSignaturePad.ts +180 -0
- package/src/ui/headless/donation/Donation.tsx +115 -0
- package/src/ui/headless/donation/DonationContext.tsx +223 -0
- package/src/ui/headless/donation/fees.ts +15 -0
- package/src/ui/headless/donation/index.ts +10 -0
- package/src/ui/headless/engagement/usePostEngagement.ts +119 -0
- package/src/ui/headless/event/useEventCheckout.ts +141 -0
- package/src/ui/headless/forms/useContactForm.ts +58 -0
- package/src/ui/headless/forms/useEmailListForm.ts +75 -0
- package/src/ui/headless/forms/useSectionedForm.ts +256 -0
- package/src/ui/headless/index.ts +60 -0
- package/src/ui/headless/invoice/useInvoicePayment.ts +48 -0
- package/src/ui/headless/memberHome/index.ts +7 -0
- package/src/ui/headless/memberHome/useMemberHomeNav.ts +82 -0
- package/src/ui/headless/membership/MembershipGate.tsx +39 -0
- package/src/ui/headless/membership/useMembershipCheckout.ts +121 -0
- package/src/ui/headless/offer/Offer.tsx +88 -0
- package/src/ui/headless/offer/OfferContext.tsx +162 -0
- package/src/ui/headless/offer/index.ts +8 -0
- package/src/ui/headless/paymentLink/usePaymentLinkPayment.ts +46 -0
- package/src/ui/headless/podcast/usePodcastPlayer.ts +96 -0
- package/src/ui/headless/posts/index.ts +13 -0
- package/src/ui/headless/posts/upsell.ts +41 -0
- package/src/ui/headless/posts/useMemberPostsFeed.ts +110 -0
- package/src/ui/headless/posts/usePostCollectionDetail.ts +56 -0
- package/src/ui/headless/posts/usePostCollectionsList.ts +90 -0
- package/src/ui/headless/posts/usePostItem.ts +81 -0
- package/src/ui/headless/reviews/index.ts +6 -0
- package/src/ui/headless/reviews/useHelpfulVote.ts +45 -0
- package/src/ui/headless/reviews/useReviewForm.ts +108 -0
- package/src/ui/headless/reviews/useReviewsList.ts +79 -0
- package/src/ui/headless/work/index.ts +25 -0
- package/src/ui/headless/work/useWorkPortal.ts +349 -0
- package/src/ui/index.ts +144 -0
- package/src/ui/payment/ForgePaymentProvider.tsx +45 -0
- package/src/ui/payment/ForgeStripePayment.tsx +171 -0
- package/src/ui/shell/TribeNestApp.tsx +121 -0
- package/src/ui/shell/shellGating.spec.ts +28 -0
- package/src/ui/shell/shellGating.ts +11 -0
- package/src/ui/styled/AccountDashboard.tsx +575 -0
- package/src/ui/styled/AiAgentWidget.tsx +152 -0
- package/src/ui/styled/AudioPlayer.tsx +123 -0
- package/src/ui/styled/BlogCategory.tsx +79 -0
- package/src/ui/styled/BlogComments.tsx +236 -0
- package/src/ui/styled/BlogList.tsx +101 -0
- package/src/ui/styled/BlogPost.tsx +160 -0
- package/src/ui/styled/BlogPostFooter.tsx +33 -0
- package/src/ui/styled/BotProtection.tsx +101 -0
- package/src/ui/styled/Button.tsx +88 -0
- package/src/ui/styled/Cart.tsx +276 -0
- package/src/ui/styled/ChatRoom.tsx +252 -0
- package/src/ui/styled/Checkout.tsx +988 -0
- package/src/ui/styled/CheckoutConfirmation.tsx +452 -0
- package/src/ui/styled/CoachingBooking.tsx +605 -0
- package/src/ui/styled/CoachingConfirmation.tsx +151 -0
- package/src/ui/styled/CoachingDetail.tsx +99 -0
- package/src/ui/styled/CohortPage.tsx +82 -0
- package/src/ui/styled/ConfirmSubscription.tsx +134 -0
- package/src/ui/styled/Confirmation.tsx +171 -0
- package/src/ui/styled/ContactForm.tsx +81 -0
- package/src/ui/styled/CookieConsent.tsx +239 -0
- package/src/ui/styled/CourseAccess.tsx +169 -0
- package/src/ui/styled/CourseCheckout.tsx +425 -0
- package/src/ui/styled/CourseConfirmation.tsx +191 -0
- package/src/ui/styled/CourseDetail.tsx +140 -0
- package/src/ui/styled/CoursesGrid.tsx +85 -0
- package/src/ui/styled/CurrencySwitcher.tsx +51 -0
- package/src/ui/styled/DonationButton.tsx +247 -0
- package/src/ui/styled/DonationPage.tsx +29 -0
- package/src/ui/styled/EmailListForm.tsx +88 -0
- package/src/ui/styled/EventConfirmation.tsx +228 -0
- package/src/ui/styled/EventCountdown.tsx +98 -0
- package/src/ui/styled/EventDetail.tsx +141 -0
- package/src/ui/styled/EventTickets.tsx +548 -0
- package/src/ui/styled/EventsList.tsx +83 -0
- package/src/ui/styled/ForgotPasswordForm.tsx +105 -0
- package/src/ui/styled/FormRenderer.tsx +249 -0
- package/src/ui/styled/InstallBanner.tsx +114 -0
- package/src/ui/styled/InvoiceConfirmation.tsx +50 -0
- package/src/ui/styled/InvoicePayment.tsx +141 -0
- package/src/ui/styled/LeadMagnet.tsx +93 -0
- package/src/ui/styled/Loading.tsx +56 -0
- package/src/ui/styled/LoginForm.tsx +134 -0
- package/src/ui/styled/Markdown.tsx +103 -0
- package/src/ui/styled/MembershipCheckout.tsx +197 -0
- package/src/ui/styled/MembershipTiers.tsx +115 -0
- package/src/ui/styled/OfferButton.tsx +181 -0
- package/src/ui/styled/PageActions.tsx +124 -0
- package/src/ui/styled/PaymentLinkConfirmation.tsx +50 -0
- package/src/ui/styled/PaymentLinkPayment.tsx +85 -0
- package/src/ui/styled/Paywall.tsx +54 -0
- package/src/ui/styled/PlayButton.tsx +48 -0
- package/src/ui/styled/PodcastEpisode.tsx +137 -0
- package/src/ui/styled/PodcastList.tsx +58 -0
- package/src/ui/styled/PodcastShow.tsx +164 -0
- package/src/ui/styled/PostsFeed.tsx +63 -0
- package/src/ui/styled/ProductDetail.tsx +616 -0
- package/src/ui/styled/ProductGrid.tsx +82 -0
- package/src/ui/styled/PushOptIn.tsx +65 -0
- package/src/ui/styled/PwaRegistration.tsx +58 -0
- package/src/ui/styled/ReactionBar.tsx +93 -0
- package/src/ui/styled/ReplayList.tsx +134 -0
- package/src/ui/styled/ResetPasswordForm.tsx +127 -0
- package/src/ui/styled/ReviewForm.tsx +292 -0
- package/src/ui/styled/ReviewsSection.tsx +260 -0
- package/src/ui/styled/SectionedFormRenderer.tsx +146 -0
- package/src/ui/styled/SignupForm.tsx +109 -0
- package/src/ui/styled/UserMenu.tsx +166 -0
- package/src/ui/styled/chat/ChatAttachments.tsx +172 -0
- package/src/ui/styled/chat/ChatComposer.tsx +153 -0
- package/src/ui/styled/chat/ChatThreadPanel.tsx +94 -0
- package/src/ui/styled/chat/MessageBubble.tsx +66 -0
- package/src/ui/styled/chat/NewDmDialog.tsx +78 -0
- package/src/ui/styled/community/CommunityComposer.tsx +358 -0
- package/src/ui/styled/community/CommunityFeed.tsx +483 -0
- package/src/ui/styled/community/CommunityLeaderboard.tsx +109 -0
- package/src/ui/styled/community/CommunityMediaGallery.tsx +116 -0
- package/src/ui/styled/community/CommunityMemberProfile.tsx +285 -0
- package/src/ui/styled/community/CommunityNotifications.tsx +222 -0
- package/src/ui/styled/community/CommunityPostCard.tsx +165 -0
- package/src/ui/styled/community/CommunityPostDetail.tsx +337 -0
- package/src/ui/styled/community/CommunityReactionBar.tsx +107 -0
- package/src/ui/styled/community/CommunityRichText.tsx +157 -0
- package/src/ui/styled/community/CommunitySpaces.tsx +174 -0
- package/src/ui/styled/community/MemberAvatar.tsx +74 -0
- package/src/ui/styled/community/index.ts +23 -0
- package/src/ui/styled/community/util.ts +15 -0
- package/src/ui/styled/members/MemberHomeLayout.tsx +98 -0
- package/src/ui/styled/members/MemberPostCard.tsx +392 -0
- package/src/ui/styled/members/MemberPostsFeed.tsx +219 -0
- package/src/ui/styled/members/PostCollectionDetail.tsx +227 -0
- package/src/ui/styled/members/PostCollectionsGrid.tsx +287 -0
- package/src/ui/styled/members/index.ts +7 -0
- package/src/ui/styled/rich-text.css +150 -0
- package/src/ui/styled/work/WorkInviteAccept.tsx +222 -0
- package/src/ui/styled/work/WorkPortalProjects.tsx +163 -0
- package/src/ui/styled/work/WorkProjectInvoices.tsx +113 -0
- package/src/ui/styled/work/WorkProjectReport.tsx +66 -0
- package/src/ui/styled/work/WorkReportBody.tsx +327 -0
- package/src/ui/styled/work/WorkTaskAttachments.tsx +67 -0
- package/src/ui/styled/work/WorkTaskComments.tsx +151 -0
- package/src/ui/styled/work/WorkTaskDetail.tsx +92 -0
- package/src/ui/styled/work/WorkTokenReport.tsx +49 -0
- package/src/ui/styled/work/index.ts +10 -0
- package/src/ui/theme/ForgeThemeProvider.tsx +121 -0
- package/src/ui/theme/contrast.ts +22 -0
- package/src/utils/analyticsBus.ts +31 -0
- package/src/utils/attribution.ts +68 -0
- package/src/utils/cookieConsent.ts +83 -0
- package/src/utils/landing.ts +153 -0
- package/src/utils/metaPixel.ts +122 -0
- package/src/utils/structuredData.ts +126 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { OrderStatus, type IPublicOrder, type ITicketOrder, type CoachingBooking, type CourseBooking } from "../../types/models";
|
|
2
|
+
import { useForge } from "../../provider/ForgeProvider";
|
|
3
|
+
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
4
|
+
|
|
5
|
+
// The payment "finalize" endpoints reconcile provider state on the return page.
|
|
6
|
+
// They're POSTs but idempotent, so they're modeled as load-time queries.
|
|
7
|
+
|
|
8
|
+
// Order is still settling (the payment provider's webhook hasn't marked it paid
|
|
9
|
+
// yet) — safe to poll a return page until it resolves.
|
|
10
|
+
const PENDING_ORDER_STATES = new Set<OrderStatus>([OrderStatus.InitiatedPayment, OrderStatus.Processing]);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Finalize a cart/product order (`/public/orders/finalize`). The backend only
|
|
14
|
+
* needs `orderId` — it reads the stored payment ref off the order to reconcile
|
|
15
|
+
* with the gateway — so nothing else is sent.
|
|
16
|
+
*/
|
|
17
|
+
export function useOrderFinalize(
|
|
18
|
+
args: { orderId?: string },
|
|
19
|
+
opts?: {
|
|
20
|
+
/**
|
|
21
|
+
* Poll while the order is still `initiated_payment` / `processing` (the
|
|
22
|
+
* gateway webhook race on the return page), stopping the moment it settles
|
|
23
|
+
* to paid/failed. The confirmation UI passes this so a just-paid order
|
|
24
|
+
* doesn't get stuck showing "processing".
|
|
25
|
+
*/
|
|
26
|
+
pollWhilePending?: boolean;
|
|
27
|
+
},
|
|
28
|
+
) {
|
|
29
|
+
const { client, profileId } = useForge();
|
|
30
|
+
const { orderId } = args;
|
|
31
|
+
|
|
32
|
+
return useQuery<IPublicOrder>({
|
|
33
|
+
queryKey: ["paymentStatus", profileId, orderId],
|
|
34
|
+
queryFn: async () => {
|
|
35
|
+
const res = await client.post("/public/orders/finalize", { profileId, orderId });
|
|
36
|
+
return res.data;
|
|
37
|
+
},
|
|
38
|
+
enabled: !!profileId && !!client && !!orderId,
|
|
39
|
+
refetchInterval: opts?.pollWhilePending
|
|
40
|
+
? (query) => (query.state.data && PENDING_ORDER_STATES.has(query.state.data.status) ? 2500 : false)
|
|
41
|
+
: false,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Finalize an event ticket order (`/public/events/:eventId/finalize`). */
|
|
46
|
+
export function useEventOrderFinalize(eventId?: string, orderId?: string) {
|
|
47
|
+
const { client, profileId } = useForge();
|
|
48
|
+
|
|
49
|
+
return useQuery<ITicketOrder>({
|
|
50
|
+
queryKey: ["eventTicketOrderStatus", profileId, orderId],
|
|
51
|
+
queryFn: async () => {
|
|
52
|
+
const res = await client.post(`/public/events/${eventId}/finalize`, {
|
|
53
|
+
profileId,
|
|
54
|
+
orderId,
|
|
55
|
+
});
|
|
56
|
+
return res.data;
|
|
57
|
+
},
|
|
58
|
+
enabled: !!profileId && !!client,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Finalize a coaching booking (`/public/coaching/products/:productId/finalize`). */
|
|
63
|
+
export function useCoachingBookingFinalize(productId?: string, orderId?: string) {
|
|
64
|
+
const { client, profileId } = useForge();
|
|
65
|
+
|
|
66
|
+
return useQuery<CoachingBooking>({
|
|
67
|
+
queryKey: ["coachingBookingStatus", profileId, orderId],
|
|
68
|
+
queryFn: async () => {
|
|
69
|
+
const res = await client.post(`/public/coaching/products/${productId}/finalize`, {
|
|
70
|
+
profileId,
|
|
71
|
+
orderId,
|
|
72
|
+
});
|
|
73
|
+
return res.data;
|
|
74
|
+
},
|
|
75
|
+
enabled: !!profileId && !!client && !!orderId,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Finalize a donation after in-place confirmation (`/public/donations/finalize`). */
|
|
80
|
+
export function useDonationFinalize() {
|
|
81
|
+
const { client, profileId } = useForge();
|
|
82
|
+
|
|
83
|
+
return useMutation<{ status: "succeeded" | "failed" | "pending" }, unknown, { donationPaymentId: string }>({
|
|
84
|
+
mutationFn: async ({ donationPaymentId }) => {
|
|
85
|
+
const res = await client.post("/public/donations/finalize", {
|
|
86
|
+
donationPaymentId,
|
|
87
|
+
profileId,
|
|
88
|
+
});
|
|
89
|
+
return res.data;
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Finalize a course booking (`/public/courses/:courseId/finalize`). */
|
|
95
|
+
export function useCourseBookingFinalize(courseId?: string, bookingId?: string) {
|
|
96
|
+
const { client, profileId } = useForge();
|
|
97
|
+
|
|
98
|
+
return useQuery<CourseBooking>({
|
|
99
|
+
queryKey: ["courseBookingStatus", profileId, bookingId],
|
|
100
|
+
queryFn: async () => {
|
|
101
|
+
const res = await client.post(`/public/courses/${courseId}/finalize`, {
|
|
102
|
+
profileId,
|
|
103
|
+
bookingId,
|
|
104
|
+
});
|
|
105
|
+
return res.data;
|
|
106
|
+
},
|
|
107
|
+
enabled: !!profileId && !!client && !!bookingId,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { FormData, FormAnswerEntry } from "../../types/models";
|
|
2
|
+
import { useForge } from "../../provider/ForgeProvider";
|
|
3
|
+
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
4
|
+
|
|
5
|
+
/** A single public form (fields + conditional edges) by id. */
|
|
6
|
+
export function usePublicForm(formId?: string) {
|
|
7
|
+
const { client, profileId } = useForge();
|
|
8
|
+
|
|
9
|
+
return useQuery<FormData>({
|
|
10
|
+
queryKey: ["publicForm", formId],
|
|
11
|
+
queryFn: async () => {
|
|
12
|
+
const res = await client.get(`/public/forms/${formId}`, {
|
|
13
|
+
params: { profileId },
|
|
14
|
+
});
|
|
15
|
+
return res.data;
|
|
16
|
+
},
|
|
17
|
+
enabled: !!client && !!formId,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Submit answers to a public form. */
|
|
22
|
+
export function useSubmitPublicForm(formId?: string) {
|
|
23
|
+
const { client } = useForge();
|
|
24
|
+
|
|
25
|
+
return useMutation({
|
|
26
|
+
mutationFn: async (answers: FormAnswerEntry[]) => {
|
|
27
|
+
const res = await client.post(`/public/forms/${formId}/submit`, { answers });
|
|
28
|
+
return res.data;
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { InvoiceData } from "../../types/models";
|
|
2
|
+
import { useForge } from "../../provider/ForgeProvider";
|
|
3
|
+
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
4
|
+
|
|
5
|
+
/** A single public invoice by id. */
|
|
6
|
+
export function useInvoice(invoiceId?: string) {
|
|
7
|
+
const { client, profileId } = useForge();
|
|
8
|
+
|
|
9
|
+
return useQuery<InvoiceData>({
|
|
10
|
+
queryKey: ["invoice", invoiceId, profileId],
|
|
11
|
+
queryFn: async () => {
|
|
12
|
+
const res = await client.get(`/public/invoices/${invoiceId}`, {
|
|
13
|
+
params: { profileId },
|
|
14
|
+
});
|
|
15
|
+
return res.data;
|
|
16
|
+
},
|
|
17
|
+
enabled: !!profileId && !!client && !!invoiceId,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Download an invoice as a PDF (returns the blob to save). */
|
|
22
|
+
export function useDownloadInvoicePdf(invoiceId?: string) {
|
|
23
|
+
const { client, profileId } = useForge();
|
|
24
|
+
|
|
25
|
+
return useMutation<Blob, unknown, void>({
|
|
26
|
+
mutationFn: async () => {
|
|
27
|
+
const res = await client.get(`/public/invoices/${invoiceId}/pdf`, {
|
|
28
|
+
params: { profileId },
|
|
29
|
+
responseType: "blob",
|
|
30
|
+
});
|
|
31
|
+
return res.data;
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type InvoiceFinalizeResult = {
|
|
37
|
+
id: string;
|
|
38
|
+
invoiceNumber: string;
|
|
39
|
+
clientName: string;
|
|
40
|
+
clientEmail: string;
|
|
41
|
+
total: string;
|
|
42
|
+
currency: string;
|
|
43
|
+
status: string;
|
|
44
|
+
paidAt: string | null;
|
|
45
|
+
profileName: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Finalize an invoice payment on the return/confirmation page. The POST is
|
|
50
|
+
* idempotent (reconciles provider state) so it's modeled as a load-time query.
|
|
51
|
+
*/
|
|
52
|
+
export function useInvoiceFinalize(invoiceId?: string) {
|
|
53
|
+
const { client, profileId } = useForge();
|
|
54
|
+
|
|
55
|
+
return useQuery<InvoiceFinalizeResult>({
|
|
56
|
+
queryKey: ["invoice-finalize", invoiceId, profileId],
|
|
57
|
+
queryFn: async () => {
|
|
58
|
+
const res = await client.post(`/public/invoices/${invoiceId}/finalize`, {
|
|
59
|
+
profileId,
|
|
60
|
+
});
|
|
61
|
+
return res.data;
|
|
62
|
+
},
|
|
63
|
+
enabled: !!profileId && !!client && !!invoiceId,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { useForge } from "../../provider/ForgeProvider";
|
|
2
|
+
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
3
|
+
|
|
4
|
+
export type LeadMagnetMedia = {
|
|
5
|
+
url: string;
|
|
6
|
+
filename?: string;
|
|
7
|
+
type?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type LeadMagnetConfirmation = {
|
|
11
|
+
leadMagnet: {
|
|
12
|
+
id: string;
|
|
13
|
+
thankYouMessage?: string | null;
|
|
14
|
+
media?: LeadMagnetMedia[];
|
|
15
|
+
};
|
|
16
|
+
profile: {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Confirms a lead-magnet subscription link and returns the lead magnet + profile.
|
|
24
|
+
* The double opt-in confirm is a read (idempotent GET) — the download tracking
|
|
25
|
+
* POST stays a separate flow.
|
|
26
|
+
*/
|
|
27
|
+
export function useLeadMagnetConfirm(leadMagnetId?: string, subscriberId?: string) {
|
|
28
|
+
const { client } = useForge();
|
|
29
|
+
|
|
30
|
+
return useQuery<LeadMagnetConfirmation>({
|
|
31
|
+
queryKey: ["lead-magnet-confirm", leadMagnetId, subscriberId],
|
|
32
|
+
queryFn: async () => {
|
|
33
|
+
const res = await client.get(`/public/email-lists/lead-magnet/confirm`, {
|
|
34
|
+
params: { leadMagnetId, subscriberId },
|
|
35
|
+
});
|
|
36
|
+
return res.data;
|
|
37
|
+
},
|
|
38
|
+
enabled: !!client && !!leadMagnetId && !!subscriberId,
|
|
39
|
+
retry: false,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Confirm a double-opt-in email subscription link. Returns the profile. */
|
|
44
|
+
export function useConfirmEmailSubscription(subscriberId?: string, enabled = true) {
|
|
45
|
+
const { client } = useForge();
|
|
46
|
+
|
|
47
|
+
return useQuery<{ profile: { id: string; name: string } }>({
|
|
48
|
+
queryKey: ["confirm-email-subscription", subscriberId],
|
|
49
|
+
queryFn: async () => {
|
|
50
|
+
const res = await client.get(`/public/email-lists/confirm-subscription`, {
|
|
51
|
+
params: { subscriberId },
|
|
52
|
+
});
|
|
53
|
+
return res.data;
|
|
54
|
+
},
|
|
55
|
+
enabled: !!client && !!subscriberId && enabled,
|
|
56
|
+
retry: false,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type JoinEmailListInput = {
|
|
61
|
+
email: string;
|
|
62
|
+
emailListId?: string;
|
|
63
|
+
leadMagnetId?: string;
|
|
64
|
+
firstName?: string;
|
|
65
|
+
/** @deprecated send `firstName` — kept only for backward compatibility. */
|
|
66
|
+
name?: string;
|
|
67
|
+
phoneNumber?: string;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** Join an email list / subscribe (may require double opt-in confirmation). */
|
|
71
|
+
export function useJoinEmailList() {
|
|
72
|
+
const { client, profileId } = useForge();
|
|
73
|
+
|
|
74
|
+
return useMutation<{ requiresConfirmation?: boolean }, unknown, JoinEmailListInput>({
|
|
75
|
+
mutationFn: async (body) => {
|
|
76
|
+
const res = await client.post(`/public/email-lists/join`, { ...body, profileId });
|
|
77
|
+
return res.data;
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Track a lead-magnet download (fire-and-forget; the file download proceeds regardless). */
|
|
83
|
+
export function useTrackLeadMagnetDownload() {
|
|
84
|
+
const { client } = useForge();
|
|
85
|
+
|
|
86
|
+
return useMutation<unknown, unknown, { leadMagnetId?: string; subscriberId?: string }>({
|
|
87
|
+
mutationFn: async ({ leadMagnetId, subscriberId }) => {
|
|
88
|
+
const res = await client.post(`/public/email-lists/lead-magnet/download`, {
|
|
89
|
+
leadMagnetId,
|
|
90
|
+
subscriberId,
|
|
91
|
+
data: { userAgent: typeof navigator !== "undefined" ? navigator.userAgent : undefined },
|
|
92
|
+
});
|
|
93
|
+
return res.data;
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { MembershipTier } from "../../types/models";
|
|
2
|
+
import { useForge } from "../../provider/ForgeProvider";
|
|
3
|
+
import { useQuery } from "@tanstack/react-query";
|
|
4
|
+
|
|
5
|
+
export function useGetMembershipTiers() {
|
|
6
|
+
const { client, profileId } = useForge();
|
|
7
|
+
|
|
8
|
+
return useQuery<MembershipTier[]>({
|
|
9
|
+
queryKey: ["membership-tiers", profileId],
|
|
10
|
+
queryFn: async () => {
|
|
11
|
+
const res = await client.get("/public/membership-tiers", {
|
|
12
|
+
params: {
|
|
13
|
+
profileId: profileId,
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
return res.data;
|
|
17
|
+
},
|
|
18
|
+
enabled: !!profileId && !!client,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { useForge } from "../../provider/ForgeProvider";
|
|
2
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3
|
+
import { useCallback, useEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Member notification headless hooks — the FROZEN public contract that the
|
|
7
|
+
* apps/client PWA and every generated custom-code site consume. Speaks ONLY the
|
|
8
|
+
* `/public/notifications/*` API (see docs/membership-notifications-initiative.md
|
|
9
|
+
* "API surface"). Moved here from apps/client so custom-code sites get push
|
|
10
|
+
* subscription registration, inbox, and a settings matrix with no apps/client
|
|
11
|
+
* code.
|
|
12
|
+
*
|
|
13
|
+
* === FROZEN PUSH-SUBSCRIPTION CONTRACT (do not break; PWA lane depends on it) ===
|
|
14
|
+
* usePushSubscription(profileId) → {
|
|
15
|
+
* isSupported, permissionState, isSubscribed, isLoading,
|
|
16
|
+
* subscribe(): Promise<boolean>, // requests permission, subscribes, POSTs to backend
|
|
17
|
+
* unsubscribe(): Promise<boolean>, // unsubscribes locally + POSTs /unsubscribe
|
|
18
|
+
* }
|
|
19
|
+
* Backend endpoints used:
|
|
20
|
+
* GET /public/notifications/vapid-key → { vapidPublicKey }
|
|
21
|
+
* POST /public/notifications/subscribe { profileId, endpoint, p256dh, auth, userAgent? }
|
|
22
|
+
* POST /public/notifications/unsubscribe { profileId, endpoint }
|
|
23
|
+
* SW push payload shape (what the generated service worker must render):
|
|
24
|
+
* { type, title, body, url, profileId, channelId?, spaceId?, entityId? }
|
|
25
|
+
* ================================================================================
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
// ---- shared types --------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
export type NotificationChannel = "inapp" | "push" | "email";
|
|
31
|
+
|
|
32
|
+
export type NotificationType =
|
|
33
|
+
| "dm"
|
|
34
|
+
| "chat_message"
|
|
35
|
+
| "mention"
|
|
36
|
+
| "reply"
|
|
37
|
+
| "reaction"
|
|
38
|
+
| "new_community_post"
|
|
39
|
+
| "new_post"
|
|
40
|
+
| "event_reminder"
|
|
41
|
+
| "stream_live"
|
|
42
|
+
| "mod";
|
|
43
|
+
|
|
44
|
+
export interface NotificationInboxItem {
|
|
45
|
+
id: string;
|
|
46
|
+
profileId: string;
|
|
47
|
+
accountId: string;
|
|
48
|
+
type: NotificationType | string;
|
|
49
|
+
entityType: string | null;
|
|
50
|
+
entityId: string | null;
|
|
51
|
+
readAt: string | null;
|
|
52
|
+
createdAt: string;
|
|
53
|
+
data: {
|
|
54
|
+
title?: string;
|
|
55
|
+
body?: string;
|
|
56
|
+
url?: string;
|
|
57
|
+
actorName?: string;
|
|
58
|
+
actorAccountId?: string;
|
|
59
|
+
} | null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type DigestCadence = "off" | "inapp_only" | "hourly" | "daily";
|
|
63
|
+
|
|
64
|
+
export interface NotificationMatrix {
|
|
65
|
+
id: string;
|
|
66
|
+
accountId: string;
|
|
67
|
+
profileId: string;
|
|
68
|
+
digestCadence: DigestCadence;
|
|
69
|
+
// fully-resolved matrix ("<type>.<channel>" -> boolean), defaults merged over overrides
|
|
70
|
+
preferences: Record<string, boolean>;
|
|
71
|
+
// raw explicit overrides only
|
|
72
|
+
overrides: Record<string, boolean>;
|
|
73
|
+
defaults: Record<string, { inapp: boolean; push: boolean; email: boolean; emailMode: string }>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---- query keys ----------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
const keys = {
|
|
79
|
+
inbox: (profileId?: string, unreadOnly?: boolean) =>
|
|
80
|
+
["public-notifications-inbox", profileId, unreadOnly] as const,
|
|
81
|
+
prefs: (profileId?: string) => ["public-notification-preferences", profileId] as const,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// ---- helpers -------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
function urlBase64ToUint8Array(base64String: string) {
|
|
87
|
+
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
|
|
88
|
+
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
|
|
89
|
+
const rawData = window.atob(base64);
|
|
90
|
+
const outputArray = new Uint8Array(rawData.length);
|
|
91
|
+
for (let i = 0; i < rawData.length; ++i) outputArray[i] = rawData.charCodeAt(i);
|
|
92
|
+
return outputArray;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---- usePushSubscription -------------------------------------------------
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Web-push subscription registration (headless). Handles permission,
|
|
99
|
+
* pushManager.subscribe with the platform VAPID key, and POST to the backend.
|
|
100
|
+
* Migrated verbatim from apps/client/src/lib/hooks/usePushNotifications so the
|
|
101
|
+
* behaviour (and the API contract above) is identical for custom-code sites.
|
|
102
|
+
*/
|
|
103
|
+
export function usePushSubscription(profileId: string) {
|
|
104
|
+
const { client } = useForge();
|
|
105
|
+
const [permissionState, setPermissionState] = useState<NotificationPermission>("default");
|
|
106
|
+
const [isSubscribed, setIsSubscribed] = useState(false);
|
|
107
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
108
|
+
const [isSupported, setIsSupported] = useState(false);
|
|
109
|
+
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
const supported = "Notification" in window && "serviceWorker" in navigator && "PushManager" in window;
|
|
112
|
+
setIsSupported(supported);
|
|
113
|
+
if (!supported) {
|
|
114
|
+
setIsLoading(false);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
setPermissionState(Notification.permission);
|
|
118
|
+
navigator.serviceWorker.ready.then((registration) => {
|
|
119
|
+
registration.pushManager.getSubscription().then((subscription) => {
|
|
120
|
+
setIsSubscribed(!!subscription);
|
|
121
|
+
setIsLoading(false);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}, []);
|
|
125
|
+
|
|
126
|
+
const subscribe = useCallback(async () => {
|
|
127
|
+
if (!isSupported || !client) return false;
|
|
128
|
+
try {
|
|
129
|
+
setIsLoading(true);
|
|
130
|
+
const permission = await Notification.requestPermission();
|
|
131
|
+
setPermissionState(permission);
|
|
132
|
+
if (permission !== "granted") {
|
|
133
|
+
setIsLoading(false);
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
const { data } = await client.get("/public/notifications/vapid-key");
|
|
137
|
+
if (!data?.vapidPublicKey) {
|
|
138
|
+
setIsLoading(false);
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
const registration = await navigator.serviceWorker.ready;
|
|
142
|
+
const subscription = await registration.pushManager.subscribe({
|
|
143
|
+
userVisibleOnly: true,
|
|
144
|
+
applicationServerKey: urlBase64ToUint8Array(data.vapidPublicKey),
|
|
145
|
+
});
|
|
146
|
+
const json = subscription.toJSON();
|
|
147
|
+
await client.post("/public/notifications/subscribe", {
|
|
148
|
+
profileId,
|
|
149
|
+
endpoint: json.endpoint,
|
|
150
|
+
p256dh: json.keys?.p256dh,
|
|
151
|
+
auth: json.keys?.auth,
|
|
152
|
+
userAgent: navigator.userAgent,
|
|
153
|
+
});
|
|
154
|
+
setIsSubscribed(true);
|
|
155
|
+
setIsLoading(false);
|
|
156
|
+
return true;
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.error("Push subscription error:", error);
|
|
159
|
+
setIsLoading(false);
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}, [isSupported, client, profileId]);
|
|
163
|
+
|
|
164
|
+
const unsubscribe = useCallback(async () => {
|
|
165
|
+
if (!isSupported || !client) return false;
|
|
166
|
+
try {
|
|
167
|
+
setIsLoading(true);
|
|
168
|
+
const registration = await navigator.serviceWorker.ready;
|
|
169
|
+
const subscription = await registration.pushManager.getSubscription();
|
|
170
|
+
if (subscription) {
|
|
171
|
+
const endpoint = subscription.endpoint;
|
|
172
|
+
await subscription.unsubscribe();
|
|
173
|
+
await client.post("/public/notifications/unsubscribe", { profileId, endpoint });
|
|
174
|
+
}
|
|
175
|
+
setIsSubscribed(false);
|
|
176
|
+
setIsLoading(false);
|
|
177
|
+
return true;
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error("Push unsubscribe error:", error);
|
|
180
|
+
setIsLoading(false);
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}, [isSupported, client, profileId]);
|
|
184
|
+
|
|
185
|
+
return { permissionState, isSubscribed, isLoading, subscribe, unsubscribe, isSupported };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ---- useNotificationInbox ------------------------------------------------
|
|
189
|
+
|
|
190
|
+
/** In-app inbox (GET /public/notifications/inbox). Returns { data, unreadCount }. */
|
|
191
|
+
export function useNotificationInbox({ unreadOnly = false, page = 1, limit = 50 } = {}) {
|
|
192
|
+
const { client, profileId } = useForge();
|
|
193
|
+
return useQuery<{ data: NotificationInboxItem[]; unreadCount: number }>({
|
|
194
|
+
queryKey: keys.inbox(profileId, unreadOnly),
|
|
195
|
+
queryFn: async () => {
|
|
196
|
+
const res = await client.get("/public/notifications/inbox", {
|
|
197
|
+
params: { profileId, page, limit, unreadOnly },
|
|
198
|
+
});
|
|
199
|
+
return res.data;
|
|
200
|
+
},
|
|
201
|
+
enabled: !!profileId && !!client,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Unread count for the bell, polled every 30s. */
|
|
206
|
+
export function useNotificationUnreadCount(enabled = true) {
|
|
207
|
+
const { client, profileId } = useForge();
|
|
208
|
+
return useQuery<number>({
|
|
209
|
+
queryKey: [...keys.inbox(profileId, true), "count"],
|
|
210
|
+
queryFn: async () => {
|
|
211
|
+
const res = await client.get("/public/notifications/inbox", {
|
|
212
|
+
params: { profileId, page: 1, limit: 1, unreadOnly: true },
|
|
213
|
+
});
|
|
214
|
+
return res.data.unreadCount as number;
|
|
215
|
+
},
|
|
216
|
+
enabled: enabled && !!profileId && !!client,
|
|
217
|
+
refetchInterval: 30000,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Mark notifications read — omit ids to mark ALL unread as read. */
|
|
222
|
+
export function useMarkNotificationsRead() {
|
|
223
|
+
const { client, profileId } = useForge();
|
|
224
|
+
const queryClient = useQueryClient();
|
|
225
|
+
return useMutation({
|
|
226
|
+
mutationFn: async (ids?: string[]) => {
|
|
227
|
+
await client.post("/public/notifications/mark-read", { profileId, ids });
|
|
228
|
+
},
|
|
229
|
+
onSuccess: () => {
|
|
230
|
+
queryClient.invalidateQueries({ queryKey: ["public-notifications-inbox"] });
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ---- useNotificationPreferences ------------------------------------------
|
|
236
|
+
|
|
237
|
+
/** The member's notification matrix + digest cadence (GET /preferences). */
|
|
238
|
+
export function useNotificationMatrix(enabled = true) {
|
|
239
|
+
const { client, profileId } = useForge();
|
|
240
|
+
return useQuery<NotificationMatrix>({
|
|
241
|
+
queryKey: keys.prefs(profileId),
|
|
242
|
+
queryFn: async () => {
|
|
243
|
+
const res = await client.get("/public/notifications/preferences", { params: { profileId } });
|
|
244
|
+
return res.data;
|
|
245
|
+
},
|
|
246
|
+
enabled: enabled && !!profileId && !!client,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Update the matrix and/or digest cadence (PUT /preferences). */
|
|
251
|
+
export function useUpdateNotificationMatrix() {
|
|
252
|
+
const { client, profileId } = useForge();
|
|
253
|
+
const queryClient = useQueryClient();
|
|
254
|
+
return useMutation<
|
|
255
|
+
NotificationMatrix,
|
|
256
|
+
unknown,
|
|
257
|
+
{ preferences?: Record<string, boolean>; digestCadence?: DigestCadence }
|
|
258
|
+
>({
|
|
259
|
+
mutationFn: async (body) => {
|
|
260
|
+
const res = await client.put("/public/notifications/preferences", { profileId, ...body });
|
|
261
|
+
return res.data;
|
|
262
|
+
},
|
|
263
|
+
onSuccess: () => {
|
|
264
|
+
queryClient.invalidateQueries({ queryKey: keys.prefs(profileId) });
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { PublicOffer } from "../../types/models";
|
|
2
|
+
import { useForge } from "../../provider/ForgeProvider";
|
|
3
|
+
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
4
|
+
|
|
5
|
+
/** A single offer by id. */
|
|
6
|
+
export function useGetOffer(offerId?: string | null, enabled = true) {
|
|
7
|
+
const { client, profileId } = useForge();
|
|
8
|
+
|
|
9
|
+
return useQuery<PublicOffer>({
|
|
10
|
+
queryKey: ["offer", offerId],
|
|
11
|
+
queryFn: async () => {
|
|
12
|
+
const res = await client.get(`/public/offers/${offerId}`);
|
|
13
|
+
return res.data;
|
|
14
|
+
},
|
|
15
|
+
enabled: !!client && !!offerId && !!profileId && enabled,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The underlying item an offer points to (product / event / course / coaching).
|
|
21
|
+
* Returns `any` because the shape is polymorphic by `offerType` — the caller
|
|
22
|
+
* renders the type-specific component.
|
|
23
|
+
*/
|
|
24
|
+
export function useOfferItem(offer?: PublicOffer | null, enabled = true) {
|
|
25
|
+
const { client, profileId } = useForge();
|
|
26
|
+
|
|
27
|
+
return useQuery({
|
|
28
|
+
queryKey: ["offer-item", offer?.id, offer?.offerType, offer?.itemId],
|
|
29
|
+
queryFn: async () => {
|
|
30
|
+
if (!offer) return null;
|
|
31
|
+
const paths: Record<string, string> = {
|
|
32
|
+
product: `/public/products/${offer.itemId}`,
|
|
33
|
+
event: `/public/events/${offer.itemId}`,
|
|
34
|
+
course: `/public/courses/${offer.itemId}`,
|
|
35
|
+
coaching: `/public/coaching/products/${offer.itemId}`,
|
|
36
|
+
};
|
|
37
|
+
const path = paths[offer.offerType];
|
|
38
|
+
if (!path) return null;
|
|
39
|
+
const res = await client.get(path, { params: { profileId } });
|
|
40
|
+
return res.data;
|
|
41
|
+
},
|
|
42
|
+
enabled: !!client && !!offer && !!profileId && enabled,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Track an offer-button click (fire-and-forget). */
|
|
47
|
+
export function useTrackOfferClick() {
|
|
48
|
+
const { client } = useForge();
|
|
49
|
+
|
|
50
|
+
return useMutation<unknown, unknown, { offerId: string }>({
|
|
51
|
+
mutationFn: async ({ offerId }) => {
|
|
52
|
+
const res = await client.post(`/public/offers/${offerId}/track`);
|
|
53
|
+
return res.data;
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|