@tribe-nest/forge 3.26.0 → 3.29.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 (90) hide show
  1. package/package.json +1 -1
  2. package/src/client/_tests/localeHeader.spec.tsx +98 -0
  3. package/src/client/activeLocale.ts +34 -0
  4. package/src/client/createForgeClient.ts +13 -0
  5. package/src/contexts/CartContext.tsx +76 -0
  6. package/src/contexts/_tests/CartContext.spec.tsx +210 -0
  7. package/src/data/queries/useCheckouts.ts +83 -0
  8. package/src/data/queries/useWebsite.ts +7 -0
  9. package/src/i18n/CONVENTION.md +184 -0
  10. package/src/i18n/_tests/translationKeys.spec.ts +153 -0
  11. package/src/i18n/de.json +960 -0
  12. package/src/i18n/en.json +960 -0
  13. package/src/i18n/index.ts +125 -0
  14. package/src/index.ts +21 -0
  15. package/src/provider/ForgeAppProvider.tsx +2 -1
  16. package/src/provider/SiteConfigProvider.tsx +11 -0
  17. package/src/ui/headless/checkout/_tests/cartResume.spec.ts +291 -0
  18. package/src/ui/headless/checkout/_tests/useCartResume.spec.tsx +294 -0
  19. package/src/ui/headless/checkout/resumedCoupon.ts +61 -0
  20. package/src/ui/headless/checkout/useCartResume.ts +340 -0
  21. package/src/ui/headless/checkout/useCheckout.ts +9 -2
  22. package/src/ui/headless/index.ts +18 -0
  23. package/src/ui/index.ts +2 -0
  24. package/src/ui/shell/TribeNestApp.tsx +36 -27
  25. package/src/ui/styled/AccountDashboard.tsx +197 -93
  26. package/src/ui/styled/AiAgentWidget.tsx +10 -6
  27. package/src/ui/styled/AudioPlayer.tsx +14 -6
  28. package/src/ui/styled/BlogCategory.tsx +4 -2
  29. package/src/ui/styled/BlogComments.tsx +27 -20
  30. package/src/ui/styled/BlogList.tsx +5 -3
  31. package/src/ui/styled/BlogPost.tsx +14 -11
  32. package/src/ui/styled/BundleConfirmation.tsx +32 -23
  33. package/src/ui/styled/CancellationTerms.tsx +3 -1
  34. package/src/ui/styled/Cart.tsx +32 -14
  35. package/src/ui/styled/ChatRoom.tsx +65 -20
  36. package/src/ui/styled/Checkout.tsx +151 -89
  37. package/src/ui/styled/CheckoutConfirmation.tsx +66 -35
  38. package/src/ui/styled/CoachingBooking.tsx +71 -44
  39. package/src/ui/styled/CoachingConfirmation.tsx +56 -19
  40. package/src/ui/styled/CoachingDetail.tsx +15 -11
  41. package/src/ui/styled/CohortPage.tsx +10 -8
  42. package/src/ui/styled/ConfirmSubscription.tsx +14 -8
  43. package/src/ui/styled/ContactForm.tsx +31 -12
  44. package/src/ui/styled/CookieConsent.tsx +19 -15
  45. package/src/ui/styled/CourseAccess.tsx +54 -47
  46. package/src/ui/styled/CourseCheckout.tsx +56 -38
  47. package/src/ui/styled/CourseConfirmation.tsx +39 -17
  48. package/src/ui/styled/CourseDetail.tsx +21 -15
  49. package/src/ui/styled/CoursesGrid.tsx +4 -2
  50. package/src/ui/styled/CurrencySwitcher.tsx +4 -1
  51. package/src/ui/styled/DiscountCode.tsx +22 -18
  52. package/src/ui/styled/DonationButton.tsx +52 -17
  53. package/src/ui/styled/DonationPage.tsx +4 -4
  54. package/src/ui/styled/EmailListForm.tsx +22 -9
  55. package/src/ui/styled/EventConfirmation.tsx +37 -14
  56. package/src/ui/styled/EventCountdown.tsx +10 -6
  57. package/src/ui/styled/EventDetail.tsx +8 -6
  58. package/src/ui/styled/EventSeriesDetail.tsx +18 -13
  59. package/src/ui/styled/EventTickets.tsx +232 -108
  60. package/src/ui/styled/EventWaitlist.tsx +42 -33
  61. package/src/ui/styled/EventsList.tsx +15 -13
  62. package/src/ui/styled/ForgotPasswordForm.tsx +12 -12
  63. package/src/ui/styled/FormRenderer.tsx +12 -7
  64. package/src/ui/styled/InstallBanner.tsx +21 -18
  65. package/src/ui/styled/InvoiceConfirmation.tsx +13 -5
  66. package/src/ui/styled/InvoicePayment.tsx +37 -16
  67. package/src/ui/styled/LeadMagnet.tsx +9 -7
  68. package/src/ui/styled/Loading.tsx +7 -1
  69. package/src/ui/styled/LoginForm.tsx +22 -14
  70. package/src/ui/styled/MembershipCheckout.tsx +32 -13
  71. package/src/ui/styled/MembershipTiers.tsx +27 -18
  72. package/src/ui/styled/OfferButton.tsx +33 -13
  73. package/src/ui/styled/PaymentLinkConfirmation.tsx +11 -5
  74. package/src/ui/styled/PaymentLinkPayment.tsx +30 -10
  75. package/src/ui/styled/PodcastEpisode.tsx +20 -9
  76. package/src/ui/styled/PodcastList.tsx +9 -3
  77. package/src/ui/styled/PodcastShow.tsx +29 -7
  78. package/src/ui/styled/PostsFeed.tsx +10 -8
  79. package/src/ui/styled/PresaleCode.tsx +8 -11
  80. package/src/ui/styled/ProductBrowseNav.tsx +21 -11
  81. package/src/ui/styled/ProductDetail.tsx +58 -28
  82. package/src/ui/styled/ReactionBar.tsx +4 -2
  83. package/src/ui/styled/ReplayList.tsx +24 -13
  84. package/src/ui/styled/ResetPasswordForm.tsx +27 -15
  85. package/src/ui/styled/ResumeCart.tsx +289 -0
  86. package/src/ui/styled/ReviewForm.tsx +52 -58
  87. package/src/ui/styled/SignupForm.tsx +29 -16
  88. package/src/ui/styled/TicketTransfer.tsx +55 -54
  89. package/src/ui/styled/UserMenu.tsx +16 -14
  90. package/src/ui/styled/_tests/ResumeCart.spec.tsx +183 -0
@@ -20,6 +20,7 @@ import {
20
20
  } from "../../../data/queries/useCheckouts";
21
21
  import { bundleCouponSummary, bundleReturnUrl } from "./bundleCoupon";
22
22
  import { holdExpiredMessage, isHoldExpiredError } from "./inventoryHold";
23
+ import { takeResumedCoupon } from "./resumedCoupon";
23
24
  import { useInventoryHold } from "./useInventoryHold";
24
25
  import { readAttributionRef } from "../../../utils/attribution";
25
26
  import { readLanding } from "../../../utils/landing";
@@ -191,8 +192,14 @@ export function useCheckout(opts: UseCheckoutOptions = {}) {
191
192
  [selectedShippingRates],
192
193
  );
193
194
 
194
- const [couponCode, setCouponCode] = useState("");
195
- const [showCouponInput, setShowCouponInput] = useState(false);
195
+ /**
196
+ * A code the buyer arrived with from a recovery link, prefilled but NOT
197
+ * applied: they still press Apply and the server re-evaluates it against
198
+ * today. The lazy initialiser consumes the stash, so a code cannot linger and
199
+ * prefill an unrelated checkout later in the same tab.
200
+ */
201
+ const [couponCode, setCouponCode] = useState(takeResumedCoupon);
202
+ const [showCouponInput, setShowCouponInput] = useState(() => couponCode.length > 0);
196
203
  const [couponError, setCouponError] = useState("");
197
204
  const [isApplyingCoupon, setIsApplyingCoupon] = useState(false);
198
205
  const [appliedCoupon, setAppliedCoupon] = useState<AppliedCoupon | null>(null);
@@ -29,6 +29,24 @@ export {
29
29
  type InventoryHoldState,
30
30
  type UseInventoryHoldOptions,
31
31
  } from "./checkout/useInventoryHold";
32
+ // Cart recovery: what a resume link does when the buyer clicks it. Shared so
33
+ // `apps/client` and a Forge code site cannot decide differently.
34
+ export {
35
+ useCartResume,
36
+ effectiveUnitCents,
37
+ partitionResumeLines,
38
+ resumeLineToCartItem,
39
+ resumeLinesToTicketItems,
40
+ type ResumePartition,
41
+ type UseCartResumeOptions,
42
+ type UseCartResumeResult,
43
+ type CartResumeStatus,
44
+ type ResumeLine,
45
+ type ResumedCheckout,
46
+ type ResumeLineOption,
47
+ type ResumeLineUnavailableReason,
48
+ } from "./checkout/useCartResume";
49
+ export { rememberResumedCoupon, takeResumedCoupon } from "./checkout/resumedCoupon";
32
50
  export {
33
51
  isHoldExpiredError,
34
52
  holdExpiredMessage,
package/src/ui/index.ts CHANGED
@@ -55,6 +55,8 @@ export { PresaleCodeField, type PresaleCodeFieldProps } from "./styled/PresaleCo
55
55
  export { MembershipCheckout, type MembershipCheckoutProps } from "./styled/MembershipCheckout";
56
56
  export { ForgeStripePayment } from "./payment/ForgeStripePayment";
57
57
  export { Cart, type CartProps } from "./styled/Cart";
58
+ /** The page a cart-recovery email lands on. Drop in at `/i/checkout/resume`. */
59
+ export { ResumeCart, type ResumeCartProps } from "./styled/ResumeCart";
58
60
  // The resolver is exported alongside the component because the Craft themes
59
61
  // are a separate rendering stack (Tailwind, not inline styles) and cannot use
60
62
  // the component — but the legacy `color`/`size` fallback must not diverge.
@@ -8,6 +8,7 @@ import { CookieConsent } from "../styled/CookieConsent";
8
8
  import { AiAgentWidget } from "../styled/AiAgentWidget";
9
9
  import { useThemeTokens } from "../theme/ForgeThemeProvider";
10
10
  import { useInitialSiteConfig } from "../../provider/SiteConfigProvider";
11
+ import { ForgeI18nProvider } from "../../i18n";
11
12
  import { PoweredBy } from "./PoweredBy";
12
13
  import { PreviewDiagnostics } from "./PreviewDiagnostics";
13
14
  import { shellAiAgentEnabled, shellPoweredByEnabled, shellPwaEnabled } from "./shellGating";
@@ -142,33 +143,41 @@ export function TribeNestApp({
142
143
  : null;
143
144
  return (
144
145
  <ForgeProvider editable={editable} {...forgeProps}>
145
- {/* First in the tree: it must render even if everything below it doesn't. */}
146
- {ssrFailure && <PreviewDiagnostics diagnostics={ssrFailure} />}
147
- {children}
148
- {/* "Powered by TribeNest" last in the page flow, so it sits below the
149
- tenant's own footer. Released builds only (live + published). */}
150
- <PoweredByGate editable={editable} state={state} />
151
- {/* Global playback bar — self-pins to the viewport bottom, renders nothing
152
- until a track loads. */}
153
- <AudioPlayer />
154
- {/* Auto page views + clicks (+ Meta Pixel, gated on marketing consent).
155
- First-party runs cookieless. Off in the editor so preview isn't counted. */}
156
- {analytics && <ForgeAnalytics enabled={!editable} firstPartyMode="cookieless" />}
157
- {/* Ad attribution capture (tracked-link ref + first-touch landing + visit
158
- beacon). Off in the editor so preview visits aren't credited. */}
159
- <AttributionCapture enabled={!editable} apiUrl={forgeProps.apiUrl} profileId={forgeProps.profileId} />
160
- {/* PWA — SW registration + install banner + (fallback) manifest link. */}
161
- {pwa && <PwaRegistration enabled={pwaEnabled} />}
162
- {pwa && <InstallBanner enabled={pwaEnabled} />}
163
- {pwa && <PwaHeadClient enabled={pwaEnabled} manifestHref={manifestHref} />}
164
- {/* Consent banner shown on first visit, re-openable from anywhere via
165
- useCookieConsent().reopen(). Hidden in the editor. */}
166
- {cookieConsent && !editable && <CookieConsent useTribeNestPrivacy />}
167
- {/* Website AI agent floating bubble, self-hiding when the profile has no
168
- agent enabled. Lives here rather than in each __root so enabling the
169
- agent in admin is enough to make it appear; before this it shipped only
170
- on the Craft stack and no code site ever rendered it. */}
171
- {shellAiAgentEnabled({ aiAgent, editable, appId: forgeProps.appId }) && <AiAgentWidget />}
146
+ {/* Forge's own copy, in the site's language. Mounted here rather than in
147
+ each tenant's __root.tsx, so a Forge upgrade is enough to give an
148
+ existing site i18n. `locale` is the ForgeProvider prop the site
149
+ already passes (absent or unsupported resolves to English). When
150
+ `profile_configurations.locale` lands on SiteConfig, that becomes the
151
+ source and this prop stays the override. */}
152
+ <ForgeI18nProvider locale={forgeProps.locale}>
153
+ {/* First in the tree: it must render even if everything below it doesn't. */}
154
+ {ssrFailure && <PreviewDiagnostics diagnostics={ssrFailure} />}
155
+ {children}
156
+ {/* "Powered by TribeNest" — last in the page flow, so it sits below the
157
+ tenant's own footer. Released builds only (live + published). */}
158
+ <PoweredByGate editable={editable} state={state} />
159
+ {/* Global playback bar self-pins to the viewport bottom, renders nothing
160
+ until a track loads. */}
161
+ <AudioPlayer />
162
+ {/* Auto page views + clicks (+ Meta Pixel, gated on marketing consent).
163
+ First-party runs cookieless. Off in the editor so preview isn't counted. */}
164
+ {analytics && <ForgeAnalytics enabled={!editable} firstPartyMode="cookieless" />}
165
+ {/* Ad attribution capture (tracked-link ref + first-touch landing + visit
166
+ beacon). Off in the editor so preview visits aren't credited. */}
167
+ <AttributionCapture enabled={!editable} apiUrl={forgeProps.apiUrl} profileId={forgeProps.profileId} />
168
+ {/* PWA SW registration + install banner + (fallback) manifest link. */}
169
+ {pwa && <PwaRegistration enabled={pwaEnabled} />}
170
+ {pwa && <InstallBanner enabled={pwaEnabled} />}
171
+ {pwa && <PwaHeadClient enabled={pwaEnabled} manifestHref={manifestHref} />}
172
+ {/* Consent banner shown on first visit, re-openable from anywhere via
173
+ useCookieConsent().reopen(). Hidden in the editor. */}
174
+ {cookieConsent && !editable && <CookieConsent useTribeNestPrivacy />}
175
+ {/* Website AI agent — floating bubble, self-hiding when the profile has no
176
+ agent enabled. Lives here rather than in each __root so enabling the
177
+ agent in admin is enough to make it appear; before this it shipped only
178
+ on the Craft stack and no code site ever rendered it. */}
179
+ {shellAiAgentEnabled({ aiAgent, editable, appId: forgeProps.appId }) && <AiAgentWidget />}
180
+ </ForgeI18nProvider>
172
181
  </ForgeProvider>
173
182
  );
174
183
  }