@tribe-nest/forge 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. package/package.json +42 -0
  2. package/src/client/createForgeClient.ts +54 -0
  3. package/src/content/ForgeContentProvider.tsx +118 -0
  4. package/src/content/ForgeEditBridge.tsx +134 -0
  5. package/src/content/editFlag.ts +23 -0
  6. package/src/content/index.ts +28 -0
  7. package/src/content/resolve.ts +67 -0
  8. package/src/content/types.ts +97 -0
  9. package/src/content/useField.ts +64 -0
  10. package/src/contexts/AppAuthContext.tsx +185 -0
  11. package/src/contexts/AudioPlayerContext.tsx +389 -0
  12. package/src/contexts/CartContext.tsx +131 -0
  13. package/src/contexts/PublicAuthContext.tsx +333 -0
  14. package/src/contexts/useAppAdminGuard.ts +56 -0
  15. package/src/css.d.ts +5 -0
  16. package/src/data/collectionParams.ts +57 -0
  17. package/src/data/queries/useAccount.ts +71 -0
  18. package/src/data/queries/useAccountSettings.ts +82 -0
  19. package/src/data/queries/useAnalytics.ts +86 -0
  20. package/src/data/queries/useAuthActions.ts +28 -0
  21. package/src/data/queries/useBlog.ts +103 -0
  22. package/src/data/queries/useBlogComments.ts +96 -0
  23. package/src/data/queries/useBroadcasts.ts +31 -0
  24. package/src/data/queries/useCertificates.ts +58 -0
  25. package/src/data/queries/useChat.ts +267 -0
  26. package/src/data/queries/useCoachingAvailability.ts +69 -0
  27. package/src/data/queries/useCoachingProducts.ts +67 -0
  28. package/src/data/queries/useCohortPage.ts +17 -0
  29. package/src/data/queries/useCollections.ts +211 -0
  30. package/src/data/queries/useCommunity.ts +794 -0
  31. package/src/data/queries/useCoupons.ts +20 -0
  32. package/src/data/queries/useCourseAccess.ts +50 -0
  33. package/src/data/queries/useCourses.ts +91 -0
  34. package/src/data/queries/useCurrencies.ts +33 -0
  35. package/src/data/queries/useDocuments.ts +208 -0
  36. package/src/data/queries/useDonations.ts +29 -0
  37. package/src/data/queries/useEngagement.ts +115 -0
  38. package/src/data/queries/useEvents.ts +69 -0
  39. package/src/data/queries/useFinalize.ts +109 -0
  40. package/src/data/queries/useForms.ts +31 -0
  41. package/src/data/queries/useInvoice.ts +65 -0
  42. package/src/data/queries/useLeadMagnet.ts +96 -0
  43. package/src/data/queries/useMembership.ts +20 -0
  44. package/src/data/queries/useNotifications.ts +267 -0
  45. package/src/data/queries/useOffers.ts +56 -0
  46. package/src/data/queries/useOrders.ts +71 -0
  47. package/src/data/queries/usePWA.ts +101 -0
  48. package/src/data/queries/usePageActions.ts +36 -0
  49. package/src/data/queries/usePaymentFlow.ts +80 -0
  50. package/src/data/queries/usePaymentLink.ts +47 -0
  51. package/src/data/queries/usePodcast.ts +85 -0
  52. package/src/data/queries/usePostCollections.ts +48 -0
  53. package/src/data/queries/usePosts.ts +94 -0
  54. package/src/data/queries/useProducts.ts +108 -0
  55. package/src/data/queries/useReplay.ts +19 -0
  56. package/src/data/queries/useReviews.ts +122 -0
  57. package/src/data/queries/useShipping.ts +109 -0
  58. package/src/data/queries/useSubscriptions.ts +66 -0
  59. package/src/data/queries/useWebsite.ts +128 -0
  60. package/src/data/queries/useWebsiteAgent.ts +100 -0
  61. package/src/index.ts +98 -0
  62. package/src/provider/ForgeAppProvider.tsx +149 -0
  63. package/src/provider/ForgeProvider.tsx +91 -0
  64. package/src/provider/SiteConfigProvider.tsx +27 -0
  65. package/src/server/_tests/buildPwaHead.spec.ts +182 -0
  66. package/src/server/_tests/buildWebManifest.spec.ts +313 -0
  67. package/src/server/_tests/forgeServiceWorkerScript.spec.ts +251 -0
  68. package/src/server/index.ts +260 -0
  69. package/src/server/jobs.ts +104 -0
  70. package/src/server/pwa.ts +332 -0
  71. package/src/types/index.ts +74 -0
  72. package/src/types/models.ts +1313 -0
  73. package/src/ui/analytics/ForgeAnalytics.tsx +208 -0
  74. package/src/ui/analytics/PageMetaPixel.tsx +92 -0
  75. package/src/ui/content/Editable.tsx +90 -0
  76. package/src/ui/format/PriceDisplay.tsx +142 -0
  77. package/src/ui/format/useFormatCurrency.ts +92 -0
  78. package/src/ui/headless/agent/useAiAgent.ts +106 -0
  79. package/src/ui/headless/auth/useLoginFlow.ts +72 -0
  80. package/src/ui/headless/auth/useSignupForm.ts +85 -0
  81. package/src/ui/headless/broadcast/useBroadcastWatch.ts +59 -0
  82. package/src/ui/headless/chat/attachmentKind.ts +27 -0
  83. package/src/ui/headless/chat/useChatAttachmentUpload.ts +132 -0
  84. package/src/ui/headless/chat/useChatRoom.ts +223 -0
  85. package/src/ui/headless/chat/useMessageThread.ts +60 -0
  86. package/src/ui/headless/checkout/useCheckout.ts +438 -0
  87. package/src/ui/headless/coaching/useCoachingBooking.ts +223 -0
  88. package/src/ui/headless/community/index.ts +20 -0
  89. package/src/ui/headless/community/lexical.ts +131 -0
  90. package/src/ui/headless/community/replyTree.ts +51 -0
  91. package/src/ui/headless/community/richLexical.ts +314 -0
  92. package/src/ui/headless/community/useCommunityAttachmentUpload.ts +137 -0
  93. package/src/ui/headless/community/useCommunitySpaceFeed.ts +122 -0
  94. package/src/ui/headless/community/useCommunityThread.ts +95 -0
  95. package/src/ui/headless/community/useMentionComposer.ts +105 -0
  96. package/src/ui/headless/community/useRichMentionComposer.ts +193 -0
  97. package/src/ui/headless/consent/useCookieConsent.ts +81 -0
  98. package/src/ui/headless/course/useCourseCheckout.ts +99 -0
  99. package/src/ui/headless/dialog.tsx +14 -0
  100. package/src/ui/headless/document/index.ts +7 -0
  101. package/src/ui/headless/document/useContractSignature.ts +68 -0
  102. package/src/ui/headless/document/useDocumentSigningFlow.ts +92 -0
  103. package/src/ui/headless/document/useOtpCodeInput.ts +86 -0
  104. package/src/ui/headless/document/useSignaturePad.ts +180 -0
  105. package/src/ui/headless/donation/Donation.tsx +115 -0
  106. package/src/ui/headless/donation/DonationContext.tsx +223 -0
  107. package/src/ui/headless/donation/fees.ts +15 -0
  108. package/src/ui/headless/donation/index.ts +10 -0
  109. package/src/ui/headless/engagement/usePostEngagement.ts +119 -0
  110. package/src/ui/headless/event/useEventCheckout.ts +141 -0
  111. package/src/ui/headless/forms/useContactForm.ts +58 -0
  112. package/src/ui/headless/forms/useEmailListForm.ts +75 -0
  113. package/src/ui/headless/forms/useSectionedForm.ts +256 -0
  114. package/src/ui/headless/index.ts +60 -0
  115. package/src/ui/headless/invoice/useInvoicePayment.ts +48 -0
  116. package/src/ui/headless/memberHome/index.ts +7 -0
  117. package/src/ui/headless/memberHome/useMemberHomeNav.ts +82 -0
  118. package/src/ui/headless/membership/MembershipGate.tsx +39 -0
  119. package/src/ui/headless/membership/useMembershipCheckout.ts +121 -0
  120. package/src/ui/headless/offer/Offer.tsx +88 -0
  121. package/src/ui/headless/offer/OfferContext.tsx +162 -0
  122. package/src/ui/headless/offer/index.ts +8 -0
  123. package/src/ui/headless/paymentLink/usePaymentLinkPayment.ts +46 -0
  124. package/src/ui/headless/podcast/usePodcastPlayer.ts +96 -0
  125. package/src/ui/headless/posts/index.ts +13 -0
  126. package/src/ui/headless/posts/upsell.ts +41 -0
  127. package/src/ui/headless/posts/useMemberPostsFeed.ts +110 -0
  128. package/src/ui/headless/posts/usePostCollectionDetail.ts +56 -0
  129. package/src/ui/headless/posts/usePostCollectionsList.ts +90 -0
  130. package/src/ui/headless/posts/usePostItem.ts +81 -0
  131. package/src/ui/headless/reviews/index.ts +6 -0
  132. package/src/ui/headless/reviews/useHelpfulVote.ts +45 -0
  133. package/src/ui/headless/reviews/useReviewForm.ts +108 -0
  134. package/src/ui/headless/reviews/useReviewsList.ts +79 -0
  135. package/src/ui/headless/work/index.ts +25 -0
  136. package/src/ui/headless/work/useWorkPortal.ts +349 -0
  137. package/src/ui/index.ts +144 -0
  138. package/src/ui/payment/ForgePaymentProvider.tsx +45 -0
  139. package/src/ui/payment/ForgeStripePayment.tsx +171 -0
  140. package/src/ui/shell/TribeNestApp.tsx +121 -0
  141. package/src/ui/shell/shellGating.spec.ts +28 -0
  142. package/src/ui/shell/shellGating.ts +11 -0
  143. package/src/ui/styled/AccountDashboard.tsx +575 -0
  144. package/src/ui/styled/AiAgentWidget.tsx +152 -0
  145. package/src/ui/styled/AudioPlayer.tsx +123 -0
  146. package/src/ui/styled/BlogCategory.tsx +79 -0
  147. package/src/ui/styled/BlogComments.tsx +236 -0
  148. package/src/ui/styled/BlogList.tsx +101 -0
  149. package/src/ui/styled/BlogPost.tsx +160 -0
  150. package/src/ui/styled/BlogPostFooter.tsx +33 -0
  151. package/src/ui/styled/BotProtection.tsx +101 -0
  152. package/src/ui/styled/Button.tsx +88 -0
  153. package/src/ui/styled/Cart.tsx +276 -0
  154. package/src/ui/styled/ChatRoom.tsx +252 -0
  155. package/src/ui/styled/Checkout.tsx +988 -0
  156. package/src/ui/styled/CheckoutConfirmation.tsx +452 -0
  157. package/src/ui/styled/CoachingBooking.tsx +605 -0
  158. package/src/ui/styled/CoachingConfirmation.tsx +151 -0
  159. package/src/ui/styled/CoachingDetail.tsx +99 -0
  160. package/src/ui/styled/CohortPage.tsx +82 -0
  161. package/src/ui/styled/ConfirmSubscription.tsx +134 -0
  162. package/src/ui/styled/Confirmation.tsx +171 -0
  163. package/src/ui/styled/ContactForm.tsx +81 -0
  164. package/src/ui/styled/CookieConsent.tsx +239 -0
  165. package/src/ui/styled/CourseAccess.tsx +169 -0
  166. package/src/ui/styled/CourseCheckout.tsx +425 -0
  167. package/src/ui/styled/CourseConfirmation.tsx +191 -0
  168. package/src/ui/styled/CourseDetail.tsx +140 -0
  169. package/src/ui/styled/CoursesGrid.tsx +85 -0
  170. package/src/ui/styled/CurrencySwitcher.tsx +51 -0
  171. package/src/ui/styled/DonationButton.tsx +247 -0
  172. package/src/ui/styled/DonationPage.tsx +29 -0
  173. package/src/ui/styled/EmailListForm.tsx +88 -0
  174. package/src/ui/styled/EventConfirmation.tsx +228 -0
  175. package/src/ui/styled/EventCountdown.tsx +98 -0
  176. package/src/ui/styled/EventDetail.tsx +141 -0
  177. package/src/ui/styled/EventTickets.tsx +548 -0
  178. package/src/ui/styled/EventsList.tsx +83 -0
  179. package/src/ui/styled/ForgotPasswordForm.tsx +105 -0
  180. package/src/ui/styled/FormRenderer.tsx +249 -0
  181. package/src/ui/styled/InstallBanner.tsx +114 -0
  182. package/src/ui/styled/InvoiceConfirmation.tsx +50 -0
  183. package/src/ui/styled/InvoicePayment.tsx +141 -0
  184. package/src/ui/styled/LeadMagnet.tsx +93 -0
  185. package/src/ui/styled/Loading.tsx +56 -0
  186. package/src/ui/styled/LoginForm.tsx +134 -0
  187. package/src/ui/styled/Markdown.tsx +103 -0
  188. package/src/ui/styled/MembershipCheckout.tsx +197 -0
  189. package/src/ui/styled/MembershipTiers.tsx +115 -0
  190. package/src/ui/styled/OfferButton.tsx +181 -0
  191. package/src/ui/styled/PageActions.tsx +124 -0
  192. package/src/ui/styled/PaymentLinkConfirmation.tsx +50 -0
  193. package/src/ui/styled/PaymentLinkPayment.tsx +85 -0
  194. package/src/ui/styled/Paywall.tsx +54 -0
  195. package/src/ui/styled/PlayButton.tsx +48 -0
  196. package/src/ui/styled/PodcastEpisode.tsx +137 -0
  197. package/src/ui/styled/PodcastList.tsx +58 -0
  198. package/src/ui/styled/PodcastShow.tsx +164 -0
  199. package/src/ui/styled/PostsFeed.tsx +63 -0
  200. package/src/ui/styled/ProductDetail.tsx +616 -0
  201. package/src/ui/styled/ProductGrid.tsx +82 -0
  202. package/src/ui/styled/PushOptIn.tsx +65 -0
  203. package/src/ui/styled/PwaRegistration.tsx +58 -0
  204. package/src/ui/styled/ReactionBar.tsx +93 -0
  205. package/src/ui/styled/ReplayList.tsx +134 -0
  206. package/src/ui/styled/ResetPasswordForm.tsx +127 -0
  207. package/src/ui/styled/ReviewForm.tsx +292 -0
  208. package/src/ui/styled/ReviewsSection.tsx +260 -0
  209. package/src/ui/styled/SectionedFormRenderer.tsx +146 -0
  210. package/src/ui/styled/SignupForm.tsx +109 -0
  211. package/src/ui/styled/UserMenu.tsx +166 -0
  212. package/src/ui/styled/chat/ChatAttachments.tsx +172 -0
  213. package/src/ui/styled/chat/ChatComposer.tsx +153 -0
  214. package/src/ui/styled/chat/ChatThreadPanel.tsx +94 -0
  215. package/src/ui/styled/chat/MessageBubble.tsx +66 -0
  216. package/src/ui/styled/chat/NewDmDialog.tsx +78 -0
  217. package/src/ui/styled/community/CommunityComposer.tsx +358 -0
  218. package/src/ui/styled/community/CommunityFeed.tsx +483 -0
  219. package/src/ui/styled/community/CommunityLeaderboard.tsx +109 -0
  220. package/src/ui/styled/community/CommunityMediaGallery.tsx +116 -0
  221. package/src/ui/styled/community/CommunityMemberProfile.tsx +285 -0
  222. package/src/ui/styled/community/CommunityNotifications.tsx +222 -0
  223. package/src/ui/styled/community/CommunityPostCard.tsx +165 -0
  224. package/src/ui/styled/community/CommunityPostDetail.tsx +337 -0
  225. package/src/ui/styled/community/CommunityReactionBar.tsx +107 -0
  226. package/src/ui/styled/community/CommunityRichText.tsx +157 -0
  227. package/src/ui/styled/community/CommunitySpaces.tsx +174 -0
  228. package/src/ui/styled/community/MemberAvatar.tsx +74 -0
  229. package/src/ui/styled/community/index.ts +23 -0
  230. package/src/ui/styled/community/util.ts +15 -0
  231. package/src/ui/styled/members/MemberHomeLayout.tsx +98 -0
  232. package/src/ui/styled/members/MemberPostCard.tsx +392 -0
  233. package/src/ui/styled/members/MemberPostsFeed.tsx +219 -0
  234. package/src/ui/styled/members/PostCollectionDetail.tsx +227 -0
  235. package/src/ui/styled/members/PostCollectionsGrid.tsx +287 -0
  236. package/src/ui/styled/members/index.ts +7 -0
  237. package/src/ui/styled/rich-text.css +150 -0
  238. package/src/ui/styled/work/WorkInviteAccept.tsx +222 -0
  239. package/src/ui/styled/work/WorkPortalProjects.tsx +163 -0
  240. package/src/ui/styled/work/WorkProjectInvoices.tsx +113 -0
  241. package/src/ui/styled/work/WorkProjectReport.tsx +66 -0
  242. package/src/ui/styled/work/WorkReportBody.tsx +327 -0
  243. package/src/ui/styled/work/WorkTaskAttachments.tsx +67 -0
  244. package/src/ui/styled/work/WorkTaskComments.tsx +151 -0
  245. package/src/ui/styled/work/WorkTaskDetail.tsx +92 -0
  246. package/src/ui/styled/work/WorkTokenReport.tsx +49 -0
  247. package/src/ui/styled/work/index.ts +10 -0
  248. package/src/ui/theme/ForgeThemeProvider.tsx +121 -0
  249. package/src/ui/theme/contrast.ts +22 -0
  250. package/src/utils/analyticsBus.ts +31 -0
  251. package/src/utils/attribution.ts +68 -0
  252. package/src/utils/cookieConsent.ts +83 -0
  253. package/src/utils/landing.ts +153 -0
  254. package/src/utils/metaPixel.ts +122 -0
  255. package/src/utils/structuredData.ts +126 -0
@@ -0,0 +1,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
+ };