@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,452 @@
1
+ import { useEffect, useState } from "react";
2
+ import {
3
+ AlertCircle,
4
+ AlertTriangle,
5
+ ArrowRight,
6
+ CheckCircle,
7
+ Clock,
8
+ Loader2,
9
+ Package,
10
+ ShoppingBag,
11
+ Truck,
12
+ XCircle,
13
+ } from "lucide-react";
14
+ import { useForgeTheme } from "../theme/ForgeThemeProvider";
15
+ import { readableTextOn } from "../theme/contrast";
16
+ import { useAmountFormatter } from "../format/useFormatCurrency";
17
+ import { ConfirmationStage, ConfirmationCard, CheckSeal, WarnSeal, Perforation, ConfirmationRow, alpha } from "./Confirmation";
18
+ import { useOrderFinalize } from "../../data/queries/useFinalize";
19
+ import { useCart } from "../../contexts/CartContext";
20
+ import { usePublicAuth } from "../../contexts/PublicAuthContext";
21
+ import { OrderStatus, ProductDeliveryType, type IPublicOrder } from "../../types/models";
22
+
23
+ export interface CheckoutConfirmationProps {
24
+ orderId?: string;
25
+ /**
26
+ * The Stripe `redirect_status` appended to the return URL (`succeeded` /
27
+ * `failed` / `processing`). When `failed`, the page shows the failed state
28
+ * immediately instead of waiting on the order status.
29
+ */
30
+ redirectStatus?: string;
31
+ /**
32
+ * Formats a monetary amount for display. Receives the order's own currency as a
33
+ * second argument so the shell can honor it (defaults to plain USD-style).
34
+ */
35
+ formatAmount?: (n: number, currency?: string) => string;
36
+ /** "Continue shopping" link. Default `/i/store`. */
37
+ explorePath?: string;
38
+ /** "View your orders" link. Default `/i/account`. */
39
+ accountPath?: string;
40
+ /** Where "Return to checkout" links on a failed payment. Default `/i/checkout`. */
41
+ checkoutPath?: string;
42
+ }
43
+
44
+ // A completed payment — anything at or past "paid" in fulfillment.
45
+ const PAID_STATES = new Set<OrderStatus>([
46
+ OrderStatus.Paid,
47
+ OrderStatus.LabelPurchased,
48
+ OrderStatus.Shipped,
49
+ OrderStatus.Delivered,
50
+ OrderStatus.Processed,
51
+ ]);
52
+ const FAILED_STATES = new Set<OrderStatus>([OrderStatus.PaymentFailed, OrderStatus.Failed, OrderStatus.Cancelled]);
53
+ const PENDING_STATES = new Set<OrderStatus>([OrderStatus.InitiatedPayment, OrderStatus.Processing]);
54
+
55
+ const DELIVERY_LABEL: Record<ProductDeliveryType, string> = {
56
+ [ProductDeliveryType.Physical]: "Physical delivery",
57
+ [ProductDeliveryType.Service]: "Service delivery",
58
+ [ProductDeliveryType.Digital]: "Digital delivery",
59
+ };
60
+
61
+ /** Short date only, e.g. "Jun 30, 2026". */
62
+ function formatDate(value: string | number | Date): string {
63
+ return new Date(value).toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" });
64
+ }
65
+
66
+ /** Title-case an order status ("payment_failed" → "Payment Failed"). */
67
+ function formatStatus(status: OrderStatus): string {
68
+ return status.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
69
+ }
70
+
71
+ function statusIcon(status: OrderStatus, color: string) {
72
+ const p = { size: 14, color };
73
+ switch (status) {
74
+ case OrderStatus.Paid:
75
+ case OrderStatus.Processed:
76
+ case OrderStatus.LabelPurchased:
77
+ return <CheckCircle {...p} />;
78
+ case OrderStatus.Shipped:
79
+ return <Truck {...p} />;
80
+ case OrderStatus.Delivered:
81
+ return <Package {...p} />;
82
+ case OrderStatus.Processing:
83
+ case OrderStatus.InitiatedPayment:
84
+ return <Clock {...p} />;
85
+ case OrderStatus.PaymentFailed:
86
+ case OrderStatus.Failed:
87
+ case OrderStatus.Cancelled:
88
+ return <XCircle {...p} />;
89
+ default:
90
+ return <AlertCircle {...p} />;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Post-checkout confirmation. Finalizes the order on load, then renders a themed
96
+ * "receipt" that accounts for every order state — success (with the fulfillment
97
+ * status), still-processing (polls the gateway webhook race), or failed/not-found
98
+ * — with the full order breakdown (delivery groups, gifts, variants, totals).
99
+ */
100
+ export function CheckoutConfirmation({
101
+ orderId,
102
+ redirectStatus,
103
+ formatAmount,
104
+ explorePath = "/i/store",
105
+ accountPath = "/i/account",
106
+ checkoutPath = "/i/checkout",
107
+ }: CheckoutConfirmationProps) {
108
+ const theme = useForgeTheme();
109
+ const formatAmt = useAmountFormatter(formatAmount);
110
+ const text = theme.colors.text;
111
+ const primary = theme.colors.primary;
112
+ const { cartItems, clearCart } = useCart();
113
+ const { user } = usePublicAuth();
114
+
115
+ const stripeFailed = redirectStatus === "failed";
116
+ const [gaveUp, setGaveUp] = useState(false);
117
+ const { data: order, isLoading, isError } = useOrderFinalize({ orderId }, { pollWhilePending: !stripeFailed && !gaveUp });
118
+
119
+ const status = order?.status;
120
+ const isPaid = !stripeFailed && !!status && PAID_STATES.has(status);
121
+ const isFailed = stripeFailed || (!!status && FAILED_STATES.has(status));
122
+ const isPending = !isPaid && !isFailed && !!status && PENDING_STATES.has(status);
123
+
124
+ // Stop polling after a while — the order settles server-side regardless.
125
+ useEffect(() => {
126
+ if (!isPending) return;
127
+ const t = setTimeout(() => setGaveUp(true), 30_000);
128
+ return () => clearTimeout(t);
129
+ }, [isPending]);
130
+
131
+ // Clear the cart once the order is confirmed paid.
132
+ useEffect(() => {
133
+ if (isPaid && cartItems.length > 0) clearCart();
134
+ }, [isPaid, cartItems.length, clearCart]);
135
+
136
+ // ── Loading (initial finalize) ──
137
+ if (isLoading && !stripeFailed) {
138
+ return (
139
+ <ConfirmationStage>
140
+ <div style={{ textAlign: "center", color: alpha(text, 0.75) }}>
141
+ <Loader2 size={40} color={primary} style={{ animation: "cc-spin 1s linear infinite" }} />
142
+ <style>{"@keyframes cc-spin{to{transform:rotate(360deg)}}"}</style>
143
+ <p style={{ marginTop: 16, fontSize: 15 }}>Confirming your payment…</p>
144
+ </div>
145
+ </ConfirmationStage>
146
+ );
147
+ }
148
+
149
+ // ── Not found (nothing to finalize / bad link) ──
150
+ if (!order && !stripeFailed) {
151
+ return (
152
+ <ConfirmationStage>
153
+ <ConfirmationCard width={440}>
154
+ <div style={{ padding: "40px 30px", textAlign: "center" }}>
155
+ <WarnSeal icon={<AlertTriangle size={34} color="#f59e0b" />} />
156
+ <h1 style={{ fontSize: 24, fontWeight: 800, margin: "22px 0 8px" }}>Order not found</h1>
157
+ <p style={{ fontSize: 15, color: alpha(text, 0.7), margin: "0 0 24px" }}>
158
+ {isError
159
+ ? "We couldn’t confirm this order. If you were charged, your receipt will still arrive by email."
160
+ : "This confirmation link looks incomplete or has expired."}
161
+ </p>
162
+ <Action href={explorePath} primary={primary}>
163
+ Continue shopping <ArrowRight size={16} />
164
+ </Action>
165
+ </div>
166
+ </ConfirmationCard>
167
+ </ConfirmationStage>
168
+ );
169
+ }
170
+
171
+ const currency = order?.currency || undefined;
172
+ const fmt = (n: number) => formatAmt(n, currency);
173
+ const totalDisplay =
174
+ order && order.totalAmountInChargedCurrency
175
+ ? formatAmt(order.totalAmountInChargedCurrency, order.chargedCurrency || currency)
176
+ : order
177
+ ? formatAmt(order.totalAmount, currency)
178
+ : "";
179
+ const groups = order?.deliveryGroups ?? [];
180
+ const subtotal = groups.reduce((s, g) => s + (g.subTotal || 0), 0);
181
+ const shipping = groups.reduce((s, g) => s + (g.shippingCost || 0), 0);
182
+ const discount = order?.discountAmount || 0;
183
+ const email = order?.customerEmail;
184
+
185
+ const eyebrow = isPaid ? "Order confirmed" : isFailed ? "Payment failed" : "Payment processing";
186
+ const eyebrowColor = isPaid ? primary : isFailed ? "#f59e0b" : primary;
187
+ const heading = isPaid ? "Thank you! 🛍" : isFailed ? "Payment not completed" : gaveUp ? "Still processing" : "Confirming your payment…";
188
+ const sub = isPaid
189
+ ? email
190
+ ? `We’ve emailed your receipt to ${email}.`
191
+ : "Your receipt is on its way."
192
+ : isFailed
193
+ ? "Your payment didn’t go through — you haven’t been charged. You can try again."
194
+ : gaveUp
195
+ ? "This is taking longer than usual. We’ll email your receipt as soon as it clears."
196
+ : "Hang tight — this only takes a few seconds.";
197
+
198
+ return (
199
+ <ConfirmationStage>
200
+ <ConfirmationCard width={600}>
201
+ {/* ── Ceremony ── */}
202
+ <div style={{ padding: "38px 30px 22px", textAlign: "center" }}>
203
+ {isPaid ? (
204
+ <CheckSeal />
205
+ ) : isFailed ? (
206
+ <WarnSeal icon={<AlertTriangle size={34} color="#f59e0b" />} />
207
+ ) : (
208
+ <div style={{ width: 84, height: 84, margin: "0 auto", display: "grid", placeItems: "center" }}>
209
+ <Loader2 size={54} color={primary} style={{ animation: "cc-spin 1s linear infinite" }} />
210
+ <style>{"@keyframes cc-spin{to{transform:rotate(360deg)}}"}</style>
211
+ </div>
212
+ )}
213
+
214
+ <p
215
+ className="cf-rise"
216
+ style={{
217
+ margin: "22px 0 6px",
218
+ fontSize: 12,
219
+ fontWeight: 700,
220
+ letterSpacing: "0.22em",
221
+ textTransform: "uppercase",
222
+ color: eyebrowColor,
223
+ animationDelay: "0.15s",
224
+ }}
225
+ >
226
+ {eyebrow}
227
+ </p>
228
+ <h1 className="cf-rise" style={{ fontSize: 29, fontWeight: 800, lineHeight: 1.12, margin: "0 0 8px", animationDelay: "0.22s" }}>
229
+ {heading}
230
+ </h1>
231
+ <p className="cf-rise" style={{ fontSize: 15, color: alpha(text, 0.7), margin: 0, animationDelay: "0.3s" }}>
232
+ {sub}
233
+ </p>
234
+
235
+ {/* Exact fulfillment status chip (paid → shipped → delivered, etc.). */}
236
+ {order && !isFailed && (
237
+ <div className="cf-rise" style={{ marginTop: 14, animationDelay: "0.36s" }}>
238
+ <span
239
+ style={{
240
+ display: "inline-flex",
241
+ alignItems: "center",
242
+ gap: 6,
243
+ padding: "5px 12px",
244
+ borderRadius: 999,
245
+ fontSize: 12,
246
+ fontWeight: 700,
247
+ color: primary,
248
+ background: alpha(primary, 0.1),
249
+ border: `1px solid ${alpha(primary, 0.24)}`,
250
+ }}
251
+ >
252
+ {statusIcon(order.status, primary)} {formatStatus(order.status)}
253
+ </span>
254
+ </div>
255
+ )}
256
+ </div>
257
+
258
+ {/* ── Failed: no summary, just a retry ── */}
259
+ {isFailed ? (
260
+ <div className="cf-rise" style={{ padding: "6px 30px 34px", animationDelay: "0.4s" }}>
261
+ <Action href={checkoutPath} primary={primary}>
262
+ Return to checkout <ArrowRight size={16} />
263
+ </Action>
264
+ </div>
265
+ ) : (
266
+ <>
267
+ {/* ── Order items (grouped by delivery) ── */}
268
+ <div className="cf-rise" style={{ padding: "0 30px 8px", animationDelay: "0.42s" }}>
269
+ <SectionLabel icon={<ShoppingBag size={16} color={primary} />} text="Your order" muted={alpha(text, 0.55)} />
270
+ {groups.length === 0 ? (
271
+ <p style={{ textAlign: "center", padding: "12px 0", color: alpha(text, 0.6), fontSize: 14 }}>
272
+ No items in this order.
273
+ </p>
274
+ ) : (
275
+ <div style={{ display: "flex", flexDirection: "column", gap: 18 }}>
276
+ {groups.map((g) => (
277
+ <GroupBlock key={g.id} group={g} fmt={fmt} theme={theme} />
278
+ ))}
279
+ </div>
280
+ )}
281
+ </div>
282
+
283
+ {/* ── Totals ── */}
284
+ <div className="cf-rise" style={{ padding: "12px 30px 0", animationDelay: "0.48s" }}>
285
+ <div style={{ paddingTop: 14, borderTop: `1px solid ${alpha(text, 0.1)}`, display: "flex", flexDirection: "column", gap: 6 }}>
286
+ <SummaryLine label="Subtotal" value={fmt(subtotal)} muted={alpha(text, 0.6)} />
287
+ {shipping > 0 && <SummaryLine label="Shipping" value={fmt(shipping)} muted={alpha(text, 0.6)} />}
288
+ {discount > 0 && <SummaryLine label="Discount" value={`−${fmt(discount)}`} muted={alpha(text, 0.6)} />}
289
+ {Number(order?.taxAmount || 0) > 0 && !order?.taxInclusive && (
290
+ <SummaryLine
291
+ label="Tax"
292
+ value={formatAmt(Number(order?.taxAmount), order?.chargedCurrency || currency)}
293
+ muted={alpha(text, 0.6)}
294
+ />
295
+ )}
296
+ </div>
297
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginTop: 12 }}>
298
+ <span style={{ fontWeight: 700 }}>Total {isPaid ? "paid" : ""}</span>
299
+ <span style={{ fontSize: 20, fontWeight: 800, color: primary }}>{totalDisplay}</span>
300
+ </div>
301
+ {Number(order?.taxAmount || 0) > 0 && order?.taxInclusive && (
302
+ <div style={{ textAlign: "right", fontSize: 12, color: alpha(text, 0.6), marginTop: 4 }}>
303
+ Includes {formatAmt(Number(order?.taxAmount), order?.chargedCurrency || currency)} tax
304
+ </div>
305
+ )}
306
+ </div>
307
+
308
+ <div style={{ padding: "20px 30px 0" }}>
309
+ <Perforation />
310
+ </div>
311
+
312
+ {/* ── Details ── */}
313
+ <div className="cf-rise" style={{ padding: "20px 30px 8px", animationDelay: "0.54s" }}>
314
+ <dl style={{ display: "grid", gridTemplateColumns: "1fr auto", rowGap: 10, columnGap: 16, margin: 0 }}>
315
+ <ConfirmationRow label="Order" value={`#${order!.id.slice(0, 8).toUpperCase()}`} mono />
316
+ <ConfirmationRow label="Date" value={formatDate(order!.createdAt)} />
317
+ {order!.customerName && <ConfirmationRow label="Name" value={order!.customerName} />}
318
+ {email && <ConfirmationRow label="Email" value={email} />}
319
+ </dl>
320
+ </div>
321
+
322
+ {/* ── Actions ── */}
323
+ <div className="cf-rise" style={{ padding: "22px 30px 30px", display: "flex", flexDirection: "column", gap: 10, animationDelay: "0.6s" }}>
324
+ <Action href={explorePath} primary={primary}>
325
+ Continue shopping <ArrowRight size={16} />
326
+ </Action>
327
+ {/* Orders live behind auth — only offer the link to signed-in buyers. */}
328
+ {user && (
329
+ <Action href={accountPath} primary={primary} ghost>
330
+ View your orders
331
+ </Action>
332
+ )}
333
+ </div>
334
+ </>
335
+ )}
336
+ </ConfirmationCard>
337
+ </ConfirmationStage>
338
+ );
339
+ }
340
+
341
+ function SectionLabel({ icon, text, muted }: { icon: React.ReactNode; text: string; muted: string }) {
342
+ return (
343
+ <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 12 }}>
344
+ {icon}
345
+ <span style={{ fontSize: 13, fontWeight: 700, letterSpacing: "0.08em", textTransform: "uppercase", color: muted }}>{text}</span>
346
+ </div>
347
+ );
348
+ }
349
+
350
+ function GroupBlock({
351
+ group,
352
+ fmt,
353
+ theme,
354
+ }: {
355
+ group: IPublicOrder["deliveryGroups"][number];
356
+ fmt: (n: number) => string;
357
+ theme: ReturnType<typeof useForgeTheme>;
358
+ }) {
359
+ const text = theme.colors.text;
360
+ const primary = theme.colors.primary;
361
+ return (
362
+ <div style={{ borderRadius: theme.cornerRadius, border: `1px solid ${alpha(primary, 0.16)}`, background: alpha(primary, 0.04), overflow: "hidden" }}>
363
+ {/* group header */}
364
+ <div style={{ padding: "10px 14px", borderBottom: `1px solid ${alpha(primary, 0.12)}` }}>
365
+ <div style={{ fontSize: 13, fontWeight: 700, color: text }}>{DELIVERY_LABEL[group.deliveryType]}</div>
366
+ <div style={{ fontSize: 12, color: alpha(text, 0.65), marginTop: 2 }}>
367
+ To {group.recipientName} ({group.recipientEmail})
368
+ </div>
369
+ {group.recipientMessage && (
370
+ <div style={{ fontSize: 12, color: alpha(text, 0.6), marginTop: 2, fontStyle: "italic" }}>“{group.recipientMessage}”</div>
371
+ )}
372
+ </div>
373
+ {/* items */}
374
+ <div style={{ display: "flex", flexDirection: "column" }}>
375
+ {group.items.map((item, i) => (
376
+ <div
377
+ key={`${item.productVariantId}-${item.isGift}-${item.recipientEmail || ""}-${i}`}
378
+ style={{ display: "flex", gap: 12, padding: "12px 14px", borderTop: i === 0 ? "none" : `1px solid ${alpha(primary, 0.1)}` }}
379
+ >
380
+ {item.coverImage ? (
381
+ <img
382
+ src={item.coverImage}
383
+ alt={item.title}
384
+ width={48}
385
+ height={48}
386
+ style={{ width: 48, height: 48, objectFit: "cover", borderRadius: Math.max(6, theme.cornerRadius - 2), flexShrink: 0 }}
387
+ />
388
+ ) : (
389
+ <div style={{ width: 48, height: 48, borderRadius: Math.max(6, theme.cornerRadius - 2), background: alpha(primary, 0.1), flexShrink: 0 }} />
390
+ )}
391
+ <div style={{ flex: 1, minWidth: 0 }}>
392
+ <div style={{ fontSize: 14, fontWeight: 600, color: text, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
393
+ {item.title}
394
+ </div>
395
+ <div style={{ fontSize: 12, color: alpha(text, 0.6), marginTop: 2 }}>
396
+ {fmt(item.price)} · Qty {item.quantity}
397
+ </div>
398
+ {item.color && item.size && (
399
+ <div style={{ fontSize: 12, marginTop: 4, display: "flex", alignItems: "center", gap: 6, color: alpha(text, 0.7) }}>
400
+ <span style={{ width: 12, height: 12, borderRadius: "50%", background: item.color, display: "inline-block", border: `1px solid ${alpha(text, 0.2)}` }} />
401
+ {item.size}
402
+ </div>
403
+ )}
404
+ {item.isGift && (
405
+ <div style={{ fontSize: 12, marginTop: 4, color: primary }}>
406
+ 🎁 Gift for {item.recipientName} ({item.recipientEmail})
407
+ </div>
408
+ )}
409
+ {item.recipientMessage && (
410
+ <div style={{ fontSize: 12, marginTop: 2, color: alpha(text, 0.6), fontStyle: "italic" }}>“{item.recipientMessage}”</div>
411
+ )}
412
+ </div>
413
+ <div style={{ fontSize: 14, fontWeight: 700, color: primary, whiteSpace: "nowrap" }}>{fmt(item.price * item.quantity)}</div>
414
+ </div>
415
+ ))}
416
+ </div>
417
+ </div>
418
+ );
419
+ }
420
+
421
+ function SummaryLine({ label, value, muted }: { label: string; value: string; muted: string }) {
422
+ return (
423
+ <div style={{ display: "flex", justifyContent: "space-between", fontSize: 14, color: muted }}>
424
+ <span>{label}</span>
425
+ <span>{value}</span>
426
+ </div>
427
+ );
428
+ }
429
+
430
+ function Action({ href, primary, ghost, children }: { href: string; primary: string; ghost?: boolean; children: React.ReactNode }) {
431
+ return (
432
+ <a
433
+ href={href}
434
+ style={{
435
+ display: "inline-flex",
436
+ alignItems: "center",
437
+ justifyContent: "center",
438
+ gap: 8,
439
+ width: "100%",
440
+ padding: "13px 18px",
441
+ borderRadius: 10,
442
+ fontWeight: 700,
443
+ textDecoration: "none",
444
+ background: ghost ? "transparent" : primary,
445
+ color: ghost ? primary : readableTextOn(primary),
446
+ border: ghost ? `1px solid ${alpha(primary, 0.3)}` : "none",
447
+ }}
448
+ >
449
+ {children}
450
+ </a>
451
+ );
452
+ }