@tribe-nest/forge 3.54.0 → 3.58.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 (102) hide show
  1. package/package.json +4 -2
  2. package/src/_tests/workspaceAliases.ts +1 -0
  3. package/src/contexts/CartContext.tsx +88 -5
  4. package/src/data/queries/useCallBreakouts.ts +112 -0
  5. package/src/data/queries/useCheckouts.ts +15 -2
  6. package/src/data/queries/useCoachingProducts.ts +15 -1
  7. package/src/data/queries/useCourses.ts +15 -1
  8. package/src/data/queries/useEvents.ts +80 -1
  9. package/src/data/queries/useMembership.ts +176 -4
  10. package/src/data/queries/useMyBookings.ts +16 -0
  11. package/src/data/queries/useNotifications.ts +1 -1
  12. package/src/data/queries/useProducts.ts +15 -1
  13. package/src/data/queries/useSubscriptions.ts +21 -1
  14. package/src/i18n/de.json +153 -0
  15. package/src/i18n/en.json +153 -0
  16. package/src/index.ts +1 -0
  17. package/src/server/_tests/buildPwaHead.spec.ts +1 -1
  18. package/src/server/_tests/buildWebManifest.spec.ts +1 -1
  19. package/src/server/_tests/forgeServiceWorkerScript.spec.ts +1 -1
  20. package/src/server/index.ts +75 -1
  21. package/src/server/platformEvents.generated.ts +60 -0
  22. package/src/server/pwa.ts +1 -1
  23. package/src/types/models.ts +137 -0
  24. package/src/ui/analytics/ForgeAnalytics.tsx +55 -1
  25. package/src/ui/analytics/_tests/ForgeAnalytics.engagement.spec.tsx +78 -0
  26. package/src/ui/headless/checkout/_tests/bundleCoupon.spec.ts +3 -1
  27. package/src/ui/headless/checkout/_tests/cartResume.spec.ts +3 -10
  28. package/src/ui/headless/checkout/_tests/seatConflict.spec.ts +187 -0
  29. package/src/ui/headless/checkout/_tests/useCartResume.spec.tsx +3 -11
  30. package/src/ui/headless/checkout/_tests/useCheckoutSeats.spec.tsx +313 -0
  31. package/src/ui/headless/checkout/seatConflict.ts +117 -0
  32. package/src/ui/headless/checkout/useCheckout.ts +178 -55
  33. package/src/ui/headless/event/_tests/seatingFixture.ts +111 -0
  34. package/src/ui/headless/event/_tests/useEventCheckoutSeats.spec.tsx +443 -0
  35. package/src/ui/headless/event/_tests/useEventSeating.spec.tsx +154 -0
  36. package/src/ui/headless/event/useEventCheckout.ts +362 -22
  37. package/src/ui/headless/event/useEventSeating.ts +233 -0
  38. package/src/ui/headless/forms/_tests/usePhoneInput.spec.ts +187 -0
  39. package/src/ui/headless/forms/useEmailListForm.ts +37 -6
  40. package/src/ui/headless/forms/usePhoneInput.ts +294 -0
  41. package/src/ui/headless/forms/useSectionedForm.ts +2 -2
  42. package/src/ui/headless/index.ts +48 -1
  43. package/src/ui/headless/membership/_tests/membershipApplication.spec.ts +85 -0
  44. package/src/ui/headless/membership/_tests/membershipCheckoutRefetch.spec.tsx +2 -0
  45. package/src/ui/headless/membership/_tests/membershipTrial.spec.ts +61 -0
  46. package/src/ui/headless/membership/_tests/useMembershipCheckoutApplication.spec.tsx +253 -0
  47. package/src/ui/headless/membership/_tests/useMembershipCheckoutQuestionnaire.spec.tsx +241 -0
  48. package/src/ui/headless/membership/_tests/useMembershipCheckoutTrial.spec.tsx +165 -0
  49. package/src/ui/headless/membership/membershipApplication.ts +109 -0
  50. package/src/ui/headless/membership/membershipQuestionnaire.ts +44 -0
  51. package/src/ui/headless/membership/membershipTrial.ts +53 -0
  52. package/src/ui/headless/membership/useMembershipCheckout.ts +227 -8
  53. package/src/ui/index.ts +16 -0
  54. package/src/ui/media/CallStage.tsx +107 -10
  55. package/src/ui/media/_tests/CallBreakoutsPanel.spec.tsx +271 -0
  56. package/src/ui/media/_tests/CallStageBreakouts.spec.tsx +212 -0
  57. package/src/ui/media/_tests/callState.spec.ts +103 -1
  58. package/src/ui/media/callBreakouts.ts +57 -0
  59. package/src/ui/media/callState.ts +83 -23
  60. package/src/ui/media/index.ts +11 -0
  61. package/src/ui/media/panels/CallBreakoutsPanel.tsx +395 -0
  62. package/src/ui/media/panels/CallSidePanel.tsx +37 -4
  63. package/src/ui/media/stage/CallCenterNotice.tsx +5 -3
  64. package/src/ui/media/stage/CallControlsBar.tsx +46 -0
  65. package/src/ui/media/stage/CallTopBar.tsx +1 -1
  66. package/src/ui/payment/ForgePaymentProvider.tsx +14 -0
  67. package/src/ui/payment/ForgeStripePayment.tsx +78 -7
  68. package/src/ui/payment/_tests/ForgeStripePaymentTrial.spec.tsx +101 -0
  69. package/src/ui/payment/_tests/stripeIntentKind.spec.ts +43 -0
  70. package/src/ui/payment/stripeIntentKind.ts +32 -0
  71. package/src/ui/shell/TribeNestApp.tsx +1 -1
  72. package/src/ui/styled/AccountDashboard.tsx +165 -2
  73. package/src/ui/styled/BundleConfirmation.tsx +47 -2
  74. package/src/ui/styled/Cart.tsx +211 -135
  75. package/src/ui/styled/Checkout.tsx +163 -32
  76. package/src/ui/styled/EmailListForm.tsx +7 -9
  77. package/src/ui/styled/EventTickets.tsx +143 -4
  78. package/src/ui/styled/LoginForm.tsx +5 -3
  79. package/src/ui/styled/MembershipCheckout.tsx +596 -256
  80. package/src/ui/styled/MembershipTierCallout.tsx +17 -3
  81. package/src/ui/styled/MembershipTiers.tsx +148 -6
  82. package/src/ui/styled/PhoneInput.tsx +127 -0
  83. package/src/ui/styled/ProductGrid.tsx +14 -2
  84. package/src/ui/styled/SignupForm.tsx +4 -2
  85. package/src/ui/styled/_tests/AccountDashboardBookingCall.spec.tsx +36 -0
  86. package/src/ui/styled/_tests/AccountDashboardTrial.spec.tsx +105 -0
  87. package/src/ui/styled/_tests/CartSeats.spec.tsx +109 -0
  88. package/src/ui/styled/_tests/CheckoutSeatsConflict.spec.tsx +194 -0
  89. package/src/ui/styled/_tests/EventSeatPicker.spec.tsx +199 -0
  90. package/src/ui/styled/_tests/MembershipCheckout.spec.tsx +106 -0
  91. package/src/ui/styled/_tests/membershipTiersCuratedAccess.spec.tsx +139 -0
  92. package/src/ui/styled/_tests/membershipTiersTrial.spec.tsx +97 -0
  93. package/src/ui/styled/forge-utilities.css +67 -0
  94. package/src/ui/styled/seating/EventSeatPicker.tsx +190 -0
  95. package/src/ui/styled/seating/SeatLegend.tsx +73 -0
  96. package/src/ui/styled/seating/SeatMapCanvas.tsx +647 -0
  97. package/src/ui/styled/seating/paint.ts +79 -0
  98. package/src/ui/styled/seating/useSeatMapViewport.ts +248 -0
  99. package/src/ui/styled/seating/viewport.ts +86 -0
  100. package/src/utils/_tests/engagementTracker.spec.ts +111 -0
  101. package/src/utils/engagementTracker.ts +73 -0
  102. package/src/utils/landing.ts +1 -1
@@ -8,6 +8,7 @@ import { readableTextOn } from "../theme/contrast";
8
8
  import { useAmountFormatter } from "../format/useFormatCurrency";
9
9
  import { usePricesIncludeTax } from "../../data/queries/useWebsite";
10
10
  import { CartLineOptions } from "./CartLineOptions";
11
+ import { internalPath } from "../../runtime/pages";
11
12
 
12
13
  export interface CartProps {
13
14
  /** Custom cart icon (defaults to a shopping bag). */
@@ -122,114 +123,178 @@ export function Cart({ icon, checkoutPath = "/i/checkout", productHref, formatAm
122
123
  )}
123
124
  </button>
124
125
 
125
- {isCartOpen && typeof document !== "undefined" && createPortal(
126
- <>
127
- <style>{KEYFRAMES}</style>
128
- <div
129
- onClick={() => setCartOpen(false)}
130
- aria-label={translate("forge.cart.aria_close")}
131
- style={{ position: "fixed", inset: 0, zIndex: OVERLAY_Z, background: "rgba(0,0,0,0.3)" }}
132
- />
133
- <aside
134
- style={{
135
- position: "fixed",
136
- top: 0,
137
- right: 0,
138
- height: "100%",
139
- width: "100%",
140
- maxWidth: 420,
141
- zIndex: DRAWER_Z,
142
- background: t.background,
143
- color: t.text,
144
- borderLeft: `2px solid ${t.primary}`,
145
- boxShadow: "-8px 0 30px -20px rgba(0,0,0,0.5)",
146
- display: "flex",
147
- flexDirection: "column",
148
- fontFamily: t.fontFamily,
149
- animation: "forge-cart-slide .3s cubic-bezier(.4,0,.2,1)",
150
- }}
151
- >
126
+ {isCartOpen &&
127
+ typeof document !== "undefined" &&
128
+ createPortal(
129
+ <>
130
+ <style>{KEYFRAMES}</style>
152
131
  <div
132
+ onClick={() => setCartOpen(false)}
133
+ aria-label={translate("forge.cart.aria_close")}
134
+ style={{ position: "fixed", inset: 0, zIndex: OVERLAY_Z, background: "rgba(0,0,0,0.3)" }}
135
+ />
136
+ <aside
153
137
  style={{
138
+ position: "fixed",
139
+ top: 0,
140
+ right: 0,
141
+ height: "100%",
142
+ width: "100%",
143
+ maxWidth: 420,
144
+ zIndex: DRAWER_Z,
145
+ background: t.background,
146
+ color: t.text,
147
+ borderLeft: `2px solid ${t.primary}`,
148
+ boxShadow: "-8px 0 30px -20px rgba(0,0,0,0.5)",
154
149
  display: "flex",
155
- alignItems: "center",
156
- justifyContent: "space-between",
157
- padding: 16,
158
- borderBottom: `1px solid ${t.border}`,
150
+ flexDirection: "column",
151
+ fontFamily: t.fontFamily,
152
+ animation: "forge-cart-slide .3s cubic-bezier(.4,0,.2,1)",
159
153
  }}
160
154
  >
161
- <h2 style={{ fontSize: 18, fontWeight: 800, margin: 0, fontFamily: t.headingFontFamily }}>
162
- {translate("forge.cart.title")}
163
- </h2>
164
- <button
165
- type="button"
166
- onClick={() => setCartOpen(false)}
167
- aria-label={translate("forge.cart.aria_close")}
168
- style={{ background: "transparent", border: "none", cursor: "pointer", color: t.text }}
155
+ <div
156
+ style={{
157
+ display: "flex",
158
+ alignItems: "center",
159
+ justifyContent: "space-between",
160
+ padding: 16,
161
+ borderBottom: `1px solid ${t.border}`,
162
+ }}
169
163
  >
170
- <X size={22} />
171
- </button>
172
- </div>
164
+ <h2 style={{ fontSize: 18, fontWeight: 800, margin: 0, fontFamily: t.headingFontFamily }}>
165
+ {translate("forge.cart.title")}
166
+ </h2>
167
+ <button
168
+ type="button"
169
+ onClick={() => setCartOpen(false)}
170
+ aria-label={translate("forge.cart.aria_close")}
171
+ style={{ background: "transparent", border: "none", cursor: "pointer", color: t.text }}
172
+ >
173
+ <X size={22} />
174
+ </button>
175
+ </div>
173
176
 
174
- <div style={{ flex: 1, overflowY: "auto", padding: 16, display: "flex", flexDirection: "column", gap: 16 }}>
175
- {count === 0 && (
176
- <p style={{ textAlign: "center", opacity: 0.7 }}>{translate("forge.cart.empty_title")}</p>
177
- )}
177
+ <div
178
+ style={{ flex: 1, overflowY: "auto", padding: 16, display: "flex", flexDirection: "column", gap: 16 }}
179
+ >
180
+ {count === 0 && (
181
+ <p style={{ textAlign: "center", opacity: 0.7 }}>{translate("forge.cart.empty_title")}</p>
182
+ )}
178
183
 
179
- {ticketItems.map((ticket) => (
180
- <div
181
- key={ticket.eventId}
182
- data-testid="cart-ticket-line"
183
- style={{
184
- display: "flex",
185
- gap: 12,
186
- alignItems: "flex-start",
187
- borderBottom: `1px solid ${t.border}`,
188
- paddingBottom: 16,
189
- position: "relative",
190
- }}
191
- >
192
- <img
193
- src={ticket.coverImage || undefined}
194
- alt={ticket.eventTitle}
184
+ {ticketItems.map((ticket) => (
185
+ <div
186
+ key={ticket.eventId}
187
+ data-testid="cart-ticket-line"
195
188
  style={{
196
- width: 64,
197
- height: 64,
198
- objectFit: "cover",
199
- borderRadius: t.cornerRadius,
200
- background: `${t.primary}10`,
201
- flexShrink: 0,
189
+ display: "flex",
190
+ gap: 12,
191
+ alignItems: "flex-start",
192
+ borderBottom: `1px solid ${t.border}`,
193
+ paddingBottom: 16,
194
+ position: "relative",
202
195
  }}
203
- />
204
- <div style={{ flex: 1, minWidth: 0, paddingRight: 28 }}>
205
- <div style={{ fontWeight: 600, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
206
- {ticket.eventTitle}
207
- </div>
208
- {Object.entries(ticket.tickets).map(([id, qty]) => (
209
- <div key={id} style={{ fontSize: 12, marginTop: 4, opacity: 0.75 }}>
210
- {translate("forge.cart.ticket_line", {
211
- ticket_title: ticket.ticketMeta[id]?.title ?? translate("forge.cart.ticket_fallback_title"),
212
- quantity: qty,
213
- })}
196
+ >
197
+ <img
198
+ src={ticket.coverImage || undefined}
199
+ alt={ticket.eventTitle}
200
+ style={{
201
+ width: 64,
202
+ height: 64,
203
+ objectFit: "cover",
204
+ borderRadius: t.cornerRadius,
205
+ background: `${t.primary}10`,
206
+ flexShrink: 0,
207
+ }}
208
+ />
209
+ <div style={{ flex: 1, minWidth: 0, paddingRight: 28 }}>
210
+ <div
211
+ style={{ fontWeight: 600, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}
212
+ >
213
+ {ticket.eventTitle}
214
214
  </div>
215
- ))}
216
- <div style={{ fontSize: 14, marginTop: 4, opacity: 0.8 }}>{fmt(ticketTotal(ticket))}</div>
215
+ {Object.entries(ticket.tickets).map(([id, qty]) => {
216
+ // A reserved-seating tier: its quantity is its seat
217
+ // count, so the seats are listed by name and there is
218
+ // no stepper. Changing them happens on the event page,
219
+ // where the map is.
220
+ const seats = ticket.ticketMeta[id]?.seats ?? [];
221
+ return (
222
+ <div key={id}>
223
+ <div style={{ fontSize: 12, marginTop: 4, opacity: 0.75 }}>
224
+ {translate("forge.cart.ticket_line", {
225
+ ticket_title:
226
+ ticket.ticketMeta[id]?.title ?? translate("forge.cart.ticket_fallback_title"),
227
+ quantity: qty,
228
+ })}
229
+ </div>
230
+ {seats.length > 0 && (
231
+ <div
232
+ data-testid="cart-seat-chips"
233
+ style={{ display: "flex", flexWrap: "wrap", gap: 4, marginTop: 4 }}
234
+ >
235
+ {seats.map((seat) => (
236
+ <span
237
+ key={seat.id}
238
+ data-testid="cart-seat-chip"
239
+ style={{
240
+ padding: "2px 8px",
241
+ borderRadius: 999,
242
+ background: `${t.primary}14`,
243
+ color: t.text,
244
+ fontSize: 11,
245
+ lineHeight: 1.4,
246
+ }}
247
+ >
248
+ {seat.label}
249
+ </span>
250
+ ))}
251
+ </div>
252
+ )}
253
+ </div>
254
+ );
255
+ })}
256
+ {Object.values(ticket.ticketMeta).some((meta) => (meta.seats?.length ?? 0) > 0) && (
257
+ <a
258
+ href={internalPath("events/$slug", { slug: ticket.eventSlug || ticket.eventId })}
259
+ data-testid="cart-change-seats"
260
+ onClick={() => setCartOpen(false)}
261
+ style={{
262
+ display: "inline-block",
263
+ fontSize: 12,
264
+ marginTop: 6,
265
+ color: t.primary,
266
+ textDecoration: "underline",
267
+ }}
268
+ >
269
+ {translate("forge.cart.change_seats")}
270
+ </a>
271
+ )}
272
+ <div style={{ fontSize: 14, marginTop: 4, opacity: 0.8 }}>{fmt(ticketTotal(ticket))}</div>
273
+ </div>
274
+ <button
275
+ type="button"
276
+ // Removing the ticket takes its add-ons with it — they
277
+ // cannot be bought on their own, and leaving them would
278
+ // produce a checkout the server refuses.
279
+ onClick={() => removeTickets(ticket.eventId)}
280
+ aria-label={translate("forge.cart.aria_remove_tickets")}
281
+ style={{
282
+ position: "absolute",
283
+ top: 0,
284
+ right: 0,
285
+ background: "transparent",
286
+ border: "none",
287
+ cursor: "pointer",
288
+ color: t.primary,
289
+ padding: 4,
290
+ }}
291
+ >
292
+ <Trash2 size={16} />
293
+ </button>
217
294
  </div>
218
- <button
219
- type="button"
220
- // Removing the ticket takes its add-ons with it — they
221
- // cannot be bought on their own, and leaving them would
222
- // produce a checkout the server refuses.
223
- onClick={() => removeTickets(ticket.eventId)}
224
- aria-label={translate("forge.cart.aria_remove_tickets")}
225
- style={{ position: "absolute", top: 0, right: 0, background: "transparent", border: "none", cursor: "pointer", color: t.primary, padding: 4 }}
226
- >
227
- <Trash2 size={16} />
228
- </button>
229
- </div>
230
- ))}
295
+ ))}
231
296
 
232
- {cartItems.map((item) => (
297
+ {cartItems.map((item) => (
233
298
  <div
234
299
  key={item.productId + item.productVariantId + String(item.isGift) + (item.recipientEmail || "")}
235
300
  // Named so a test can assert on the LINE rather than the
@@ -300,55 +365,66 @@ export function Cart({ icon, checkoutPath = "/i/checkout", productHref, formatAm
300
365
  type="button"
301
366
  onClick={() => removeFromCart(item.productVariantId, item.isGift, item.recipientEmail)}
302
367
  aria-label={translate("forge.cart.aria_remove_item")}
303
- style={{ position: "absolute", top: 0, right: 0, background: "transparent", border: "none", cursor: "pointer", color: t.primary, padding: 4 }}
368
+ style={{
369
+ position: "absolute",
370
+ top: 0,
371
+ right: 0,
372
+ background: "transparent",
373
+ border: "none",
374
+ cursor: "pointer",
375
+ color: t.primary,
376
+ padding: 4,
377
+ }}
304
378
  >
305
379
  <Trash2 size={16} />
306
380
  </button>
307
381
  </div>
308
382
  ))}
309
- </div>
383
+ </div>
310
384
 
311
- {count > 0 && (
312
- <div
313
- style={{
314
- padding: 16,
315
- borderTop: `1px solid ${t.border}`,
316
- background: t.background,
317
- }}
318
- >
319
- <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 }}>
320
- <span style={{ fontSize: 18, fontWeight: 600 }}>{translate("forge.cart.total")}</span>
321
- <span style={{ fontSize: 18, fontWeight: 800, color: t.primary, textAlign: "right" }}>
322
- {fmt(total)}
323
- {pricesIncludeTax && (
324
- <span style={{ display: "block", fontSize: 11, fontWeight: 400, color: t.text, opacity: 0.65 }}>
325
- {translate("forge.cart.incl_tax")}
326
- </span>
327
- )}
328
- </span>
329
- </div>
330
- <button
331
- type="button"
332
- onClick={goCheckout}
385
+ {count > 0 && (
386
+ <div
333
387
  style={{
334
- width: "100%",
335
- padding: "12px 16px",
336
- borderRadius: t.cornerRadius,
337
- border: "none",
338
- fontWeight: 700,
339
- cursor: "pointer",
340
- background: t.primary,
341
- color: onPrimary,
388
+ padding: 16,
389
+ borderTop: `1px solid ${t.border}`,
390
+ background: t.background,
342
391
  }}
343
392
  >
344
- {translate("forge.cart.checkout")}
345
- </button>
346
- </div>
347
- )}
348
- </aside>
349
- </>,
350
- document.body,
351
- )}
393
+ <div
394
+ style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 }}
395
+ >
396
+ <span style={{ fontSize: 18, fontWeight: 600 }}>{translate("forge.cart.total")}</span>
397
+ <span style={{ fontSize: 18, fontWeight: 800, color: t.primary, textAlign: "right" }}>
398
+ {fmt(total)}
399
+ {pricesIncludeTax && (
400
+ <span style={{ display: "block", fontSize: 11, fontWeight: 400, color: t.text, opacity: 0.65 }}>
401
+ {translate("forge.cart.incl_tax")}
402
+ </span>
403
+ )}
404
+ </span>
405
+ </div>
406
+ <button
407
+ type="button"
408
+ onClick={goCheckout}
409
+ style={{
410
+ width: "100%",
411
+ padding: "12px 16px",
412
+ borderRadius: t.cornerRadius,
413
+ border: "none",
414
+ fontWeight: 700,
415
+ cursor: "pointer",
416
+ background: t.primary,
417
+ color: onPrimary,
418
+ }}
419
+ >
420
+ {translate("forge.cart.checkout")}
421
+ </button>
422
+ </div>
423
+ )}
424
+ </aside>
425
+ </>,
426
+ document.body,
427
+ )}
352
428
  </>
353
429
  );
354
430
  }