@tribe-nest/forge 2.2.0 → 3.4.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.
- package/package.json +1 -1
- package/src/client/createForgeClient.ts +85 -2
- package/src/client/tokenStorage.ts +31 -0
- package/src/contexts/AppAuthContext.tsx +100 -15
- package/src/contexts/PublicAuthContext.tsx +46 -9
- package/src/data/queries/useCheckouts.ts +84 -1
- package/src/data/queries/useCoachingAvailability.ts +18 -3
- package/src/data/queries/useCourseAccess.ts +1 -1
- package/src/data/queries/useCourses.ts +42 -1
- package/src/data/queries/useEvents.ts +15 -1
- package/src/data/queries/usePaymentFlow.ts +12 -0
- package/src/data/queries/useWebsite.ts +6 -0
- package/src/index.ts +19 -2
- package/src/provider/ForgeProvider.tsx +30 -1
- package/src/server/_tests/platformEvents.spec.ts +315 -0
- package/src/server/index.ts +17 -0
- package/src/server/jobs.ts +41 -10
- package/src/server/platform.ts +234 -9
- package/src/server/platformEvents.generated.ts +422 -0
- package/src/types/models.ts +110 -3
- package/src/ui/headless/auth/useSignupForm.ts +69 -4
- package/src/ui/headless/calendar/useAddToCalendar.ts +194 -0
- package/src/ui/headless/checkout/_tests/bundleCoupon.spec.ts +169 -0
- package/src/ui/headless/checkout/bundleCoupon.ts +96 -0
- package/src/ui/headless/checkout/useCheckout.ts +156 -8
- package/src/ui/headless/coaching/useCoachingBooking.ts +53 -3
- package/src/ui/headless/coupon/_tests/couponFailureMessage.spec.ts +84 -0
- package/src/ui/headless/coupon/useCouponField.ts +164 -0
- package/src/ui/headless/course/useCourseCheckout.ts +113 -18
- package/src/ui/headless/event/useEventCheckout.ts +53 -2
- package/src/ui/headless/index.ts +15 -0
- package/src/ui/headless/work/useWorkPortal.ts +24 -21
- package/src/ui/index.ts +7 -0
- package/src/ui/shell/PoweredBy.tsx +60 -0
- package/src/ui/shell/TribeNestApp.tsx +15 -1
- package/src/ui/shell/shellGating.spec.ts +21 -1
- package/src/ui/shell/shellGating.ts +14 -0
- package/src/ui/styled/AddToCalendar.tsx +104 -0
- package/src/ui/styled/Checkout.tsx +45 -14
- package/src/ui/styled/CoachingBooking.tsx +28 -8
- package/src/ui/styled/CoachingConfirmation.tsx +12 -0
- package/src/ui/styled/CourseCheckout.tsx +49 -18
- package/src/ui/styled/DiscountCode.tsx +206 -0
- package/src/ui/styled/EventConfirmation.tsx +68 -22
- package/src/ui/styled/EventDetail.tsx +18 -5
- package/src/ui/styled/EventTickets.tsx +49 -5
- package/src/ui/styled/SignupForm.tsx +86 -35
- package/src/ui/styled/_tests/DiscountCode.spec.tsx +272 -0
- package/src/ui/styled/_tests/EventConfirmation.spec.tsx +154 -0
- package/src/ui/styled/work/WorkInviteAccept.tsx +54 -5
- package/src/utils/_tests/safeRedirect.spec.ts +117 -0
- package/src/utils/_tests/ticketOrderOutcome.spec.ts +126 -0
- package/src/utils/safeRedirect.ts +41 -0
- package/src/utils/ticketOrderOutcome.ts +125 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
// GENERATED FILE — DO NOT EDIT.
|
|
2
|
+
// Produced from the platform event catalog by:
|
|
3
|
+
// cd apps/backend && npm run generate:forge-events
|
|
4
|
+
//
|
|
5
|
+
// The catalog (apps/backend/src/services/admin/app/eventCatalog.ts) is the single
|
|
6
|
+
// definition of what an app can receive and what shape it arrives in. Editing
|
|
7
|
+
// this file by hand creates a second one, and the copy that drifts is the one
|
|
8
|
+
// nobody is testing.
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Every platform event an app can subscribe to, and the exact body it carries.
|
|
12
|
+
*
|
|
13
|
+
* Delivery is at-least-once — dedupe on `eventId`. The same event WILL arrive
|
|
14
|
+
* again after a retry, and a handler that acts twice will double-send.
|
|
15
|
+
*
|
|
16
|
+
* A body may additionally carry `partial: true`. That means the platform could
|
|
17
|
+
* not build the full payload (the record was missing, or the enriched body did
|
|
18
|
+
* not match its own schema) and sent the identifying ids instead. Re-read what
|
|
19
|
+
* you need through the platform client rather than trusting the missing fields.
|
|
20
|
+
*/
|
|
21
|
+
export type PlatformEventMap = {
|
|
22
|
+
/**
|
|
23
|
+
* Affiliate applied — Someone applied to the affiliate program.
|
|
24
|
+
*
|
|
25
|
+
* Requires the `affiliates.read` grant.
|
|
26
|
+
*/
|
|
27
|
+
"affiliate.applied": {
|
|
28
|
+
affiliateId: string;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Checkout abandoned — Someone started a checkout and did not finish it.
|
|
32
|
+
*
|
|
33
|
+
* Requires the `products.read` grant.
|
|
34
|
+
*/
|
|
35
|
+
"checkout.abandoned": {
|
|
36
|
+
checkoutId: string;
|
|
37
|
+
source: string;
|
|
38
|
+
sourceRecordId?: string | null;
|
|
39
|
+
contactId?: string | null;
|
|
40
|
+
email?: string | null;
|
|
41
|
+
totalCents?: number | null;
|
|
42
|
+
currency?: string | null;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Checkout recovered — An abandoned checkout was completed after all.
|
|
46
|
+
*
|
|
47
|
+
* Requires the `products.read` grant.
|
|
48
|
+
*/
|
|
49
|
+
"checkout.recovered": {
|
|
50
|
+
checkoutId: string;
|
|
51
|
+
source: string | null;
|
|
52
|
+
sourceRecordId: string | null;
|
|
53
|
+
contactId: string | null;
|
|
54
|
+
email: string | null;
|
|
55
|
+
totalCents: number | null;
|
|
56
|
+
currency: string | null;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Collection entry submitted — A visitor submitted an entry to a collection.
|
|
60
|
+
*
|
|
61
|
+
* Requires the `collections.read` grant.
|
|
62
|
+
*/
|
|
63
|
+
"collection.submission": {
|
|
64
|
+
entryId: string;
|
|
65
|
+
collectionId: string;
|
|
66
|
+
collectionName?: string | null;
|
|
67
|
+
status?: string | null;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Community post created — A member posted in a community space.
|
|
71
|
+
*
|
|
72
|
+
* Requires the `community.new_post` grant.
|
|
73
|
+
*/
|
|
74
|
+
"community_post.created": {
|
|
75
|
+
postId: string;
|
|
76
|
+
spaceId: string;
|
|
77
|
+
authorAccountId?: string | null;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Community reply created — A member replied to a community post.
|
|
81
|
+
*
|
|
82
|
+
* Requires the `community.new_comment` grant.
|
|
83
|
+
*/
|
|
84
|
+
"community_reply.created": {
|
|
85
|
+
replyId: string;
|
|
86
|
+
postId: string;
|
|
87
|
+
spaceId?: string | null;
|
|
88
|
+
authorAccountId?: string | null;
|
|
89
|
+
parentReplyId?: string | null;
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Contact form message — Someone sent a message through a website contact form.
|
|
93
|
+
*
|
|
94
|
+
* Requires the `contacts.read` grant.
|
|
95
|
+
*/
|
|
96
|
+
"contact_message.created": {
|
|
97
|
+
senderName?: string | null;
|
|
98
|
+
senderEmail?: string | null;
|
|
99
|
+
message?: string | null;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* Form submitted — A form was submitted. Carries the submission id — read the answers through the platform client.
|
|
103
|
+
*
|
|
104
|
+
* Requires the `forms.read` grant.
|
|
105
|
+
*/
|
|
106
|
+
"contact.form_submitted": {
|
|
107
|
+
formId: string;
|
|
108
|
+
formTitle: string | null;
|
|
109
|
+
submissionId: string | null;
|
|
110
|
+
contactId: string | null;
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* Quiz completed — A contact completed a graded form, with their score and outcome.
|
|
114
|
+
*
|
|
115
|
+
* Requires the `contacts.read` grant.
|
|
116
|
+
*/
|
|
117
|
+
"contact.quiz_completed": {
|
|
118
|
+
contactId: string | null;
|
|
119
|
+
formId: string;
|
|
120
|
+
submissionId?: string | null;
|
|
121
|
+
quizResultId?: string | null;
|
|
122
|
+
percent?: number | null;
|
|
123
|
+
passed?: boolean | null;
|
|
124
|
+
outcomeKey?: string | null;
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* Tag added to contact — A tag was applied to a contact.
|
|
128
|
+
*
|
|
129
|
+
* Requires the `contacts.read` grant.
|
|
130
|
+
*/
|
|
131
|
+
"contact.tag_added": {
|
|
132
|
+
contactId: string;
|
|
133
|
+
tagId: string;
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Course completed — A learner finished a course.
|
|
137
|
+
*
|
|
138
|
+
* Requires the `courses.read` grant.
|
|
139
|
+
*/
|
|
140
|
+
"course.completed": {
|
|
141
|
+
courseId: string;
|
|
142
|
+
courseTitle: string | null;
|
|
143
|
+
courseAccessId: string | null;
|
|
144
|
+
contactId: string | null;
|
|
145
|
+
completedAt: string | null;
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* Lesson progressed — A learner completed a lesson.
|
|
149
|
+
*
|
|
150
|
+
* Requires the `courses.read` grant.
|
|
151
|
+
*/
|
|
152
|
+
"course.lesson_progressed": {
|
|
153
|
+
courseId: string;
|
|
154
|
+
courseAccessId?: string | null;
|
|
155
|
+
lessonId?: string | null;
|
|
156
|
+
contactId?: string | null;
|
|
157
|
+
cohortId?: string | null;
|
|
158
|
+
};
|
|
159
|
+
/**
|
|
160
|
+
* Payment disputed — A customer disputed a payment (chargeback).
|
|
161
|
+
*
|
|
162
|
+
* Requires the `income.read` grant.
|
|
163
|
+
*/
|
|
164
|
+
"dispute.created": {
|
|
165
|
+
disputeId: string;
|
|
166
|
+
amountCents?: number | null;
|
|
167
|
+
reason?: string | null;
|
|
168
|
+
};
|
|
169
|
+
/**
|
|
170
|
+
* Document signed or decided — A proposal or contract was signed, accepted or declined.
|
|
171
|
+
*
|
|
172
|
+
* Requires the `deals.read` grant.
|
|
173
|
+
*/
|
|
174
|
+
"document.completed": {
|
|
175
|
+
documentId: string;
|
|
176
|
+
event: string;
|
|
177
|
+
title?: string | null;
|
|
178
|
+
kind?: string | null;
|
|
179
|
+
};
|
|
180
|
+
/**
|
|
181
|
+
* Email link clicked — A recipient clicked a link in a marketing email.
|
|
182
|
+
*
|
|
183
|
+
* Requires the `emails.read` grant.
|
|
184
|
+
*/
|
|
185
|
+
"email.clicked": {
|
|
186
|
+
emailId: string;
|
|
187
|
+
contactId?: string | null;
|
|
188
|
+
linkUrl?: string | null;
|
|
189
|
+
occurredAt?: string | null;
|
|
190
|
+
};
|
|
191
|
+
/**
|
|
192
|
+
* Email opened — A recipient opened a marketing email.
|
|
193
|
+
*
|
|
194
|
+
* Requires the `emails.read` grant.
|
|
195
|
+
*/
|
|
196
|
+
"email.opened": {
|
|
197
|
+
emailId: string;
|
|
198
|
+
contactId?: string | null;
|
|
199
|
+
occurredAt?: string | null;
|
|
200
|
+
};
|
|
201
|
+
/**
|
|
202
|
+
* Event tickets bought — Someone paid for tickets to an event.
|
|
203
|
+
*
|
|
204
|
+
* Requires the `events.read` grant.
|
|
205
|
+
*/
|
|
206
|
+
"event_ticket_order.paid": {
|
|
207
|
+
orderId: string;
|
|
208
|
+
eventId: string | null;
|
|
209
|
+
status: string | null;
|
|
210
|
+
total: number | null;
|
|
211
|
+
currency: string | null;
|
|
212
|
+
buyer: {
|
|
213
|
+
contactId: string | null;
|
|
214
|
+
email: string | null;
|
|
215
|
+
name: string | null;
|
|
216
|
+
};
|
|
217
|
+
tickets: Array<{
|
|
218
|
+
ticketId: string | null;
|
|
219
|
+
quantity: number;
|
|
220
|
+
unitPrice: number | null;
|
|
221
|
+
}>;
|
|
222
|
+
};
|
|
223
|
+
/**
|
|
224
|
+
* Membership tier archived — A membership tier was archived and can no longer be joined.
|
|
225
|
+
*
|
|
226
|
+
* Requires the `membership.tier_archived` grant.
|
|
227
|
+
*/
|
|
228
|
+
"membership_tier.archived": {
|
|
229
|
+
membershipTierId: string;
|
|
230
|
+
name: string | null;
|
|
231
|
+
archived: boolean;
|
|
232
|
+
};
|
|
233
|
+
/**
|
|
234
|
+
* Membership tier created — A new membership tier was published.
|
|
235
|
+
*
|
|
236
|
+
* Requires the `membership.tier_created` grant.
|
|
237
|
+
*/
|
|
238
|
+
"membership_tier.created": {
|
|
239
|
+
membershipTierId: string;
|
|
240
|
+
name: string | null;
|
|
241
|
+
priceMonthly: number | null;
|
|
242
|
+
priceYearly: number | null;
|
|
243
|
+
isDefault: boolean;
|
|
244
|
+
archived: boolean;
|
|
245
|
+
};
|
|
246
|
+
/**
|
|
247
|
+
* Inbound message — An SMS or WhatsApp message arrived from a contact.
|
|
248
|
+
*
|
|
249
|
+
* Requires the `contacts.read` grant.
|
|
250
|
+
*/
|
|
251
|
+
"message.received": {
|
|
252
|
+
channel: string;
|
|
253
|
+
inboundId: string;
|
|
254
|
+
contactId?: string | null;
|
|
255
|
+
fromPhone?: string | null;
|
|
256
|
+
body?: string | null;
|
|
257
|
+
occurredAt?: string | null;
|
|
258
|
+
};
|
|
259
|
+
/**
|
|
260
|
+
* Content reported — A member reported community content for moderation.
|
|
261
|
+
*
|
|
262
|
+
* Requires the `community.content_reported` grant.
|
|
263
|
+
*/
|
|
264
|
+
"moderation.report_created": {
|
|
265
|
+
reportId: string;
|
|
266
|
+
entityId?: string | null;
|
|
267
|
+
entityType?: string | null;
|
|
268
|
+
};
|
|
269
|
+
/**
|
|
270
|
+
* Order paid — A store order was paid for. Carries the order, its line items and a buyer reference.
|
|
271
|
+
*
|
|
272
|
+
* Requires the `products.read` grant.
|
|
273
|
+
*/
|
|
274
|
+
"order.paid": {
|
|
275
|
+
orderId: string;
|
|
276
|
+
status: string | null;
|
|
277
|
+
total: number | null;
|
|
278
|
+
currency: string | null;
|
|
279
|
+
placedAt: string | null;
|
|
280
|
+
buyer: {
|
|
281
|
+
contactId: string | null;
|
|
282
|
+
email: string | null;
|
|
283
|
+
name: string | null;
|
|
284
|
+
};
|
|
285
|
+
items: Array<{
|
|
286
|
+
productId: string | null;
|
|
287
|
+
variantId: string | null;
|
|
288
|
+
title: string | null;
|
|
289
|
+
quantity: number;
|
|
290
|
+
unitPrice: number | null;
|
|
291
|
+
}>;
|
|
292
|
+
};
|
|
293
|
+
/**
|
|
294
|
+
* Payout settled — A payout to the creator's bank account settled.
|
|
295
|
+
*
|
|
296
|
+
* Requires the `income.read` grant.
|
|
297
|
+
*/
|
|
298
|
+
"payout.settled": {
|
|
299
|
+
payoutId: string | null;
|
|
300
|
+
status: string;
|
|
301
|
+
amountCents: number;
|
|
302
|
+
currency: string;
|
|
303
|
+
};
|
|
304
|
+
/**
|
|
305
|
+
* Member post published — A post was published to the member feed, with the tiers that can see it.
|
|
306
|
+
*
|
|
307
|
+
* Requires the `membership.new_post` grant.
|
|
308
|
+
*/
|
|
309
|
+
"post.created": {
|
|
310
|
+
postId: string;
|
|
311
|
+
membershipTiers?: Array<unknown> | null;
|
|
312
|
+
};
|
|
313
|
+
/**
|
|
314
|
+
* Refund failed — A refund could not be completed.
|
|
315
|
+
*
|
|
316
|
+
* Requires the `income.read` grant.
|
|
317
|
+
*/
|
|
318
|
+
"refund.failed": {
|
|
319
|
+
refundId: string;
|
|
320
|
+
amountCents: number;
|
|
321
|
+
currency?: string | null;
|
|
322
|
+
reason?: string | null;
|
|
323
|
+
sourceType?: string | null;
|
|
324
|
+
sourceId?: string | null;
|
|
325
|
+
};
|
|
326
|
+
/**
|
|
327
|
+
* Refund succeeded — A refund completed.
|
|
328
|
+
*
|
|
329
|
+
* Requires the `income.read` grant.
|
|
330
|
+
*/
|
|
331
|
+
"refund.succeeded": {
|
|
332
|
+
refundId: string;
|
|
333
|
+
sourceType?: string | null;
|
|
334
|
+
sourceId?: string | null;
|
|
335
|
+
amountCents: number;
|
|
336
|
+
capturedCents?: number | null;
|
|
337
|
+
};
|
|
338
|
+
/**
|
|
339
|
+
* Review submitted — A customer left a review, with its rating and moderation status.
|
|
340
|
+
*
|
|
341
|
+
* Requires the `reviews.read` grant.
|
|
342
|
+
*/
|
|
343
|
+
"review_submitted": {
|
|
344
|
+
reviewId: string;
|
|
345
|
+
entityType: string;
|
|
346
|
+
entityId: string;
|
|
347
|
+
contactId?: string | null;
|
|
348
|
+
rating?: number | null;
|
|
349
|
+
status?: string | null;
|
|
350
|
+
isEdit?: boolean | null;
|
|
351
|
+
};
|
|
352
|
+
/**
|
|
353
|
+
* Review published — A review passed moderation and is now public.
|
|
354
|
+
*
|
|
355
|
+
* Requires the `reviews.read` grant.
|
|
356
|
+
*/
|
|
357
|
+
"review.published": {
|
|
358
|
+
reviewId: string;
|
|
359
|
+
};
|
|
360
|
+
/**
|
|
361
|
+
* Shipment status changed — A shipment for an order moved — dispatched, in transit, delivered.
|
|
362
|
+
*
|
|
363
|
+
* Requires the `products.read` grant.
|
|
364
|
+
*/
|
|
365
|
+
"shipping.status_updated": {
|
|
366
|
+
orderId: string;
|
|
367
|
+
deliveryGroupId?: string | null;
|
|
368
|
+
shippingLabelId?: string | null;
|
|
369
|
+
status?: string | null;
|
|
370
|
+
trackingNumber?: string | null;
|
|
371
|
+
carrier?: string | null;
|
|
372
|
+
};
|
|
373
|
+
/**
|
|
374
|
+
* Website published — A website was published.
|
|
375
|
+
*
|
|
376
|
+
* Requires the `websites.read` grant.
|
|
377
|
+
*/
|
|
378
|
+
"website.published": Record<string, unknown>;
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
/** The name of any event an app can receive. */
|
|
382
|
+
export type PlatformEventName = keyof PlatformEventMap;
|
|
383
|
+
|
|
384
|
+
/** The body carried by one named event. */
|
|
385
|
+
export type PlatformEventData<E extends PlatformEventName> = PlatformEventMap[E] & { partial?: boolean };
|
|
386
|
+
|
|
387
|
+
/** Which grant unlocks each event, for error messages and docs. */
|
|
388
|
+
export const PLATFORM_EVENT_PERMISSIONS: Record<PlatformEventName, string> = {
|
|
389
|
+
"affiliate.applied": "affiliates.read",
|
|
390
|
+
"checkout.abandoned": "products.read",
|
|
391
|
+
"checkout.recovered": "products.read",
|
|
392
|
+
"collection.submission": "collections.read",
|
|
393
|
+
"community_post.created": "community.new_post",
|
|
394
|
+
"community_reply.created": "community.new_comment",
|
|
395
|
+
"contact_message.created": "contacts.read",
|
|
396
|
+
"contact.form_submitted": "forms.read",
|
|
397
|
+
"contact.quiz_completed": "contacts.read",
|
|
398
|
+
"contact.tag_added": "contacts.read",
|
|
399
|
+
"course.completed": "courses.read",
|
|
400
|
+
"course.lesson_progressed": "courses.read",
|
|
401
|
+
"dispute.created": "income.read",
|
|
402
|
+
"document.completed": "deals.read",
|
|
403
|
+
"email.clicked": "emails.read",
|
|
404
|
+
"email.opened": "emails.read",
|
|
405
|
+
"event_ticket_order.paid": "events.read",
|
|
406
|
+
"membership_tier.archived": "membership.tier_archived",
|
|
407
|
+
"membership_tier.created": "membership.tier_created",
|
|
408
|
+
"message.received": "contacts.read",
|
|
409
|
+
"moderation.report_created": "community.content_reported",
|
|
410
|
+
"order.paid": "products.read",
|
|
411
|
+
"payout.settled": "income.read",
|
|
412
|
+
"post.created": "membership.new_post",
|
|
413
|
+
"refund.failed": "income.read",
|
|
414
|
+
"refund.succeeded": "income.read",
|
|
415
|
+
"review_submitted": "reviews.read",
|
|
416
|
+
"review.published": "reviews.read",
|
|
417
|
+
"shipping.status_updated": "products.read",
|
|
418
|
+
"website.published": "websites.read",
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
/** Every event name, in one array — handy for building a UI or validating input. */
|
|
422
|
+
export const PLATFORM_EVENT_NAMES = Object.keys(PLATFORM_EVENT_PERMISSIONS) as PlatformEventName[];
|
package/src/types/models.ts
CHANGED
|
@@ -88,6 +88,44 @@ export type PaymentFlowResult = PaymentStartResponse & {
|
|
|
88
88
|
provider?: PaymentProviderName;
|
|
89
89
|
};
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* One coupon that ACTUALLY applied to a checkout, exactly as the ticket,
|
|
93
|
+
* booking and course pillars report it. Entered codes and automatic (no-code)
|
|
94
|
+
* discounts are reported identically — which is the only way a buyer ever finds
|
|
95
|
+
* out an automatic discount happened, since nothing else names it.
|
|
96
|
+
*
|
|
97
|
+
* `discountKind` is the engine's kind (`simple`, `free_shipping`, `bogo`,
|
|
98
|
+
* `volume`, `membership_grant`), kept as a string so a new kind added on the
|
|
99
|
+
* server does not fail to type-check here.
|
|
100
|
+
*/
|
|
101
|
+
export type AppliedDiscountCoupon = {
|
|
102
|
+
code: string;
|
|
103
|
+
discountKind: string;
|
|
104
|
+
/** MAJOR units — this coupon's own contribution, not the running total. */
|
|
105
|
+
discountAmount: number;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* What a pillar's pre-payment call reports about pricing after discounts.
|
|
110
|
+
* `POST /public/events/:id/orders`, `POST /public/coaching/products/:id/booking/update`
|
|
111
|
+
* and `POST /public/courses/:id/booking/update` all return this same shape
|
|
112
|
+
* (S.3, `89a721af`).
|
|
113
|
+
*
|
|
114
|
+
* Every figure here is the SERVER'S, in major units. Nothing in the UI
|
|
115
|
+
* recomputes a discount: a buyer-visible number that disagrees with the charge
|
|
116
|
+
* is worse than no number at all.
|
|
117
|
+
*/
|
|
118
|
+
export type PillarDiscountQuote = {
|
|
119
|
+
/** GROSS, before any discount. */
|
|
120
|
+
subTotal: number;
|
|
121
|
+
/** NET — what the buyer is being asked to pay. */
|
|
122
|
+
totalAmount: number;
|
|
123
|
+
/** The whole discount, summed across `appliedCoupons`. */
|
|
124
|
+
discountAmount: number;
|
|
125
|
+
couponId: string | null;
|
|
126
|
+
appliedCoupons: AppliedDiscountCoupon[];
|
|
127
|
+
};
|
|
128
|
+
|
|
91
129
|
export enum ProductDeliveryType {
|
|
92
130
|
Digital = "digital",
|
|
93
131
|
Physical = "physical",
|
|
@@ -265,9 +303,40 @@ export interface PublicAuthState {
|
|
|
265
303
|
} | null;
|
|
266
304
|
}
|
|
267
305
|
|
|
306
|
+
/**
|
|
307
|
+
* What `register` returns: the platform emailed a 6-digit code and issued NO
|
|
308
|
+
* session. Collect the code and call `verifyRegistration` — there is no variant
|
|
309
|
+
* of signup that logs a fan straight in.
|
|
310
|
+
*/
|
|
311
|
+
export type PublicAuthChallenge = { status: "verify_email"; email: string };
|
|
312
|
+
|
|
268
313
|
export interface PublicAuthContextType extends PublicAuthState {
|
|
314
|
+
/**
|
|
315
|
+
* @deprecated Broken — do not use. Fan login is a two-step code flow, so
|
|
316
|
+
* `POST /public/sessions` returns `{ message, email }` and never a token; this
|
|
317
|
+
* method still expects one and will store `undefined`. Use
|
|
318
|
+
* `validateLoginCredentials` (emails the code) then `verifyLoginCode`.
|
|
319
|
+
*/
|
|
269
320
|
login: (data: LoginInput & { profileId: string }) => Promise<{ smartLinkPath: string | undefined }>;
|
|
270
|
-
|
|
321
|
+
/**
|
|
322
|
+
* Step 1 of 2. Creates the account and emails a code; issues no session.
|
|
323
|
+
* Returns the same challenge whether or not the address is already known to the
|
|
324
|
+
* platform, so the response can't be used to discover registered emails.
|
|
325
|
+
*/
|
|
326
|
+
register: (data: PublicCreateAccountInput) => Promise<PublicAuthChallenge>;
|
|
327
|
+
/**
|
|
328
|
+
* Step 2 of 2. Redeems the emailed code, signs the fan in, and applies what the
|
|
329
|
+
* signup earned (member identity, a membership from `couponCode`, consent) —
|
|
330
|
+
* which is why the coupon/consent flags are repeated here.
|
|
331
|
+
*/
|
|
332
|
+
verifyRegistration: (data: {
|
|
333
|
+
email: string;
|
|
334
|
+
code: string;
|
|
335
|
+
profileId?: string;
|
|
336
|
+
couponCode?: string;
|
|
337
|
+
acceptedTerms?: boolean;
|
|
338
|
+
acceptedPrivacy?: boolean;
|
|
339
|
+
}) => Promise<{ smartLinkPath: string | undefined }>;
|
|
271
340
|
validateLoginCredentials: (data: LoginInput & { profileId: string }) => Promise<string>;
|
|
272
341
|
verifyLoginCode: (email: string, code: string, profileId: string) => Promise<{ smartLinkPath: string | undefined }>;
|
|
273
342
|
resendLoginCode: (email: string) => Promise<void>;
|
|
@@ -385,8 +454,18 @@ export type CollectionSearchResult = PaginatedData<CollectionEntry> & {
|
|
|
385
454
|
|
|
386
455
|
// A leaf { field, op, value } or an and/or group — the structured filter tree.
|
|
387
456
|
export type CollectionFilterOp =
|
|
388
|
-
| "eq"
|
|
389
|
-
| "
|
|
457
|
+
| "eq"
|
|
458
|
+
| "ne"
|
|
459
|
+
| "gt"
|
|
460
|
+
| "gte"
|
|
461
|
+
| "lt"
|
|
462
|
+
| "lte"
|
|
463
|
+
| "in"
|
|
464
|
+
| "between"
|
|
465
|
+
| "contains"
|
|
466
|
+
| "startsWith"
|
|
467
|
+
| "arrayContains"
|
|
468
|
+
| "isNull";
|
|
390
469
|
export type CollectionFilterNode =
|
|
391
470
|
| { and: CollectionFilterNode[] }
|
|
392
471
|
| { or: CollectionFilterNode[] }
|
|
@@ -772,6 +851,23 @@ export interface IEvent {
|
|
|
772
851
|
id: string;
|
|
773
852
|
profileId: string;
|
|
774
853
|
dateTime: string;
|
|
854
|
+
/**
|
|
855
|
+
* When the event ends. Nullable — plenty of events are published with only a
|
|
856
|
+
* start. Calendar links fall back to a default duration when it is absent.
|
|
857
|
+
*/
|
|
858
|
+
endDateTime: string | null;
|
|
859
|
+
/** Doors/arrival time, when the host publishes one separately from the start. */
|
|
860
|
+
doorsOpenAt: string | null;
|
|
861
|
+
/**
|
|
862
|
+
* Event lifecycle (mirrors the backend `EVENT_STATUS` const — Forge cannot
|
|
863
|
+
* import from the API, so the union is restated here).
|
|
864
|
+
* `draft` never reaches a public surface.
|
|
865
|
+
*/
|
|
866
|
+
status: "draft" | "scheduled" | "on_sale" | "sold_out" | "cancelled" | "completed";
|
|
867
|
+
/** Set when the host cancels; distinct from `archivedAt` (a hide-from-lists flag). */
|
|
868
|
+
cancelledAt: string | null;
|
|
869
|
+
/** Host-supplied reason shown to ticket-holders on a cancelled event. */
|
|
870
|
+
cancellationReason: string | null;
|
|
775
871
|
timezone?: string;
|
|
776
872
|
type: "physical" | "virtual" | "hybrid";
|
|
777
873
|
ticketSaleMessage?: string;
|
|
@@ -1052,6 +1148,17 @@ export type ITicketOrder = {
|
|
|
1052
1148
|
taxInclusive?: boolean;
|
|
1053
1149
|
currency?: string;
|
|
1054
1150
|
status: OrderStatus;
|
|
1151
|
+
/**
|
|
1152
|
+
* Refund tracking, written by the refund service and returned by the public
|
|
1153
|
+
* finalize endpoint (`getOrderById` is `selectAll` over the order row).
|
|
1154
|
+
*
|
|
1155
|
+
* These are what tell a refund-voided `cancelled` order apart from an
|
|
1156
|
+
* abandoned one — `status` alone cannot. See `getTicketOrderOutcome`.
|
|
1157
|
+
* `refundedAmountCents` is a bigint column, so it arrives as a string.
|
|
1158
|
+
*/
|
|
1159
|
+
refundedAmountCents?: number | string | null;
|
|
1160
|
+
refundState?: "none" | "partial" | "full" | string | null;
|
|
1161
|
+
lastRefundedAt?: string | null;
|
|
1055
1162
|
customerName: string;
|
|
1056
1163
|
customerEmail: string;
|
|
1057
1164
|
createdAt: string;
|
|
@@ -13,10 +13,17 @@ export interface UseSignupFormOptions {
|
|
|
13
13
|
requireAcceptance?: boolean;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* Headless account signup. Bring your own UI (or use the styled <SignupForm>).
|
|
18
|
+
*
|
|
19
|
+
* TWO STEPS: `submit()` creates the account and the platform emails a 6-digit
|
|
20
|
+
* code — no session yet. `awaitingCode` then flips true; collect the code into
|
|
21
|
+
* `code` and call `submitCode()`, which signs the fan in and fires `onSuccess`.
|
|
22
|
+
* Render the code input whenever `awaitingCode` is true.
|
|
23
|
+
*/
|
|
17
24
|
export function useSignupForm(opts: UseSignupFormOptions = {}) {
|
|
18
25
|
const profileId = useForge().profileId ?? "";
|
|
19
|
-
const { register } = usePublicAuth();
|
|
26
|
+
const { register, verifyRegistration } = usePublicAuth();
|
|
20
27
|
|
|
21
28
|
const [firstName, setFirstName] = useState("");
|
|
22
29
|
const [lastName, setLastName] = useState("");
|
|
@@ -25,6 +32,9 @@ export function useSignupForm(opts: UseSignupFormOptions = {}) {
|
|
|
25
32
|
const [acceptedTerms, setAcceptedTerms] = useState(false);
|
|
26
33
|
const [error, setError] = useState<string | null>(null);
|
|
27
34
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
35
|
+
// Step 2: set once the code has been emailed.
|
|
36
|
+
const [awaitingCode, setAwaitingCode] = useState(false);
|
|
37
|
+
const [code, setCode] = useState("");
|
|
28
38
|
|
|
29
39
|
const requireAcceptance = opts.requireAcceptance ?? true;
|
|
30
40
|
|
|
@@ -48,7 +58,7 @@ export function useSignupForm(opts: UseSignupFormOptions = {}) {
|
|
|
48
58
|
}
|
|
49
59
|
setIsSubmitting(true);
|
|
50
60
|
try {
|
|
51
|
-
|
|
61
|
+
await register({
|
|
52
62
|
email: email.trim(),
|
|
53
63
|
password,
|
|
54
64
|
firstName: firstName.trim(),
|
|
@@ -59,7 +69,8 @@ export function useSignupForm(opts: UseSignupFormOptions = {}) {
|
|
|
59
69
|
acceptedTerms: true,
|
|
60
70
|
acceptedPrivacy: true,
|
|
61
71
|
});
|
|
62
|
-
|
|
72
|
+
// No session yet — the emailed code is the next step.
|
|
73
|
+
setAwaitingCode(true);
|
|
63
74
|
} catch (e) {
|
|
64
75
|
setError(msg(e) || "Could not create your account.");
|
|
65
76
|
} finally {
|
|
@@ -67,6 +78,55 @@ export function useSignupForm(opts: UseSignupFormOptions = {}) {
|
|
|
67
78
|
}
|
|
68
79
|
};
|
|
69
80
|
|
|
81
|
+
/** Step 2: redeem the emailed code. Signs the fan in and fires `onSuccess`. */
|
|
82
|
+
const submitCode = async () => {
|
|
83
|
+
setError(null);
|
|
84
|
+
if (!/^\d{6}$/.test(code.trim())) {
|
|
85
|
+
setError("Enter the 6-digit code we emailed you.");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
setIsSubmitting(true);
|
|
89
|
+
try {
|
|
90
|
+
const { smartLinkPath } = await verifyRegistration({
|
|
91
|
+
email: email.trim(),
|
|
92
|
+
code: code.trim(),
|
|
93
|
+
profileId,
|
|
94
|
+
// Repeated here because this is the request that actually applies them.
|
|
95
|
+
couponCode: opts.couponCode,
|
|
96
|
+
acceptedTerms: true,
|
|
97
|
+
acceptedPrivacy: true,
|
|
98
|
+
});
|
|
99
|
+
opts.onSuccess?.(smartLinkPath);
|
|
100
|
+
} catch (e) {
|
|
101
|
+
setError(msg(e) || "That code did not work.");
|
|
102
|
+
} finally {
|
|
103
|
+
setIsSubmitting(false);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/** Re-send a code (throttled to one per 60s server-side). */
|
|
108
|
+
const resendCode = async () => {
|
|
109
|
+
setError(null);
|
|
110
|
+
setIsSubmitting(true);
|
|
111
|
+
try {
|
|
112
|
+
await register({
|
|
113
|
+
email: email.trim(),
|
|
114
|
+
password,
|
|
115
|
+
firstName: firstName.trim(),
|
|
116
|
+
lastName: lastName.trim(),
|
|
117
|
+
profileId,
|
|
118
|
+
membershipTierId: opts.membershipTierId,
|
|
119
|
+
couponCode: opts.couponCode,
|
|
120
|
+
acceptedTerms: true,
|
|
121
|
+
acceptedPrivacy: true,
|
|
122
|
+
});
|
|
123
|
+
} catch (e) {
|
|
124
|
+
setError(msg(e) || "Could not resend the code.");
|
|
125
|
+
} finally {
|
|
126
|
+
setIsSubmitting(false);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
70
130
|
return {
|
|
71
131
|
firstName,
|
|
72
132
|
setFirstName,
|
|
@@ -81,5 +141,10 @@ export function useSignupForm(opts: UseSignupFormOptions = {}) {
|
|
|
81
141
|
error,
|
|
82
142
|
isSubmitting,
|
|
83
143
|
submit,
|
|
144
|
+
awaitingCode,
|
|
145
|
+
code,
|
|
146
|
+
setCode,
|
|
147
|
+
submitCode,
|
|
148
|
+
resendCode,
|
|
84
149
|
};
|
|
85
150
|
}
|