@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,171 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from "react";
|
|
2
|
+
import { type Appearance, loadStripe } from "@stripe/stripe-js";
|
|
3
|
+
import { Elements, PaymentElement, useElements, useStripe } from "@stripe/react-stripe-js";
|
|
4
|
+
import { useForgeTheme } from "../theme/ForgeThemeProvider";
|
|
5
|
+
import { readableTextOn } from "../theme/contrast";
|
|
6
|
+
import { useFormatCurrency } from "../format/useFormatCurrency";
|
|
7
|
+
import { useSiteConfig } from "../../data/queries/useWebsite";
|
|
8
|
+
import type { PaymentRenderProps } from "./ForgePaymentProvider";
|
|
9
|
+
|
|
10
|
+
function alpha(hex: number) {
|
|
11
|
+
return Math.round(hex * 255)
|
|
12
|
+
.toString(16)
|
|
13
|
+
.padStart(2, "0");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Forge's built-in Stripe payment UI — the default renderer used by every
|
|
18
|
+
* payment block (Checkout, Donation, Offer, …) when the app hasn't registered
|
|
19
|
+
* its own via `<ForgePaymentProvider>`. It is fully self-contained: the Stripe
|
|
20
|
+
* publishable key + connected account are read from the tenant's runtime
|
|
21
|
+
* `useSiteConfig()` (Stripe Connect: `loadStripe(platformKey, { stripeAccount })`),
|
|
22
|
+
* the appearance from `ForgeThemeProvider`, and the pay-button amount from
|
|
23
|
+
* `useFormatCurrency()`. The consuming site wires nothing.
|
|
24
|
+
*/
|
|
25
|
+
export function ForgeStripePayment({
|
|
26
|
+
clientSecret,
|
|
27
|
+
returnUrl,
|
|
28
|
+
amount,
|
|
29
|
+
currency,
|
|
30
|
+
mode = "redirect",
|
|
31
|
+
onSucceeded,
|
|
32
|
+
onFailed,
|
|
33
|
+
}: PaymentRenderProps) {
|
|
34
|
+
const theme = useForgeTheme();
|
|
35
|
+
const { data: siteConfig } = useSiteConfig();
|
|
36
|
+
const publishableKey = siteConfig?.stripePublishableKey ?? undefined;
|
|
37
|
+
const stripeAccountId = siteConfig?.stripeAccountId ?? undefined;
|
|
38
|
+
|
|
39
|
+
const stripePromise = useMemo(
|
|
40
|
+
() => (publishableKey && stripeAccountId ? loadStripe(publishableKey, { stripeAccount: stripeAccountId }) : null),
|
|
41
|
+
[publishableKey, stripeAccountId],
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// Fully themed Stripe Elements appearance (variables + rules), ported from the
|
|
45
|
+
// client app's payment form so the inputs/tabs match the site theme.
|
|
46
|
+
const appearance: Appearance = useMemo(
|
|
47
|
+
() => ({
|
|
48
|
+
theme: "flat",
|
|
49
|
+
variables: {
|
|
50
|
+
fontLineHeight: "1.5",
|
|
51
|
+
borderRadius: `${theme.cornerRadius}px`,
|
|
52
|
+
colorBackground: theme.colors.background,
|
|
53
|
+
accessibleColorOnColorPrimary: theme.colors.text,
|
|
54
|
+
colorPrimary: theme.colors.primary,
|
|
55
|
+
colorText: theme.colors.text,
|
|
56
|
+
logoColor: theme.colors.text,
|
|
57
|
+
colorTextPlaceholder: `${theme.colors.text}${alpha(0.5)}`,
|
|
58
|
+
},
|
|
59
|
+
rules: {
|
|
60
|
+
".AccordionItem": { backgroundColor: theme.colors.background },
|
|
61
|
+
".Block": { backgroundColor: theme.colors.background, boxShadow: "none", padding: "12px" },
|
|
62
|
+
".Input": {
|
|
63
|
+
padding: "12px",
|
|
64
|
+
borderRadius: `${theme.cornerRadius}px`,
|
|
65
|
+
border: `1px solid ${theme.colors.primary}${alpha(0.4)}`,
|
|
66
|
+
},
|
|
67
|
+
".Input:disabled, .Input--invalid:disabled": { color: "lightgray" },
|
|
68
|
+
".Tab": { padding: "10px 12px 8px 12px", border: "none", backgroundColor: theme.colors.background },
|
|
69
|
+
".Tab:hover": {
|
|
70
|
+
border: "none",
|
|
71
|
+
boxShadow: "0px 1px 1px rgba(0, 0, 0, 0.03), 0px 3px 7px rgba(18, 42, 66, 0.04)",
|
|
72
|
+
},
|
|
73
|
+
".Tab--selected, .Tab--selected:focus, .Tab--selected:hover": {
|
|
74
|
+
border: "none",
|
|
75
|
+
backgroundColor: theme.colors.background,
|
|
76
|
+
boxShadow:
|
|
77
|
+
"0 0 0 1.5px var(--colorPrimaryText), 0px 1px 1px rgba(0, 0, 0, 0.03), 0px 3px 7px rgba(18, 42, 66, 0.04)",
|
|
78
|
+
},
|
|
79
|
+
".Label": { fontWeight: "500" },
|
|
80
|
+
},
|
|
81
|
+
}),
|
|
82
|
+
[theme],
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
if (!clientSecret) return null;
|
|
86
|
+
if (!stripePromise) {
|
|
87
|
+
return (
|
|
88
|
+
<p style={{ color: "#ef4444", fontSize: 14 }}>Payment is not configured.</p>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<Elements options={{ clientSecret, appearance, loader: "auto" }} stripe={stripePromise}>
|
|
94
|
+
<Inner returnUrl={returnUrl} amount={amount} currency={currency} mode={mode} onSucceeded={onSucceeded} onFailed={onFailed} />
|
|
95
|
+
</Elements>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function Inner({
|
|
100
|
+
returnUrl,
|
|
101
|
+
amount,
|
|
102
|
+
currency,
|
|
103
|
+
mode = "redirect",
|
|
104
|
+
onSucceeded,
|
|
105
|
+
onFailed,
|
|
106
|
+
}: Omit<PaymentRenderProps, "clientSecret">) {
|
|
107
|
+
const theme = useForgeTheme();
|
|
108
|
+
const { formatCurrency } = useFormatCurrency();
|
|
109
|
+
const stripe = useStripe();
|
|
110
|
+
const elements = useElements();
|
|
111
|
+
const [message, setMessage] = useState("");
|
|
112
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
113
|
+
|
|
114
|
+
const buttonText = `Pay ${formatCurrency(amount, currency || undefined)} Now`;
|
|
115
|
+
|
|
116
|
+
const handleSubmit = useCallback(async () => {
|
|
117
|
+
if (!stripe || !elements) return;
|
|
118
|
+
setIsLoading(true);
|
|
119
|
+
setMessage("");
|
|
120
|
+
|
|
121
|
+
if (mode === "manual") {
|
|
122
|
+
const { error, paymentIntent } = await stripe.confirmPayment({
|
|
123
|
+
elements,
|
|
124
|
+
confirmParams: { return_url: returnUrl },
|
|
125
|
+
redirect: "if_required",
|
|
126
|
+
});
|
|
127
|
+
if (error) {
|
|
128
|
+
const msg = error.message ?? "Your payment could not be processed.";
|
|
129
|
+
setMessage(msg);
|
|
130
|
+
setIsLoading(false);
|
|
131
|
+
onFailed?.(msg);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (paymentIntent?.status === "succeeded" || paymentIntent?.status === "processing") {
|
|
135
|
+
setIsLoading(false);
|
|
136
|
+
onSucceeded?.();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
setIsLoading(false);
|
|
140
|
+
onFailed?.("Payment did not complete.");
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const { error } = await stripe.confirmPayment({ elements, confirmParams: { return_url: returnUrl } });
|
|
145
|
+
setMessage(error.message ?? "An unexpected error occurred.");
|
|
146
|
+
setIsLoading(false);
|
|
147
|
+
}, [stripe, elements, mode, returnUrl, onSucceeded, onFailed]);
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<form onSubmit={(e) => e.preventDefault()} style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
|
151
|
+
<PaymentElement options={{ layout: "accordion" }} />
|
|
152
|
+
{message && <div style={{ color: "#ef4444", fontSize: 14 }}>{message}</div>}
|
|
153
|
+
<button
|
|
154
|
+
onClick={handleSubmit}
|
|
155
|
+
disabled={isLoading || !stripe || !elements}
|
|
156
|
+
style={{
|
|
157
|
+
background: theme.colors.primary,
|
|
158
|
+
color: readableTextOn(theme.colors.primary),
|
|
159
|
+
border: "none",
|
|
160
|
+
borderRadius: theme.cornerRadius,
|
|
161
|
+
padding: "12px 18px",
|
|
162
|
+
fontWeight: 600,
|
|
163
|
+
cursor: "pointer",
|
|
164
|
+
opacity: isLoading ? 0.6 : 1,
|
|
165
|
+
}}
|
|
166
|
+
>
|
|
167
|
+
{isLoading ? "Processing…" : buttonText}
|
|
168
|
+
</button>
|
|
169
|
+
</form>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { useEffect, type ReactNode } from "react";
|
|
2
|
+
import { ForgeProvider, type ForgeProviderProps } from "../../provider/ForgeAppProvider";
|
|
3
|
+
import { AudioPlayer } from "../styled/AudioPlayer";
|
|
4
|
+
import { ForgeAnalytics } from "../analytics/ForgeAnalytics";
|
|
5
|
+
import { PwaRegistration } from "../styled/PwaRegistration";
|
|
6
|
+
import { InstallBanner } from "../styled/InstallBanner";
|
|
7
|
+
import { CookieConsent } from "../styled/CookieConsent";
|
|
8
|
+
import { useThemeTokens } from "../theme/ForgeThemeProvider";
|
|
9
|
+
import { shellPwaEnabled } from "./shellGating";
|
|
10
|
+
import { captureAttributionRefFromUrl, readAttributionRef } from "../../utils/attribution";
|
|
11
|
+
import { captureLandingFromUrl, postLandingBeacon } from "../../utils/landing";
|
|
12
|
+
|
|
13
|
+
// Captures both attribution signals on first paint: the tracked-link `?tn_ref`
|
|
14
|
+
// (stripped afterwards) and the first-touch landing snapshot (all utm/click-id
|
|
15
|
+
// params + referrer, kept in the URL for page-level ad pixels). On a FRESH
|
|
16
|
+
// landing it also fires the visit-level beacon so the sale-less visit is counted.
|
|
17
|
+
// Client-only, renders null. Lives in the shell so every code website gets ad
|
|
18
|
+
// attribution without editing the tenant-owned __root. See docs/ads-attribution-initiative.md.
|
|
19
|
+
function AttributionCapture({ enabled, apiUrl, profileId }: { enabled: boolean; apiUrl?: string; profileId?: string }) {
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (!enabled) return;
|
|
22
|
+
captureAttributionRefFromUrl();
|
|
23
|
+
const fresh = captureLandingFromUrl();
|
|
24
|
+
if (fresh && apiUrl && profileId) {
|
|
25
|
+
postLandingBeacon(apiUrl, profileId, readAttributionRef() ?? undefined);
|
|
26
|
+
}
|
|
27
|
+
}, [enabled, apiUrl, profileId]);
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface TribeNestAppProps extends Omit<ForgeProviderProps, "children"> {
|
|
32
|
+
/** The page tree — the tenant's own Nav / <Outlet/> / Footer. */
|
|
33
|
+
children: ReactNode;
|
|
34
|
+
/** "published" on the live site, "draft" in the editor preview / review Worker.
|
|
35
|
+
* With `editable`, gates the PWA (register + install) to the live site only. */
|
|
36
|
+
state?: "draft" | "published";
|
|
37
|
+
/** Href for the client-injected manifest link fallback. */
|
|
38
|
+
manifestHref?: string;
|
|
39
|
+
/** Turn individual shell concerns off (all default on). */
|
|
40
|
+
analytics?: boolean;
|
|
41
|
+
pwa?: boolean;
|
|
42
|
+
cookieConsent?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Ensure the PWA `<link rel="manifest">` + a `theme-color` meta exist even if the
|
|
46
|
+
// route's head() never called buildPwaHead (retrofit for older shells). Runs
|
|
47
|
+
// inside ForgeProvider so it can read the resolved theme; client-only, renders null.
|
|
48
|
+
function PwaHeadClient({ enabled, manifestHref }: { enabled: boolean; manifestHref: string }) {
|
|
49
|
+
const t = useThemeTokens();
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (!enabled || typeof document === "undefined") return;
|
|
52
|
+
if (!document.querySelector('link[rel="manifest"]')) {
|
|
53
|
+
const link = document.createElement("link");
|
|
54
|
+
link.rel = "manifest";
|
|
55
|
+
link.href = manifestHref;
|
|
56
|
+
document.head.appendChild(link);
|
|
57
|
+
}
|
|
58
|
+
if (t.primary && !document.querySelector('meta[name="theme-color"]')) {
|
|
59
|
+
const meta = document.createElement("meta");
|
|
60
|
+
meta.name = "theme-color";
|
|
61
|
+
meta.content = t.primary;
|
|
62
|
+
document.head.appendChild(meta);
|
|
63
|
+
}
|
|
64
|
+
}, [enabled, manifestHref, t.primary]);
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The single root component for a TribeNest code website. Wrap the page tree once
|
|
70
|
+
* — it owns the whole provider tree (via `ForgeProvider`) PLUS the global shell
|
|
71
|
+
* UI: audio player, analytics + Meta Pixel, PWA (service-worker registration +
|
|
72
|
+
* install banner + manifest link), and cookie consent. Because these live in
|
|
73
|
+
* Forge, a Forge version bump delivers shell changes to every site without
|
|
74
|
+
* editing the tenant-owned `__root.tsx`.
|
|
75
|
+
*
|
|
76
|
+
* <TribeNestApp apiUrl={API_URL} profileId={PROFILE_ID} subdomain={SUBDOMAIN}
|
|
77
|
+
* editable={FORGE_EDIT} state={FORGE_STATE}
|
|
78
|
+
* initialContentDoc={contentDoc} initialSiteConfig={siteConfig}>
|
|
79
|
+
* <Nav />
|
|
80
|
+
* <main><Outlet /></main>
|
|
81
|
+
* <SiteFooter />
|
|
82
|
+
* </TribeNestApp>
|
|
83
|
+
*
|
|
84
|
+
* Nav / footer / pages stay tenant-owned children. A footer "Cookie settings"
|
|
85
|
+
* link re-opens the consent banner via `useCookieConsent().reopen()`.
|
|
86
|
+
*/
|
|
87
|
+
export function TribeNestApp({
|
|
88
|
+
children,
|
|
89
|
+
editable,
|
|
90
|
+
state,
|
|
91
|
+
manifestHref = "/manifest.webmanifest",
|
|
92
|
+
analytics = true,
|
|
93
|
+
pwa = true,
|
|
94
|
+
cookieConsent = true,
|
|
95
|
+
...forgeProps
|
|
96
|
+
}: TribeNestAppProps) {
|
|
97
|
+
// Register the SW + offer install only on the live published site — never in
|
|
98
|
+
// the HMR editor or on a preview-<versionId>.* review Worker (state="draft").
|
|
99
|
+
const pwaEnabled = shellPwaEnabled({ pwa, editable, state });
|
|
100
|
+
return (
|
|
101
|
+
<ForgeProvider editable={editable} {...forgeProps}>
|
|
102
|
+
{children}
|
|
103
|
+
{/* Global playback bar — self-pins to the viewport bottom, renders nothing
|
|
104
|
+
until a track loads. */}
|
|
105
|
+
<AudioPlayer />
|
|
106
|
+
{/* Auto page views + clicks (+ Meta Pixel, gated on marketing consent).
|
|
107
|
+
First-party runs cookieless. Off in the editor so preview isn't counted. */}
|
|
108
|
+
{analytics && <ForgeAnalytics enabled={!editable} firstPartyMode="cookieless" />}
|
|
109
|
+
{/* Ad attribution capture (tracked-link ref + first-touch landing + visit
|
|
110
|
+
beacon). Off in the editor so preview visits aren't credited. */}
|
|
111
|
+
<AttributionCapture enabled={!editable} apiUrl={forgeProps.apiUrl} profileId={forgeProps.profileId} />
|
|
112
|
+
{/* PWA — SW registration + install banner + (fallback) manifest link. */}
|
|
113
|
+
{pwa && <PwaRegistration enabled={pwaEnabled} />}
|
|
114
|
+
{pwa && <InstallBanner enabled={pwaEnabled} />}
|
|
115
|
+
{pwa && <PwaHeadClient enabled={pwaEnabled} manifestHref={manifestHref} />}
|
|
116
|
+
{/* Consent banner — shown on first visit, re-openable from anywhere via
|
|
117
|
+
useCookieConsent().reopen(). Hidden in the editor. */}
|
|
118
|
+
{cookieConsent && !editable && <CookieConsent useTribeNestPrivacy />}
|
|
119
|
+
</ForgeProvider>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { shellPwaEnabled } from "./shellGating";
|
|
3
|
+
|
|
4
|
+
// The PWA (SW register + install prompt) must be ON only for the live published
|
|
5
|
+
// site, and OFF everywhere else (editor, preview/draft), so review Workers never
|
|
6
|
+
// SW-cache or prompt to install.
|
|
7
|
+
describe("shellPwaEnabled", () => {
|
|
8
|
+
it("is true only for a live published, non-editable site", () => {
|
|
9
|
+
expect(shellPwaEnabled({ editable: false, state: "published" })).toBe(true);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("is false in the editor even when published", () => {
|
|
13
|
+
expect(shellPwaEnabled({ editable: true, state: "published" })).toBe(false);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("is false for a draft/preview deploy", () => {
|
|
17
|
+
expect(shellPwaEnabled({ editable: false, state: "draft" })).toBe(false);
|
|
18
|
+
expect(shellPwaEnabled({ editable: false })).toBe(false); // state undefined
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("is false when pwa is explicitly opted out", () => {
|
|
22
|
+
expect(shellPwaEnabled({ pwa: false, editable: false, state: "published" })).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("defaults pwa to on when omitted", () => {
|
|
26
|
+
expect(shellPwaEnabled({ editable: false, state: "published" })).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Pure gating logic for TribeNestApp, kept React-free so it's unit-testable in the
|
|
2
|
+
// node test env (no jsdom). The PWA (service-worker registration + install
|
|
3
|
+
// prompt) must run ONLY on the live published site — never in the HMR editor
|
|
4
|
+
// (`editable`) or on a preview-<versionId>.* review Worker (state="draft").
|
|
5
|
+
export function shellPwaEnabled(opts: {
|
|
6
|
+
pwa?: boolean;
|
|
7
|
+
editable?: boolean;
|
|
8
|
+
state?: "draft" | "published";
|
|
9
|
+
}): boolean {
|
|
10
|
+
return (opts.pwa ?? true) && !opts.editable && opts.state === "published";
|
|
11
|
+
}
|