@tribe-nest/forge 3.21.0 → 3.23.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 (97) hide show
  1. package/package.json +2 -1
  2. package/src/data/queries/_tests/eventWaitlist.spec.ts +38 -0
  3. package/src/data/queries/_tests/passTransfers.spec.ts +100 -4
  4. package/src/data/queries/useAuthActions.ts +1 -1
  5. package/src/data/queries/useCheckouts.ts +5 -0
  6. package/src/data/queries/useEventWaitlist.ts +123 -5
  7. package/src/data/queries/useEvents.ts +82 -4
  8. package/src/data/queries/useMembership.ts +8 -2
  9. package/src/data/queries/useMyBookings.ts +37 -0
  10. package/src/data/queries/useMyTickets.ts +112 -0
  11. package/src/data/queries/useOrders.ts +10 -0
  12. package/src/data/queries/usePassTransfers.ts +73 -13
  13. package/src/data/queries/usePaymentFlow.ts +84 -18
  14. package/src/data/queries/useShipping.ts +5 -0
  15. package/src/data/queries/useSubscriptions.ts +50 -4
  16. package/src/index.ts +5 -0
  17. package/src/server/index.ts +52 -0
  18. package/src/server/platform.ts +1 -1
  19. package/src/server/platformEvents.generated.ts +33 -33
  20. package/src/server/pwa.ts +2 -2
  21. package/src/types/models.ts +203 -5
  22. package/src/types/paystack-inline.d.ts +45 -0
  23. package/src/ui/format/_tests/attendees.spec.ts +231 -0
  24. package/src/ui/format/_tests/membershipGate.spec.ts +220 -0
  25. package/src/ui/format/_tests/ticketAvailability.spec.ts +283 -0
  26. package/src/ui/format/attendees.ts +187 -0
  27. package/src/ui/format/membershipGate.ts +209 -0
  28. package/src/ui/format/ticketAvailability.ts +226 -0
  29. package/src/ui/headless/calendar/_tests/useAddToCalendar.spec.ts +83 -0
  30. package/src/ui/headless/calendar/useAddToCalendar.ts +46 -5
  31. package/src/ui/headless/checkout/_tests/inventoryHold.spec.ts +111 -0
  32. package/src/ui/headless/checkout/bundleCoupon.ts +14 -6
  33. package/src/ui/headless/checkout/inventoryHold.ts +83 -0
  34. package/src/ui/headless/checkout/useCheckout.ts +144 -8
  35. package/src/ui/headless/checkout/useInventoryHold.ts +104 -0
  36. package/src/ui/headless/coaching/useCoachingBooking.ts +4 -0
  37. package/src/ui/headless/course/useCourseCheckout.ts +4 -0
  38. package/src/ui/headless/donation/Donation.tsx +10 -0
  39. package/src/ui/headless/donation/DonationContext.tsx +27 -1
  40. package/src/ui/headless/event/useEventCheckout.ts +203 -2
  41. package/src/ui/headless/event/usePresaleCode.ts +181 -0
  42. package/src/ui/headless/index.ts +25 -0
  43. package/src/ui/headless/invoice/useInvoicePayment.ts +7 -2
  44. package/src/ui/headless/membership/useMembershipCheckout.ts +63 -2
  45. package/src/ui/headless/membership/useMembershipGateNotice.ts +83 -0
  46. package/src/ui/headless/offer/Offer.tsx +19 -1
  47. package/src/ui/headless/offer/OfferContext.tsx +69 -1
  48. package/src/ui/headless/paymentLink/usePaymentLinkPayment.ts +7 -1
  49. package/src/ui/headless/work/useWorkPortal.ts +3 -3
  50. package/src/ui/index.ts +64 -0
  51. package/src/ui/shell/diagnosticsGating.ts +3 -3
  52. package/src/ui/styled/AccountDashboard.tsx +143 -12
  53. package/src/ui/styled/AddToCalendar.tsx +34 -10
  54. package/src/ui/styled/AudioPlayer.tsx +1 -1
  55. package/src/ui/styled/BundleConfirmation.tsx +1 -1
  56. package/src/ui/styled/Checkout.tsx +42 -32
  57. package/src/ui/styled/CheckoutConfirmation.tsx +2 -2
  58. package/src/ui/styled/CoachingBooking.tsx +11 -2
  59. package/src/ui/styled/CoachingConfirmation.tsx +7 -3
  60. package/src/ui/styled/CohortPage.tsx +1 -1
  61. package/src/ui/styled/ContactForm.tsx +1 -1
  62. package/src/ui/styled/CourseCheckout.tsx +10 -1
  63. package/src/ui/styled/CourseConfirmation.tsx +2 -2
  64. package/src/ui/styled/CourseDetail.tsx +30 -1
  65. package/src/ui/styled/DiscountCode.tsx +1 -1
  66. package/src/ui/styled/EmailListForm.tsx +1 -1
  67. package/src/ui/styled/EventConfirmation.tsx +3 -1
  68. package/src/ui/styled/EventDetail.tsx +53 -22
  69. package/src/ui/styled/EventSeriesDetail.tsx +1 -1
  70. package/src/ui/styled/EventTickets.tsx +284 -19
  71. package/src/ui/styled/EventWaitlist.tsx +23 -3
  72. package/src/ui/styled/HoldNotice.tsx +192 -0
  73. package/src/ui/styled/InvoicePayment.tsx +14 -5
  74. package/src/ui/styled/MembershipCheckout.tsx +20 -11
  75. package/src/ui/styled/MembershipGateNotice.tsx +159 -0
  76. package/src/ui/styled/OfferButton.tsx +24 -1
  77. package/src/ui/styled/PaymentLinkPayment.tsx +12 -4
  78. package/src/ui/styled/PaystackPayButton.tsx +66 -0
  79. package/src/ui/styled/PresaleCode.tsx +174 -0
  80. package/src/ui/styled/ProductDetail.tsx +75 -5
  81. package/src/ui/styled/ProductGrid.tsx +26 -0
  82. package/src/ui/styled/ReviewForm.tsx +2 -2
  83. package/src/ui/styled/TicketTransfer.tsx +71 -42
  84. package/src/ui/styled/_tests/AddToCalendar.spec.tsx +88 -0
  85. package/src/ui/styled/_tests/EventConfirmation.spec.tsx +5 -1
  86. package/src/ui/styled/_tests/PresaleCode.spec.tsx +106 -0
  87. package/src/ui/styled/community/CommunityFeed.tsx +1 -1
  88. package/src/ui/styled/community/CommunityPostDetail.tsx +1 -1
  89. package/src/utils/_tests/paystackCheckout.spec.ts +266 -0
  90. package/src/utils/_tests/paystackCheckoutBlocked.spec.ts +51 -0
  91. package/src/utils/_tests/presaleCode.spec.ts +168 -0
  92. package/src/utils/_tests/structuredData.spec.ts +275 -0
  93. package/src/utils/membershipAccess.ts +3 -3
  94. package/src/utils/paystackCheckout.ts +277 -0
  95. package/src/utils/presaleCode.ts +96 -0
  96. package/src/utils/structuredData.ts +361 -27
  97. package/src/utils/ticketOrderOutcome.ts +1 -1
@@ -1,4 +1,4 @@
1
- // GENERATED FILE DO NOT EDIT.
1
+ // GENERATED FILE. DO NOT EDIT.
2
2
  // Produced from the platform event catalog by:
3
3
  // cd apps/backend && npm run generate:forge-events
4
4
  //
@@ -10,7 +10,7 @@
10
10
  /**
11
11
  * Every platform event an app can subscribe to, and the exact body it carries.
12
12
  *
13
- * Delivery is at-least-once dedupe on `eventId`. The same event WILL arrive
13
+ * Delivery is at-least-once, so dedupe on `eventId`. The same event WILL arrive
14
14
  * again after a retry, and a handler that acts twice will double-send.
15
15
  *
16
16
  * A body may additionally carry `partial: true`. That means the platform could
@@ -20,7 +20,7 @@
20
20
  */
21
21
  export type PlatformEventMap = {
22
22
  /**
23
- * Affiliate applied Someone applied to the affiliate program.
23
+ * Affiliate applied: Someone applied to the affiliate program.
24
24
  *
25
25
  * Requires the `affiliates.read` grant.
26
26
  */
@@ -28,7 +28,7 @@ export type PlatformEventMap = {
28
28
  affiliateId: string;
29
29
  };
30
30
  /**
31
- * Checkout abandoned Someone started a checkout and did not finish it.
31
+ * Checkout abandoned: Someone started a checkout and did not finish it.
32
32
  *
33
33
  * Requires the `products.read` grant.
34
34
  */
@@ -42,7 +42,7 @@ export type PlatformEventMap = {
42
42
  currency?: string | null;
43
43
  };
44
44
  /**
45
- * Checkout recovered An abandoned checkout was completed after all.
45
+ * Checkout recovered: An abandoned checkout was completed after all.
46
46
  *
47
47
  * Requires the `products.read` grant.
48
48
  */
@@ -56,7 +56,7 @@ export type PlatformEventMap = {
56
56
  currency: string | null;
57
57
  };
58
58
  /**
59
- * Collection entry submitted A visitor submitted an entry to a collection.
59
+ * Collection entry submitted: A visitor submitted an entry to a collection.
60
60
  *
61
61
  * Requires the `collections.read` grant.
62
62
  */
@@ -67,7 +67,7 @@ export type PlatformEventMap = {
67
67
  status?: string | null;
68
68
  };
69
69
  /**
70
- * Community post created A member posted in a community space.
70
+ * Community post created: A member posted in a community space.
71
71
  *
72
72
  * Requires the `community.new_post` grant.
73
73
  */
@@ -77,7 +77,7 @@ export type PlatformEventMap = {
77
77
  authorAccountId?: string | null;
78
78
  };
79
79
  /**
80
- * Community reply created A member replied to a community post.
80
+ * Community reply created: A member replied to a community post.
81
81
  *
82
82
  * Requires the `community.new_comment` grant.
83
83
  */
@@ -89,7 +89,7 @@ export type PlatformEventMap = {
89
89
  parentReplyId?: string | null;
90
90
  };
91
91
  /**
92
- * Contact form message Someone sent a message through a website contact form.
92
+ * Contact form message: Someone sent a message through a website contact form.
93
93
  *
94
94
  * Requires the `contacts.read` grant.
95
95
  */
@@ -99,7 +99,7 @@ export type PlatformEventMap = {
99
99
  message?: string | null;
100
100
  };
101
101
  /**
102
- * Form submitted A form was submitted. Carries the submission id read the answers through the platform client.
102
+ * Form submitted: A form was submitted. Carries the submission id. Read the answers through the platform client.
103
103
  *
104
104
  * Requires the `forms.read` grant.
105
105
  */
@@ -110,7 +110,7 @@ export type PlatformEventMap = {
110
110
  contactId: string | null;
111
111
  };
112
112
  /**
113
- * Quiz completed A contact completed a graded form, with their score and outcome.
113
+ * Quiz completed: A contact completed a graded form, with their score and outcome.
114
114
  *
115
115
  * Requires the `contacts.read` grant.
116
116
  */
@@ -124,7 +124,7 @@ export type PlatformEventMap = {
124
124
  outcomeKey?: string | null;
125
125
  };
126
126
  /**
127
- * Tag added to contact A tag was applied to a contact.
127
+ * Tag added to contact: A tag was applied to a contact.
128
128
  *
129
129
  * Requires the `contacts.read` grant.
130
130
  */
@@ -133,7 +133,7 @@ export type PlatformEventMap = {
133
133
  tagId: string;
134
134
  };
135
135
  /**
136
- * Course completed A learner finished a course.
136
+ * Course completed: A learner finished a course.
137
137
  *
138
138
  * Requires the `courses.read` grant.
139
139
  */
@@ -145,7 +145,7 @@ export type PlatformEventMap = {
145
145
  completedAt: string | null;
146
146
  };
147
147
  /**
148
- * Lesson progressed A learner completed a lesson.
148
+ * Lesson progressed: A learner completed a lesson.
149
149
  *
150
150
  * Requires the `courses.read` grant.
151
151
  */
@@ -157,7 +157,7 @@ export type PlatformEventMap = {
157
157
  cohortId?: string | null;
158
158
  };
159
159
  /**
160
- * Payment disputed A customer disputed a payment (chargeback).
160
+ * Payment disputed: A customer disputed a payment (chargeback).
161
161
  *
162
162
  * Requires the `income.read` grant.
163
163
  */
@@ -167,7 +167,7 @@ export type PlatformEventMap = {
167
167
  reason?: string | null;
168
168
  };
169
169
  /**
170
- * Document signed or decided A proposal or contract was signed, accepted or declined.
170
+ * Document signed or decided: A proposal or contract was signed, accepted or declined.
171
171
  *
172
172
  * Requires the `deals.read` grant.
173
173
  */
@@ -178,7 +178,7 @@ export type PlatformEventMap = {
178
178
  kind?: string | null;
179
179
  };
180
180
  /**
181
- * Email link clicked A recipient clicked a link in a marketing email.
181
+ * Email link clicked: A recipient clicked a link in a marketing email.
182
182
  *
183
183
  * Requires the `emails.read` grant.
184
184
  */
@@ -189,7 +189,7 @@ export type PlatformEventMap = {
189
189
  occurredAt?: string | null;
190
190
  };
191
191
  /**
192
- * Email opened A recipient opened a marketing email.
192
+ * Email opened: A recipient opened a marketing email.
193
193
  *
194
194
  * Requires the `emails.read` grant.
195
195
  */
@@ -199,7 +199,7 @@ export type PlatformEventMap = {
199
199
  occurredAt?: string | null;
200
200
  };
201
201
  /**
202
- * Event tickets bought Someone paid for tickets to an event.
202
+ * Event tickets bought: Someone paid for tickets to an event.
203
203
  *
204
204
  * Requires the `events.read` grant.
205
205
  */
@@ -221,7 +221,7 @@ export type PlatformEventMap = {
221
221
  }>;
222
222
  };
223
223
  /**
224
- * Membership tier archived A membership tier was archived and can no longer be joined.
224
+ * Membership tier archived: A membership tier was archived and can no longer be joined.
225
225
  *
226
226
  * Requires the `membership.tier_archived` grant.
227
227
  */
@@ -231,7 +231,7 @@ export type PlatformEventMap = {
231
231
  archived: boolean;
232
232
  };
233
233
  /**
234
- * Membership tier created A new membership tier was published.
234
+ * Membership tier created: A new membership tier was published.
235
235
  *
236
236
  * Requires the `membership.tier_created` grant.
237
237
  */
@@ -244,7 +244,7 @@ export type PlatformEventMap = {
244
244
  archived: boolean;
245
245
  };
246
246
  /**
247
- * Inbound message An SMS or WhatsApp message arrived from a contact.
247
+ * Inbound message: An SMS or WhatsApp message arrived from a contact.
248
248
  *
249
249
  * Requires the `contacts.read` grant.
250
250
  */
@@ -257,7 +257,7 @@ export type PlatformEventMap = {
257
257
  occurredAt?: string | null;
258
258
  };
259
259
  /**
260
- * Content reported A member reported community content for moderation.
260
+ * Content reported: A member reported community content for moderation.
261
261
  *
262
262
  * Requires the `community.content_reported` grant.
263
263
  */
@@ -267,7 +267,7 @@ export type PlatformEventMap = {
267
267
  entityType?: string | null;
268
268
  };
269
269
  /**
270
- * Order paid A store order was paid for. Carries the order, its line items and a buyer reference.
270
+ * Order paid: A store order was paid for. Carries the order, its line items and a buyer reference.
271
271
  *
272
272
  * Requires the `products.read` grant.
273
273
  */
@@ -291,7 +291,7 @@ export type PlatformEventMap = {
291
291
  }>;
292
292
  };
293
293
  /**
294
- * Payout settled A payout to the creator's bank account settled.
294
+ * Payout settled: A payout to the creator's bank account settled.
295
295
  *
296
296
  * Requires the `income.read` grant.
297
297
  */
@@ -302,7 +302,7 @@ export type PlatformEventMap = {
302
302
  currency: string;
303
303
  };
304
304
  /**
305
- * Member post published A post was published to the member feed, with the tiers that can see it.
305
+ * Member post published: A post was published to the member feed, with the tiers that can see it.
306
306
  *
307
307
  * Requires the `membership.new_post` grant.
308
308
  */
@@ -311,7 +311,7 @@ export type PlatformEventMap = {
311
311
  membershipTiers?: Array<unknown> | null;
312
312
  };
313
313
  /**
314
- * Refund failed A refund could not be completed.
314
+ * Refund failed: A refund could not be completed.
315
315
  *
316
316
  * Requires the `income.read` grant.
317
317
  */
@@ -324,7 +324,7 @@ export type PlatformEventMap = {
324
324
  sourceId?: string | null;
325
325
  };
326
326
  /**
327
- * Refund succeeded A refund completed.
327
+ * Refund succeeded: A refund completed.
328
328
  *
329
329
  * Requires the `income.read` grant.
330
330
  */
@@ -336,7 +336,7 @@ export type PlatformEventMap = {
336
336
  capturedCents?: number | null;
337
337
  };
338
338
  /**
339
- * Review submitted A customer left a review, with its rating and moderation status.
339
+ * Review submitted: A customer left a review, with its rating and moderation status.
340
340
  *
341
341
  * Requires the `reviews.read` grant.
342
342
  */
@@ -350,7 +350,7 @@ export type PlatformEventMap = {
350
350
  isEdit?: boolean | null;
351
351
  };
352
352
  /**
353
- * Review published A review passed moderation and is now public.
353
+ * Review published: A review passed moderation and is now public.
354
354
  *
355
355
  * Requires the `reviews.read` grant.
356
356
  */
@@ -358,7 +358,7 @@ export type PlatformEventMap = {
358
358
  reviewId: string;
359
359
  };
360
360
  /**
361
- * Shipment status changed A shipment for an order moved dispatched, in transit, delivered.
361
+ * Shipment status changed: A shipment for an order moved: dispatched, in transit, delivered.
362
362
  *
363
363
  * Requires the `products.read` grant.
364
364
  */
@@ -371,7 +371,7 @@ export type PlatformEventMap = {
371
371
  carrier?: string | null;
372
372
  };
373
373
  /**
374
- * Website published A website was published.
374
+ * Website published: A website was published.
375
375
  *
376
376
  * Requires the `websites.read` grant.
377
377
  */
@@ -418,5 +418,5 @@ export const PLATFORM_EVENT_PERMISSIONS: Record<PlatformEventName, string> = {
418
418
  "website.published": "websites.read",
419
419
  };
420
420
 
421
- /** Every event name, in one array handy for building a UI or validating input. */
421
+ /** Every event name, in one array, handy for building a UI or validating input. */
422
422
  export const PLATFORM_EVENT_NAMES = Object.keys(PLATFORM_EVENT_PERMISSIONS) as PlatformEventName[];
package/src/server/pwa.ts CHANGED
@@ -242,7 +242,7 @@ export function buildPwaHead(opts: BuildPwaHeadOptions): PwaHead {
242
242
  */
243
243
  export function forgeServiceWorkerScript(version: string = FORGE_PWA_VERSION): string {
244
244
  const cacheName = `forge-pwa-v${version}`;
245
- return `// Auto-generated Forge service worker (v${version}). Do not edit served by /sw.js.
245
+ return `// Auto-generated Forge service worker (v${version}). Do not edit. Served by /sw.js.
246
246
  const CACHE = ${JSON.stringify(cacheName)};
247
247
  const OFFLINE_URL = "/offline";
248
248
 
@@ -312,7 +312,7 @@ self.addEventListener("push", (event) => {
312
312
  };
313
313
  event.waitUntil(self.registration.showNotification(title, options));
314
314
  } catch (err) {
315
- // swallow a malformed push must not crash the SW
315
+ // swallow: a malformed push must not crash the SW
316
316
  }
317
317
  });
318
318
 
@@ -57,13 +57,34 @@ export type PublicTaxQuote = {
57
57
  /**
58
58
  * Superset response of the `/public/<domain>/.../start-payment` endpoint family.
59
59
  * Each domain (orders, events, coaching, courses, invoices, payment-links,
60
- * donations) returns the same core paymentSecret + paymentId + a provider
61
- * plus its own optional extras. The provider field is named inconsistently
62
- * across domains (`paymentProvider` vs `paymentProviderName`); usePaymentFlow
63
- * normalizes it to `provider`.
60
+ * donations) returns the same core (paymentId, a provider, and that provider's
61
+ * session) plus its own optional extras. The provider field is named
62
+ * inconsistently across domains (`paymentProvider` vs `paymentProviderName`);
63
+ * usePaymentFlow normalizes it to `provider`.
64
+ *
65
+ * The session field is provider-specific and the two never overlap:
66
+ * Stripe always populates `paymentSecret` and never the Paystack pair; Paystack
67
+ * always populates `accessCode` + `checkoutUrl` and never `paymentSecret`.
64
68
  */
65
69
  export type PaymentStartResponse = {
66
- paymentSecret: string;
70
+ /**
71
+ * STRIPE ONLY: the PaymentIntent client secret fed to Stripe Elements.
72
+ * Optional on the type because Paystack has no equivalent; still ALWAYS
73
+ * populated by the Stripe branch, whose meaning is frozen.
74
+ */
75
+ paymentSecret?: string;
76
+ /**
77
+ * PAYSTACK ONLY: the checkout-session access code from
78
+ * `/transaction/initialize`, handed to the inline modal. Carries the session,
79
+ * so no public key is involved.
80
+ */
81
+ accessCode?: string;
82
+ /**
83
+ * PAYSTACK ONLY: the hosted checkout URL from the SAME initialize response.
84
+ * The RUNTIME fallback used when the modal cannot open, never a version
85
+ * fallback.
86
+ */
87
+ checkoutUrl?: string;
67
88
  paymentId: string;
68
89
  paymentProvider?: PaymentProviderName;
69
90
  paymentProviderName?: PaymentProviderName;
@@ -80,6 +101,17 @@ export type PaymentStartResponse = {
80
101
  shippingCosts?: { deliveryGroupId: string; amount: number; currency: string }[];
81
102
  /** Authoritative sales-tax quote for this charge (all wired pillars). */
82
103
  taxQuote?: PublicTaxQuote;
104
+ /**
105
+ * When the reserved stock goes back on sale, ISO-8601 — the RESTARTED clock,
106
+ * measured from the card form rather than from when the cart was assembled,
107
+ * so this supersedes whatever the create call reported.
108
+ *
109
+ * `null` (or absent, on an older API) when this order holds no inventory:
110
+ * a digital/service-only cart, an already-settled free order, or a profile
111
+ * with the holds switch off. Nothing may show a countdown in that case —
112
+ * "no reservation" is not "expires now".
113
+ */
114
+ holdExpiresAt?: string | null;
83
115
  };
84
116
 
85
117
  /** Normalized result surfaced by usePaymentFlow. */
@@ -825,6 +857,11 @@ export type IPublicProduct = {
825
857
  height?: number;
826
858
  dimensionUnit?: string;
827
859
  shippingCountries?: string[] | null;
860
+ /**
861
+ * Members-only restriction, from `GET /public/products` and the detail read.
862
+ * `null`/absent when nothing gates it. See {@link PublicMembershipGate}.
863
+ */
864
+ membershipGate?: PublicMembershipGate | null;
828
865
  };
829
866
 
830
867
  export type QuestionnaireQuestion = {
@@ -885,6 +922,40 @@ export type CoachingProduct = {
885
922
  cancellation?: CancellationTermsView | null;
886
923
  };
887
924
 
925
+ // ---- Membership gates (S.4) --------------------------------------------------
926
+
927
+ /**
928
+ * `sign_in_required` — the server does not know who the caller is, so they may
929
+ * already be a member. `membership_required` — it does, and they are not.
930
+ *
931
+ * Same outcome, different next action. That is the entire reason the API
932
+ * distinguishes them instead of sending one "denied", and a renderer that
933
+ * collapses them either tells a signed-in member to sign in (a loop with no
934
+ * exit) or asks an anonymous visitor to buy a membership they may already hold.
935
+ */
936
+ export type MembershipGateReason = "membership_required" | "sign_in_required";
937
+
938
+ /**
939
+ * A members-only restriction, as announced by a PUBLIC read.
940
+ *
941
+ * Present on tickets, events, products and courses. `null` on anything ungated,
942
+ * and on everything in a workspace where the gates switch is off — so a surface
943
+ * that has never heard of this field behaves exactly as it did before.
944
+ *
945
+ * The item is ANNOUNCED, never hidden: "Gold members get first refusal on the
946
+ * vinyl" only works as an offer if non-members can see it. Which means the
947
+ * storefront owes the buyer the badge AND the way out of it — see
948
+ * `buildMembershipGateNotice` in `ui/format/membershipGate`.
949
+ */
950
+ export type PublicMembershipGate = {
951
+ /** Which tiers unlock it. ANY one is enough — tiers are alternatives, never components. */
952
+ requiredTierIds: string[];
953
+ /** True when THIS caller may buy. */
954
+ allowed: boolean;
955
+ /** `null` exactly when `allowed`. */
956
+ reason: MembershipGateReason | null;
957
+ };
958
+
888
959
  export type PublicCourseLesson = {
889
960
  id: string;
890
961
  title: string;
@@ -918,6 +989,15 @@ export type PublicCourse = {
918
989
  modules?: PublicCourseModule[];
919
990
  /** Rating aggregate — present on the detail endpoint, null until first published review. */
920
991
  reviewAggregate?: IReviewAggregate | null;
992
+ /**
993
+ * Members-only restriction — present on the course DETAIL read.
994
+ *
995
+ * A live gate locks the content even for someone holding a grant, because
996
+ * "this course is part of Gold" is a condition for access rather than a
997
+ * one-off unlock. So a lapsed member sees the outline and this gate, and the
998
+ * page owes them the way back in. See {@link PublicMembershipGate}.
999
+ */
1000
+ membershipGate?: PublicMembershipGate | null;
921
1001
  };
922
1002
 
923
1003
  // ---- Blog --------------------------------------------------------------------
@@ -1004,6 +1084,14 @@ export interface IEvent {
1004
1084
  timezone?: string;
1005
1085
  type: "physical" | "virtual" | "hybrid";
1006
1086
  ticketSaleMessage?: string;
1087
+ /**
1088
+ * Members-only restriction on the WHOLE event — no tier of it sells to a
1089
+ * non-member. Distinct from `ITicket.membershipGate`, which restricts one tier
1090
+ * alongside public ones, and the two are asserted separately at checkout: a
1091
+ * page that collapses them badges every tier on a members-only event, or none
1092
+ * on a members-only tier. Present on the detail read only.
1093
+ */
1094
+ membershipGate?: PublicMembershipGate | null;
1007
1095
  questionnaire?: {
1008
1096
  id: string;
1009
1097
  question: string;
@@ -1019,6 +1107,16 @@ export interface IEvent {
1019
1107
  country: string;
1020
1108
  zipCode?: string;
1021
1109
  };
1110
+ /**
1111
+ * The host asks for one NAME PER PASS at checkout (Events 2.1).
1112
+ *
1113
+ * Optional because every event that predates the feature comes down without
1114
+ * it, and because a renderer must treat "absent" as "do not ask" — an event
1115
+ * that has not opted in must be unaffected by the field existing at all. A
1116
+ * checkout that ignores this ships four passes in the buyer's name and the
1117
+ * artist's door list is wrong; see `ui/format/attendees.ts`.
1118
+ */
1119
+ collectAttendeeDetails?: boolean;
1022
1120
  title: string;
1023
1121
  description?: string;
1024
1122
  actionText: string;
@@ -1038,6 +1136,52 @@ export interface IEvent {
1038
1136
  * must not assume it is present.
1039
1137
  */
1040
1138
  bookingFee?: IBookingFee | null;
1139
+ /**
1140
+ * This event's `.ics` address, in both forms — the DETAIL endpoint only.
1141
+ *
1142
+ * `calendarUrl` is the https download; `calendarWebcalUrl` is the same
1143
+ * address under the scheme iOS and macOS route to Calendar. Apple publishes
1144
+ * no template URL the way Google and Microsoft do, so without these an
1145
+ * add-to-calendar control has no Apple option at all — which is exactly the
1146
+ * state every surface in this product was in until they existed.
1147
+ *
1148
+ * Not derivable client-side: the address carries a keyed HMAC. Optional
1149
+ * because `useEvents()` (the list) deliberately does not mint one, and null
1150
+ * for a cancelled show, where the feed would 404 anyway.
1151
+ */
1152
+ calendarUrl?: string | null;
1153
+ calendarWebcalUrl?: string | null;
1154
+ /**
1155
+ * Whether this event has a presale behind a code, and whether the code sent
1156
+ * with THIS request opened it (1.2).
1157
+ *
1158
+ * Present only on the DETAIL endpoint, and only there because it is the only
1159
+ * page with a box to type a code into. Absent on `useEvents()`.
1160
+ *
1161
+ * Neither field is derivable from `tickets`: a hidden tier is filtered out
1162
+ * server-side (so a presale-only event looks like an event with no tickets at
1163
+ * all), and a VISIBLE code-gated tier is already listed (so the right code
1164
+ * changes nothing on screen). A renderer that guessed would either show a code
1165
+ * box on every event or call a correct code wrong.
1166
+ */
1167
+ presale?: IEventPresaleState;
1168
+ }
1169
+
1170
+ /**
1171
+ * The two facts a storefront needs to offer a presale-code box.
1172
+ *
1173
+ * It carries nothing about the codes themselves — not how many exist, not which
1174
+ * tier a code opened. A visitor guessing learns only "not a code for this
1175
+ * event", which is what they would learn by trying to check out anyway.
1176
+ */
1177
+ export interface IEventPresaleState {
1178
+ /** At least one live tier is code-gated. FALSE means: render no box at all. */
1179
+ hasCodedTiers: boolean;
1180
+ /**
1181
+ * `null` when no code was sent — a first page load must not open with a
1182
+ * rejection. `false` is the only value that should show "not valid".
1183
+ */
1184
+ codeAccepted: boolean | null;
1041
1185
  }
1042
1186
 
1043
1187
  // ---- Event series ------------------------------------------------------------
@@ -1125,9 +1269,56 @@ export type ITicket = {
1125
1269
  quantity: number;
1126
1270
  order: number;
1127
1271
  sold: number;
1272
+ /**
1273
+ * Seats promised to somebody else's IN-FLIGHT checkout — not yet sold, but
1274
+ * not on the table either.
1275
+ *
1276
+ * The server sells on `quantity - sold - held` (`inventoryHold.ts`), so a
1277
+ * surface that shows `quantity - sold` tells a fan "3 left" about seats that
1278
+ * are in other people's carts and then refuses them at reserve. Use
1279
+ * `ticketSeatsAvailable` from `@tribe-nest/forge/ui` rather than subtracting
1280
+ * by hand — it is the one place the two stacks agree with the server.
1281
+ *
1282
+ * Optional, and absent means zero: a site pinned to an API build that
1283
+ * predates the hold engine keeps its previous behaviour instead of reading
1284
+ * every tier as sold out.
1285
+ */
1286
+ held?: number;
1128
1287
  maxPerPerson: number;
1129
1288
  expiresAt?: string;
1289
+ /**
1290
+ * Scheduled on-sale. Before this instant the server refuses the tier with
1291
+ * `ticket_not_yet_on_sale` — `evaluateTierSellability` is the authority.
1292
+ *
1293
+ * The tier stays VISIBLE on purpose: "on sale Friday 10am" is information a
1294
+ * fan wants, and hiding it makes a scheduled on-sale indistinguishable from
1295
+ * the tier not existing. So a renderer must announce the time and refuse the
1296
+ * add, exactly as it does for a members-only tier — see `ticketSaleWindow`
1297
+ * and `onSaleBadge` in `@tribe-nest/forge/ui`.
1298
+ */
1299
+ availableFrom?: string | null;
1130
1300
  archivedAt?: string;
1301
+ /**
1302
+ * This tier is not listed publicly (1.2). If you are looking at one, it is
1303
+ * because a presale code revealed it — the server filters hidden tiers out of
1304
+ * every response that did not carry the right code.
1305
+ *
1306
+ * Worth badging: a buyer who has just unlocked something should be able to
1307
+ * SEE what unlocked, otherwise a correct code looks like it did nothing.
1308
+ */
1309
+ isHidden?: boolean;
1310
+ /**
1311
+ * Members-only restriction on THIS tier — the members-only ticket alongside
1312
+ * public ones. Present on the event DETAIL read; absent on list endpoints,
1313
+ * which quote no ticket and offer no buy button.
1314
+ *
1315
+ * A gated tier stays in the list on purpose: hiding it kills the upsell that
1316
+ * is the whole reason to sell a members-only ticket. So a renderer must badge
1317
+ * it and offer the resolving action — sign in, or view the tier — rather than
1318
+ * let the buyer discover the gate by being refused at payment. See
1319
+ * {@link PublicMembershipGate}.
1320
+ */
1321
+ membershipGate?: PublicMembershipGate | null;
1131
1322
  };
1132
1323
 
1133
1324
  // ---- Invoices ----------------------------------------------------------------
@@ -1423,6 +1614,13 @@ export type CoachingBooking = {
1423
1614
  productTitle?: string | null;
1424
1615
  durationMinutes?: number | null;
1425
1616
  timezone?: string | null;
1617
+ /**
1618
+ * The session's `.ics`, both forms — see {@link CalendarFeedLinks}. Null
1619
+ * unless the booking is CONFIRMED: a held slot is not a session, and offering
1620
+ * to diarise a cancelled one is worse than offering nothing.
1621
+ */
1622
+ calendarUrl?: string | null;
1623
+ calendarWebcalUrl?: string | null;
1426
1624
  };
1427
1625
 
1428
1626
  export type CourseBooking = {
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Minimal typings for `@paystack/inline-js` v2, which ships no declarations of
3
+ * its own (no `types` field, no bundled `.d.ts`).
4
+ *
5
+ * This file is pulled into every consuming program by an explicit
6
+ * `/// <reference path>` in `utils/paystackCheckout.ts` rather than by living in
7
+ * a `typeRoots` directory. Forge ships SOURCE, so a site (or `apps/client`)
8
+ * type-checks these files inside its own program, and an ambient declaration
9
+ * that is merely present in the package is not part of that program unless
10
+ * something in the import graph reaches it.
11
+ *
12
+ * Only the surface Forge actually calls is described. Widening it is fine;
13
+ * guessing at shapes we never call is not.
14
+ */
15
+ declare module "@paystack/inline-js" {
16
+ /** What Paystack hands back on a completed transaction. */
17
+ export interface PaystackTransaction {
18
+ /** Our own `paymentId` UUID: the reference `startCharge` minted. */
19
+ reference?: string;
20
+ /** Paystack's echo of the same reference on the callback URL. */
21
+ trxref?: string;
22
+ /** Paystack's internal numeric transaction id, as a string. */
23
+ transaction?: string;
24
+ status?: string;
25
+ message?: string;
26
+ }
27
+
28
+ export interface PaystackResumeOptions {
29
+ onSuccess?: (transaction: PaystackTransaction) => void;
30
+ /** The fan closed the modal. NOT a failure. */
31
+ onCancel?: () => void;
32
+ /** Fires once the checkout iframe has actually loaded. */
33
+ onLoad?: (response: unknown) => void;
34
+ onError?: (error: unknown) => void;
35
+ }
36
+
37
+ export default class PaystackPop {
38
+ /**
39
+ * Open the modal against an existing checkout session. The access code
40
+ * carries the session, so no public key is involved.
41
+ */
42
+ resumeTransaction(accessCode: string, options?: PaystackResumeOptions): unknown;
43
+ cancelTransaction(id?: string): void;
44
+ }
45
+ }