@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,14 @@
|
|
|
1
|
+
// Unstyled dialog primitives — thin re-export of Radix Dialog so the headless
|
|
2
|
+
// blocks get portal + focus-trap + a11y + keyboard for free, with zero styling.
|
|
3
|
+
export {
|
|
4
|
+
Root as DialogRoot,
|
|
5
|
+
Trigger as DialogTrigger,
|
|
6
|
+
Portal as DialogPortal,
|
|
7
|
+
Overlay as DialogOverlay,
|
|
8
|
+
Content as DialogContent,
|
|
9
|
+
Close as DialogClose,
|
|
10
|
+
Title as DialogTitle,
|
|
11
|
+
Description as DialogDescription,
|
|
12
|
+
} from "@radix-ui/react-dialog";
|
|
13
|
+
|
|
14
|
+
export { Slot } from "@radix-ui/react-slot";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Public document-signing headless primitives (Tier 1). NOTE: this is a
|
|
2
|
+
// lane-local aggregator — the integrator re-exports it from
|
|
3
|
+
// `ui/headless/index.ts` (`export * from "./document"`).
|
|
4
|
+
export { useDocumentSigningFlow, type DocumentSigningStage } from "./useDocumentSigningFlow";
|
|
5
|
+
export { useSignaturePad, type SignaturePadApi, type UseSignaturePadOptions } from "./useSignaturePad";
|
|
6
|
+
export { useOtpCodeInput, OTP_LENGTH } from "./useOtpCodeInput";
|
|
7
|
+
export { useContractSignature, type SignatureMode } from "./useContractSignature";
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { getDocumentApiErrorMessage, useSignDocument } from "../../../data/queries/useDocuments";
|
|
3
|
+
|
|
4
|
+
export type SignatureMode = "draw" | "type";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Headless contract signature capture: draw (pair with `useSignaturePad`) or
|
|
8
|
+
* type. The signer's legal name is set by the sender (admin) and is read-only
|
|
9
|
+
* here — exposed as `legalName` for display, never editable and never sent to
|
|
10
|
+
* the server (the backend derives the signed name from the invited recipient).
|
|
11
|
+
* Both modes require the explicit e-signature consent checkbox (the backend
|
|
12
|
+
* rejects sign requests without `consent: true`; typed mode omits
|
|
13
|
+
* `signatureImage`). `submit` resolves to `{ ok, error }` so the view layer
|
|
14
|
+
* decides how to surface failures.
|
|
15
|
+
*/
|
|
16
|
+
export function useContractSignature(
|
|
17
|
+
token: string,
|
|
18
|
+
{
|
|
19
|
+
defaultName = "",
|
|
20
|
+
getDrawnSignature,
|
|
21
|
+
}: {
|
|
22
|
+
/** The signer's read-only legal name (the invited `recipient.name`). */
|
|
23
|
+
defaultName?: string;
|
|
24
|
+
/** Returns the drawn PNG data-URL (a `useSignaturePad`'s `toDataURL`) —
|
|
25
|
+
* required for draw mode. */
|
|
26
|
+
getDrawnSignature?: () => string | null;
|
|
27
|
+
} = {},
|
|
28
|
+
) {
|
|
29
|
+
const sign = useSignDocument(token);
|
|
30
|
+
const [mode, setMode] = useState<SignatureMode>("draw");
|
|
31
|
+
// Read-only — the sender sets the legal name; the signer cannot change it.
|
|
32
|
+
const legalName = defaultName;
|
|
33
|
+
const [consent, setConsent] = useState(false);
|
|
34
|
+
const [hasInk, setHasInk] = useState(false);
|
|
35
|
+
|
|
36
|
+
const canSubmit = consent && (mode === "type" || hasInk) && !sign.isPending;
|
|
37
|
+
|
|
38
|
+
const submit = async (): Promise<{ ok: boolean; error?: string }> => {
|
|
39
|
+
if (!consent) return { ok: false, error: "Please agree to the electronic signature consent." };
|
|
40
|
+
let signatureImage: string | undefined;
|
|
41
|
+
if (mode === "draw") {
|
|
42
|
+
const dataUrl = getDrawnSignature?.();
|
|
43
|
+
if (!dataUrl) return { ok: false, error: "Draw your signature, or switch to the typed option." };
|
|
44
|
+
signatureImage = dataUrl;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
await sign.mutateAsync({ signatureImage });
|
|
48
|
+
return { ok: true };
|
|
49
|
+
} catch (error) {
|
|
50
|
+
return { ok: false, error: getDocumentApiErrorMessage(error, "Signing failed. Please try again.") };
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
mode,
|
|
56
|
+
setMode,
|
|
57
|
+
/** The signer's read-only legal name (set by the sender). */
|
|
58
|
+
legalName,
|
|
59
|
+
consent,
|
|
60
|
+
setConsent,
|
|
61
|
+
/** Wire to the signature pad's `hasInk` (drives draw-mode submit gating). */
|
|
62
|
+
hasInk,
|
|
63
|
+
setHasInk,
|
|
64
|
+
canSubmit,
|
|
65
|
+
isSigning: sign.isPending,
|
|
66
|
+
submit,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useAcceptDocument,
|
|
3
|
+
useDeclineDocument,
|
|
4
|
+
useDownloadSignedDocument,
|
|
5
|
+
usePublicDocument,
|
|
6
|
+
useRequestDocumentOtp,
|
|
7
|
+
useConfirmDocumentOtp,
|
|
8
|
+
useSignDocument,
|
|
9
|
+
type PublicDocumentResponse,
|
|
10
|
+
} from "../../../data/queries/useDocuments";
|
|
11
|
+
|
|
12
|
+
/** Which screen the public signing page should render. */
|
|
13
|
+
export type DocumentSigningStage =
|
|
14
|
+
| "loading" // document still loading
|
|
15
|
+
| "invalid" // bad/unknown/expired token (fetch failed)
|
|
16
|
+
| "terminal" // document reached a final status (signed/accepted/declined/expired/converted)
|
|
17
|
+
| "waiting" // doc still open but THIS recipient already acted (multi-signer wait)
|
|
18
|
+
| "otp" // email verification required before acting
|
|
19
|
+
| "sign" // contract, ready for this signer
|
|
20
|
+
| "accept" // proposal, ready to accept/decline
|
|
21
|
+
| "view-only"; // recipient is a viewer — no action panel
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Headless public document-signing flow (`/documents/:token` — the emailed
|
|
25
|
+
* magic link IS the identity, no login). Owns the whole state machine:
|
|
26
|
+
* loading → invalid → terminal banners (signed/accepted/declined/expired/
|
|
27
|
+
* converted) → per-recipient waiting → OTP gate → contract-sign or
|
|
28
|
+
* proposal-accept panel, with viewers locked to read-only. Exposes the
|
|
29
|
+
* document/recipient/pricing data plus every action (request/confirm OTP,
|
|
30
|
+
* sign, accept, decline, download signed PDF) as mutations.
|
|
31
|
+
*/
|
|
32
|
+
export function useDocumentSigningFlow(token: string) {
|
|
33
|
+
const query = usePublicDocument(token);
|
|
34
|
+
const data: PublicDocumentResponse | undefined = query.data;
|
|
35
|
+
|
|
36
|
+
const requestOtp = useRequestDocumentOtp(token);
|
|
37
|
+
const confirmOtp = useConfirmDocumentOtp(token);
|
|
38
|
+
const sign = useSignDocument(token);
|
|
39
|
+
const accept = useAcceptDocument(token);
|
|
40
|
+
const decline = useDeclineDocument(token);
|
|
41
|
+
const download = useDownloadSignedDocument(token, data?.document.title);
|
|
42
|
+
|
|
43
|
+
const doc = data?.document ?? null;
|
|
44
|
+
const recipient = data?.recipient ?? null;
|
|
45
|
+
|
|
46
|
+
const isTerminal = !!doc && ["signed", "accepted", "declined", "expired", "converted"].includes(doc.status);
|
|
47
|
+
const recipientDone = !!recipient && ["signed", "accepted", "declined"].includes(recipient.status);
|
|
48
|
+
const needsOtp = !!recipient && recipient.requireOtp && !recipient.emailVerified;
|
|
49
|
+
const isViewer = recipient?.role === "viewer";
|
|
50
|
+
const actionable = !!doc && !isTerminal && !recipientDone && ["sent", "viewed"].includes(doc.status);
|
|
51
|
+
|
|
52
|
+
const stage: DocumentSigningStage = query.isLoading
|
|
53
|
+
? "loading"
|
|
54
|
+
: query.error || !data || !doc || !recipient
|
|
55
|
+
? "invalid"
|
|
56
|
+
: isTerminal
|
|
57
|
+
? "terminal"
|
|
58
|
+
: recipientDone || !actionable
|
|
59
|
+
? "waiting"
|
|
60
|
+
: isViewer
|
|
61
|
+
? "view-only"
|
|
62
|
+
: needsOtp
|
|
63
|
+
? "otp"
|
|
64
|
+
: doc.kind === "contract"
|
|
65
|
+
? "sign"
|
|
66
|
+
: "accept";
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
stage,
|
|
70
|
+
// Data
|
|
71
|
+
document: doc,
|
|
72
|
+
recipient,
|
|
73
|
+
pricingOptions: data?.pricingOptions ?? [],
|
|
74
|
+
profile: data?.profile ?? null,
|
|
75
|
+
/** The pricing option the acceptor picked (terminal accepted state). */
|
|
76
|
+
selectedPricingOption: data?.pricingOptions.find((o) => o.selected) ?? null,
|
|
77
|
+
// Derived flags (also encoded in `stage`, exposed for custom renders)
|
|
78
|
+
isTerminal,
|
|
79
|
+
recipientDone,
|
|
80
|
+
needsOtp,
|
|
81
|
+
isViewer,
|
|
82
|
+
actionable,
|
|
83
|
+
kindLabel: doc?.kind === "contract" ? "contract" : "proposal",
|
|
84
|
+
// Actions
|
|
85
|
+
requestOtp,
|
|
86
|
+
confirmOtp,
|
|
87
|
+
sign,
|
|
88
|
+
accept,
|
|
89
|
+
decline,
|
|
90
|
+
download,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { useCallback, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export const OTP_LENGTH = 6;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Headless N-digit OTP input state machine (segmented boxes): per-digit state,
|
|
7
|
+
* paste of the whole code into any box, auto-focus advance, backspace-to-
|
|
8
|
+
* previous, and auto-submit when all digits are filled. Consumer renders the
|
|
9
|
+
* inputs and spreads `getInputProps(index)`.
|
|
10
|
+
*/
|
|
11
|
+
export function useOtpCodeInput({
|
|
12
|
+
length = OTP_LENGTH,
|
|
13
|
+
onComplete,
|
|
14
|
+
disabled = false,
|
|
15
|
+
}: {
|
|
16
|
+
length?: number;
|
|
17
|
+
/** Called with the full code once every digit is filled. */
|
|
18
|
+
onComplete?: (code: string) => void;
|
|
19
|
+
/** Disable all inputs (e.g. while the confirm mutation is pending). */
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
} = {}) {
|
|
22
|
+
const [digits, setDigits] = useState<string[]>(Array(length).fill(""));
|
|
23
|
+
const inputsRef = useRef<(HTMLInputElement | null)[]>([]);
|
|
24
|
+
|
|
25
|
+
const reset = useCallback(() => {
|
|
26
|
+
setDigits(Array(length).fill(""));
|
|
27
|
+
inputsRef.current[0]?.focus();
|
|
28
|
+
}, [length]);
|
|
29
|
+
|
|
30
|
+
const setDigit = useCallback(
|
|
31
|
+
(index: number, raw: string) => {
|
|
32
|
+
// Support paste of the full code into any box. State updaters must stay
|
|
33
|
+
// pure (React may replay them), so focus + auto-submit happen out here on
|
|
34
|
+
// a locally computed `next` instead of inside setDigits.
|
|
35
|
+
const clean = raw.replace(/\D/g, "");
|
|
36
|
+
if (!clean) {
|
|
37
|
+
const next = [...digits];
|
|
38
|
+
next[index] = "";
|
|
39
|
+
setDigits(next);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const next = [...digits];
|
|
43
|
+
for (let i = 0; i < clean.length && index + i < length; i++) {
|
|
44
|
+
next[index + i] = clean[i];
|
|
45
|
+
}
|
|
46
|
+
setDigits(next);
|
|
47
|
+
const focusIndex = Math.min(index + clean.length, length - 1);
|
|
48
|
+
inputsRef.current[focusIndex]?.focus();
|
|
49
|
+
if (next.every((d) => d !== "") && !disabled) {
|
|
50
|
+
onComplete?.(next.join(""));
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
[digits, length, disabled, onComplete],
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const getInputProps = useCallback(
|
|
57
|
+
(index: number) => ({
|
|
58
|
+
ref: (el: HTMLInputElement | null) => {
|
|
59
|
+
inputsRef.current[index] = el;
|
|
60
|
+
},
|
|
61
|
+
type: "text" as const,
|
|
62
|
+
inputMode: "numeric" as const,
|
|
63
|
+
autoComplete: index === 0 ? "one-time-code" : "off",
|
|
64
|
+
maxLength: length,
|
|
65
|
+
value: digits[index],
|
|
66
|
+
disabled,
|
|
67
|
+
onChange: (e: React.ChangeEvent<HTMLInputElement>) => setDigit(index, e.target.value),
|
|
68
|
+
onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
69
|
+
if (e.key === "Backspace" && !digits[index] && index > 0) {
|
|
70
|
+
inputsRef.current[index - 1]?.focus();
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"aria-label": `Digit ${index + 1}`,
|
|
74
|
+
}),
|
|
75
|
+
[digits, length, disabled, setDigit],
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
digits,
|
|
80
|
+
/** The code entered so far (join of the digits). */
|
|
81
|
+
code: digits.join(""),
|
|
82
|
+
isComplete: digits.every((d) => d !== ""),
|
|
83
|
+
getInputProps,
|
|
84
|
+
reset,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export interface UseSignaturePadOptions {
|
|
4
|
+
/** Stroke color. The canvas needs a concrete color — pass the theme's text
|
|
5
|
+
* color. Default near-black. */
|
|
6
|
+
inkColor?: string;
|
|
7
|
+
/** Stroke width in CSS pixels. Default 2.5. */
|
|
8
|
+
lineWidth?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SignaturePadApi {
|
|
12
|
+
/** Spread onto the `<canvas>`: wires pointer drawing (mouse + touch +
|
|
13
|
+
* stylus), pointer capture, and `touch-action: none`. Give the canvas an
|
|
14
|
+
* explicit CSS height — the hook manages the backing-store size. */
|
|
15
|
+
getCanvasProps: () => {
|
|
16
|
+
ref: (el: HTMLCanvasElement | null) => void;
|
|
17
|
+
onPointerDown: (event: React.PointerEvent<HTMLCanvasElement>) => void;
|
|
18
|
+
onPointerMove: (event: React.PointerEvent<HTMLCanvasElement>) => void;
|
|
19
|
+
onPointerUp: (event: React.PointerEvent<HTMLCanvasElement>) => void;
|
|
20
|
+
onPointerCancel: (event: React.PointerEvent<HTMLCanvasElement>) => void;
|
|
21
|
+
style: React.CSSProperties;
|
|
22
|
+
};
|
|
23
|
+
/** True once the pad has any ink — reactive, drive placeholder/submit state. */
|
|
24
|
+
hasInk: boolean;
|
|
25
|
+
/** PNG data-URL of the drawn signature, or null when the pad is empty. */
|
|
26
|
+
toDataURL: () => string | null;
|
|
27
|
+
clear: () => void;
|
|
28
|
+
isEmpty: () => boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Headless signature draw pad (dependency-free — no signature-pad package).
|
|
33
|
+
* Pointer events cover mouse + touch + stylus; `touch-action: none` stops the
|
|
34
|
+
* page from scrolling mid-stroke; the backing store is scaled for
|
|
35
|
+
* devicePixelRatio so exported PNGs stay crisp, and ink survives resizes
|
|
36
|
+
* (e.g. phone rotation). Consumer renders the canvas + chrome, spreads
|
|
37
|
+
* `getCanvasProps()`, and submits `toDataURL()` as the `signatureImage`.
|
|
38
|
+
*/
|
|
39
|
+
export function useSignaturePad(options: UseSignaturePadOptions = {}): SignaturePadApi {
|
|
40
|
+
const { inkColor = "#111111", lineWidth = 2.5 } = options;
|
|
41
|
+
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
|
42
|
+
const observerRef = useRef<ResizeObserver | null>(null);
|
|
43
|
+
const drawingRef = useRef(false);
|
|
44
|
+
const lastPointRef = useRef<{ x: number; y: number } | null>(null);
|
|
45
|
+
const hasInkRef = useRef(false);
|
|
46
|
+
const [hasInk, setHasInk] = useState(false);
|
|
47
|
+
|
|
48
|
+
const setupCanvas = useCallback(() => {
|
|
49
|
+
const canvas = canvasRef.current;
|
|
50
|
+
if (!canvas) return;
|
|
51
|
+
const rect = canvas.getBoundingClientRect();
|
|
52
|
+
const dpr = window.devicePixelRatio || 1;
|
|
53
|
+
// Preserve existing ink across resizes (e.g. phone rotation).
|
|
54
|
+
const previous = hasInkRef.current ? canvas.toDataURL("image/png") : null;
|
|
55
|
+
canvas.width = Math.max(1, Math.round(rect.width * dpr));
|
|
56
|
+
canvas.height = Math.max(1, Math.round(rect.height * dpr));
|
|
57
|
+
const ctx = canvas.getContext("2d");
|
|
58
|
+
if (!ctx) return;
|
|
59
|
+
ctx.scale(dpr, dpr);
|
|
60
|
+
ctx.lineWidth = lineWidth;
|
|
61
|
+
ctx.lineCap = "round";
|
|
62
|
+
ctx.lineJoin = "round";
|
|
63
|
+
ctx.strokeStyle = inkColor;
|
|
64
|
+
if (previous) {
|
|
65
|
+
const img = new Image();
|
|
66
|
+
img.onload = () => ctx.drawImage(img, 0, 0, rect.width, rect.height);
|
|
67
|
+
img.src = previous;
|
|
68
|
+
}
|
|
69
|
+
}, [inkColor, lineWidth]);
|
|
70
|
+
|
|
71
|
+
// Re-apply stroke settings when the ink color/width props change.
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (canvasRef.current) setupCanvas();
|
|
74
|
+
}, [setupCanvas]);
|
|
75
|
+
|
|
76
|
+
const attachCanvas = useCallback(
|
|
77
|
+
(el: HTMLCanvasElement | null) => {
|
|
78
|
+
observerRef.current?.disconnect();
|
|
79
|
+
observerRef.current = null;
|
|
80
|
+
canvasRef.current = el;
|
|
81
|
+
if (!el) return;
|
|
82
|
+
setupCanvas();
|
|
83
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
84
|
+
const observer = new ResizeObserver(() => setupCanvas());
|
|
85
|
+
observer.observe(el);
|
|
86
|
+
observerRef.current = observer;
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
[setupCanvas],
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
useEffect(() => () => observerRef.current?.disconnect(), []);
|
|
93
|
+
|
|
94
|
+
const markInk = useCallback((value: boolean) => {
|
|
95
|
+
if (hasInkRef.current === value) return;
|
|
96
|
+
hasInkRef.current = value;
|
|
97
|
+
setHasInk(value);
|
|
98
|
+
}, []);
|
|
99
|
+
|
|
100
|
+
const pointFromEvent = (event: React.PointerEvent<HTMLCanvasElement>) => {
|
|
101
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
102
|
+
return { x: event.clientX - rect.left, y: event.clientY - rect.top };
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const onPointerDown = useCallback((event: React.PointerEvent<HTMLCanvasElement>) => {
|
|
106
|
+
event.preventDefault();
|
|
107
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
108
|
+
drawingRef.current = true;
|
|
109
|
+
lastPointRef.current = pointFromEvent(event);
|
|
110
|
+
}, []);
|
|
111
|
+
|
|
112
|
+
const onPointerMove = useCallback(
|
|
113
|
+
(event: React.PointerEvent<HTMLCanvasElement>) => {
|
|
114
|
+
if (!drawingRef.current) return;
|
|
115
|
+
const canvas = canvasRef.current;
|
|
116
|
+
const ctx = canvas?.getContext("2d");
|
|
117
|
+
const last = lastPointRef.current;
|
|
118
|
+
if (!canvas || !ctx || !last) return;
|
|
119
|
+
const point = pointFromEvent(event);
|
|
120
|
+
ctx.beginPath();
|
|
121
|
+
ctx.moveTo(last.x, last.y);
|
|
122
|
+
ctx.lineTo(point.x, point.y);
|
|
123
|
+
ctx.stroke();
|
|
124
|
+
lastPointRef.current = point;
|
|
125
|
+
markInk(true);
|
|
126
|
+
},
|
|
127
|
+
[markInk],
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const onPointerUp = useCallback(
|
|
131
|
+
(event: React.PointerEvent<HTMLCanvasElement>) => {
|
|
132
|
+
if (!drawingRef.current) return;
|
|
133
|
+
// A tap without movement still leaves a dot.
|
|
134
|
+
const canvas = canvasRef.current;
|
|
135
|
+
const ctx = canvas?.getContext("2d");
|
|
136
|
+
const last = lastPointRef.current;
|
|
137
|
+
if (ctx && last) {
|
|
138
|
+
ctx.beginPath();
|
|
139
|
+
ctx.arc(last.x, last.y, 1.25, 0, Math.PI * 2);
|
|
140
|
+
ctx.fillStyle = inkColor;
|
|
141
|
+
ctx.fill();
|
|
142
|
+
markInk(true);
|
|
143
|
+
}
|
|
144
|
+
drawingRef.current = false;
|
|
145
|
+
lastPointRef.current = null;
|
|
146
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
147
|
+
},
|
|
148
|
+
[inkColor, markInk],
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const clear = useCallback(() => {
|
|
152
|
+
const canvas = canvasRef.current;
|
|
153
|
+
const ctx = canvas?.getContext("2d");
|
|
154
|
+
if (!canvas || !ctx) return;
|
|
155
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
156
|
+
markInk(false);
|
|
157
|
+
}, [markInk]);
|
|
158
|
+
|
|
159
|
+
const toDataURL = useCallback(() => {
|
|
160
|
+
const canvas = canvasRef.current;
|
|
161
|
+
if (!canvas || !hasInkRef.current) return null;
|
|
162
|
+
return canvas.toDataURL("image/png");
|
|
163
|
+
}, []);
|
|
164
|
+
|
|
165
|
+
const isEmpty = useCallback(() => !hasInkRef.current, []);
|
|
166
|
+
|
|
167
|
+
const getCanvasProps = useCallback(
|
|
168
|
+
() => ({
|
|
169
|
+
ref: attachCanvas,
|
|
170
|
+
onPointerDown,
|
|
171
|
+
onPointerMove,
|
|
172
|
+
onPointerUp,
|
|
173
|
+
onPointerCancel: onPointerUp,
|
|
174
|
+
style: { touchAction: "none" as const, cursor: "crosshair" as const, display: "block" as const, width: "100%" },
|
|
175
|
+
}),
|
|
176
|
+
[attachCanvas, onPointerDown, onPointerMove, onPointerUp],
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
return { getCanvasProps, hasInk, toDataURL, clear, isEmpty };
|
|
180
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
import {
|
|
3
|
+
DialogRoot,
|
|
4
|
+
DialogTrigger,
|
|
5
|
+
DialogPortal,
|
|
6
|
+
DialogOverlay,
|
|
7
|
+
DialogContent,
|
|
8
|
+
DialogClose,
|
|
9
|
+
DialogTitle,
|
|
10
|
+
} from "../dialog";
|
|
11
|
+
import {
|
|
12
|
+
DonationProvider,
|
|
13
|
+
useDonationContext,
|
|
14
|
+
type DonationProviderProps,
|
|
15
|
+
} from "./DonationContext";
|
|
16
|
+
|
|
17
|
+
/** Render-prop payload for `<Donation.PaymentStep>`. The app supplies the
|
|
18
|
+
* actual payment UI (e.g. Stripe Elements) — the SDK owns only the data. */
|
|
19
|
+
export interface DonationPaymentRenderProps {
|
|
20
|
+
clientSecret?: string;
|
|
21
|
+
returnUrl: string;
|
|
22
|
+
amount: number;
|
|
23
|
+
onSucceeded: () => void;
|
|
24
|
+
onFailed: (message?: string) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type DonationComponent = ((props: DonationProviderProps) => React.ReactElement) & {
|
|
28
|
+
Trigger: (props: { children: ReactNode; asChild?: boolean }) => React.ReactElement;
|
|
29
|
+
Modal: (props: {
|
|
30
|
+
children: ReactNode;
|
|
31
|
+
className?: string;
|
|
32
|
+
overlayClassName?: string;
|
|
33
|
+
style?: CSSProperties;
|
|
34
|
+
overlayStyle?: CSSProperties;
|
|
35
|
+
}) => React.ReactElement;
|
|
36
|
+
Title: (props: { children: ReactNode; className?: string }) => React.ReactElement;
|
|
37
|
+
Close: (props: { children: ReactNode; asChild?: boolean }) => React.ReactElement;
|
|
38
|
+
AmountStep: (props: { children: ReactNode }) => React.ReactElement | null;
|
|
39
|
+
PaymentStep: (props: {
|
|
40
|
+
children: ReactNode | ((p: DonationPaymentRenderProps) => ReactNode);
|
|
41
|
+
}) => React.ReactElement | null;
|
|
42
|
+
Result: (props: { children: ReactNode }) => React.ReactElement | null;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function DonationRoot({ children }: { children: ReactNode }) {
|
|
46
|
+
const { open, setOpen } = useDonationContext();
|
|
47
|
+
return (
|
|
48
|
+
<DialogRoot open={open} onOpenChange={setOpen}>
|
|
49
|
+
{children}
|
|
50
|
+
</DialogRoot>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Headless donation flow. Owns interaction state, the multi-step machine, fee
|
|
56
|
+
* math and payment orchestration; renders no styling. Compose the slots and
|
|
57
|
+
* style them yourself, or use the Tier-2 `<DonationButton>`.
|
|
58
|
+
*/
|
|
59
|
+
export const Donation = (({ donationId, returnUrl, onSuccess, children }: DonationProviderProps) => (
|
|
60
|
+
<DonationProvider donationId={donationId} returnUrl={returnUrl} onSuccess={onSuccess}>
|
|
61
|
+
<DonationRoot>{children}</DonationRoot>
|
|
62
|
+
</DonationProvider>
|
|
63
|
+
)) as DonationComponent;
|
|
64
|
+
|
|
65
|
+
Donation.Trigger = function DonationTrigger({ children, asChild }) {
|
|
66
|
+
return <DialogTrigger asChild={asChild}>{children}</DialogTrigger>;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
Donation.Modal = function DonationModal({ children, className, overlayClassName, style, overlayStyle }) {
|
|
70
|
+
return (
|
|
71
|
+
<DialogPortal>
|
|
72
|
+
<DialogOverlay className={overlayClassName} style={overlayStyle} />
|
|
73
|
+
<DialogContent className={className} style={style}>
|
|
74
|
+
{children}
|
|
75
|
+
</DialogContent>
|
|
76
|
+
</DialogPortal>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
Donation.Title = function DonationModalTitle({ children, className }) {
|
|
81
|
+
return <DialogTitle className={className}>{children}</DialogTitle>;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
Donation.Close = function DonationModalClose({ children, asChild }) {
|
|
85
|
+
return <DialogClose asChild={asChild}>{children}</DialogClose>;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
Donation.AmountStep = function DonationAmountStep({ children }) {
|
|
89
|
+
const { step } = useDonationContext();
|
|
90
|
+
return step === "amount" ? <>{children}</> : null;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
Donation.PaymentStep = function DonationPaymentStep({ children }) {
|
|
94
|
+
const ctx = useDonationContext();
|
|
95
|
+
if (ctx.step !== "payment") return null;
|
|
96
|
+
if (typeof children === "function") {
|
|
97
|
+
return (
|
|
98
|
+
<>
|
|
99
|
+
{children({
|
|
100
|
+
clientSecret: ctx.startResult?.paymentSecret,
|
|
101
|
+
returnUrl: ctx.returnUrl,
|
|
102
|
+
amount: ctx.startResult?.totalAmount ?? ctx.estimatedTotal,
|
|
103
|
+
onSucceeded: ctx.onPaymentSucceeded,
|
|
104
|
+
onFailed: ctx.onPaymentFailed,
|
|
105
|
+
})}
|
|
106
|
+
</>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
return <>{children}</>;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
Donation.Result = function DonationResult({ children }) {
|
|
113
|
+
const { step } = useDonationContext();
|
|
114
|
+
return step === "result" ? <>{children}</> : null;
|
|
115
|
+
};
|