@tribe-nest/forge 3.22.0 → 3.24.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 +2 -1
- package/src/data/queries/_tests/eventWaitlist.spec.ts +38 -0
- package/src/data/queries/useAnalytics.ts +59 -8
- package/src/data/queries/useAuthActions.ts +1 -1
- package/src/data/queries/useCheckouts.ts +5 -0
- package/src/data/queries/useEventWaitlist.ts +123 -5
- package/src/data/queries/useEvents.ts +16 -0
- package/src/data/queries/useMusicLink.ts +28 -0
- package/src/data/queries/useMyBookings.ts +25 -0
- package/src/data/queries/usePaymentFlow.ts +84 -18
- package/src/data/queries/useShipping.ts +5 -0
- package/src/data/queries/useSubscriptions.ts +50 -4
- package/src/index.ts +6 -0
- package/src/server/index.ts +51 -0
- package/src/server/platform.ts +1 -1
- package/src/server/platformEvents.generated.ts +33 -33
- package/src/server/pwa.ts +2 -2
- package/src/types/models.ts +94 -5
- package/src/types/paystack-inline.d.ts +45 -0
- package/src/ui/analytics/ForgeAnalytics.tsx +15 -0
- package/src/ui/format/_tests/attendees.spec.ts +4 -4
- package/src/ui/format/_tests/ticketAvailability.spec.ts +283 -0
- package/src/ui/format/attendees.ts +2 -2
- package/src/ui/format/ticketAvailability.ts +226 -0
- package/src/ui/headless/checkout/bundleCoupon.ts +14 -6
- package/src/ui/headless/checkout/useCheckout.ts +72 -8
- package/src/ui/headless/coaching/useCoachingBooking.ts +4 -0
- package/src/ui/headless/course/useCourseCheckout.ts +4 -0
- package/src/ui/headless/donation/Donation.tsx +10 -0
- package/src/ui/headless/donation/DonationContext.tsx +27 -1
- package/src/ui/headless/event/useEventCheckout.ts +70 -0
- package/src/ui/headless/invoice/useInvoicePayment.ts +7 -2
- package/src/ui/headless/membership/useMembershipCheckout.ts +63 -2
- package/src/ui/headless/offer/Offer.tsx +19 -1
- package/src/ui/headless/offer/OfferContext.tsx +14 -1
- package/src/ui/headless/paymentLink/usePaymentLinkPayment.ts +7 -1
- package/src/ui/headless/work/useWorkPortal.ts +3 -3
- package/src/ui/index.ts +23 -0
- package/src/ui/shell/diagnosticsGating.ts +3 -3
- package/src/ui/styled/AccountDashboard.tsx +73 -4
- package/src/ui/styled/AudioPlayer.tsx +1 -1
- package/src/ui/styled/BundleConfirmation.tsx +1 -1
- package/src/ui/styled/Checkout.tsx +25 -32
- package/src/ui/styled/CheckoutConfirmation.tsx +2 -2
- package/src/ui/styled/CoachingBooking.tsx +11 -2
- package/src/ui/styled/CoachingConfirmation.tsx +3 -3
- package/src/ui/styled/CohortPage.tsx +1 -1
- package/src/ui/styled/ContactForm.tsx +1 -1
- package/src/ui/styled/CourseCheckout.tsx +10 -1
- package/src/ui/styled/CourseConfirmation.tsx +2 -2
- package/src/ui/styled/DiscountCode.tsx +1 -1
- package/src/ui/styled/EmailListForm.tsx +1 -1
- package/src/ui/styled/EventConfirmation.tsx +1 -1
- package/src/ui/styled/EventSeriesDetail.tsx +1 -1
- package/src/ui/styled/EventTickets.tsx +135 -21
- package/src/ui/styled/EventWaitlist.tsx +23 -3
- package/src/ui/styled/HoldNotice.tsx +2 -2
- package/src/ui/styled/InvoicePayment.tsx +14 -5
- package/src/ui/styled/MembershipCheckout.tsx +20 -11
- package/src/ui/styled/MusicLinkPage.tsx +365 -0
- package/src/ui/styled/OfferButton.tsx +1 -1
- package/src/ui/styled/PaymentLinkPayment.tsx +12 -4
- package/src/ui/styled/PaystackPayButton.tsx +66 -0
- package/src/ui/styled/PresaleCode.tsx +1 -1
- package/src/ui/styled/ReviewForm.tsx +2 -2
- package/src/ui/styled/TicketTransfer.tsx +3 -3
- package/src/ui/styled/_tests/PresaleCode.spec.tsx +1 -1
- package/src/ui/styled/community/CommunityFeed.tsx +1 -1
- package/src/ui/styled/community/CommunityPostDetail.tsx +1 -1
- package/src/utils/_tests/paystackCheckout.spec.ts +266 -0
- package/src/utils/_tests/paystackCheckoutBlocked.spec.ts +51 -0
- package/src/utils/membershipAccess.ts +3 -3
- package/src/utils/paystackCheckout.ts +277 -0
- package/src/utils/ticketOrderOutcome.ts +1 -1
|
@@ -16,11 +16,38 @@ export type CreateSubscriptionInput = {
|
|
|
16
16
|
attributionRefId?: string;
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* The two things subscribing can produce.
|
|
21
|
+
*
|
|
22
|
+
* A member with no live subscription gets a provider session to pay with:
|
|
23
|
+
* `clientSecret` on Stripe, `accessCode` + `checkoutUrl` on Paystack. A member
|
|
24
|
+
* who ALREADY subscribes to this artist is changing tier: the server moves their
|
|
25
|
+
* existing subscription onto the new price with proration, so there is nothing
|
|
26
|
+
* to pay right now: `requiresPayment` is false, `changed` is true and no
|
|
27
|
+
* session is issued. Branch on `requiresPayment`, never on the presence of a
|
|
28
|
+
* secret alone. A Paystack subscription has never had one, and reading its
|
|
29
|
+
* absence as "nothing to pay" hands out the tier for free.
|
|
30
|
+
*/
|
|
31
|
+
export type CreateSubscriptionResult = {
|
|
32
|
+
membershipId: string;
|
|
33
|
+
subscriptionId: string;
|
|
34
|
+
/** STRIPE ONLY. */
|
|
35
|
+
clientSecret?: string;
|
|
36
|
+
/** PAYSTACK ONLY: inline checkout session. */
|
|
37
|
+
accessCode?: string;
|
|
38
|
+
/** PAYSTACK ONLY: hosted page, the runtime fallback. */
|
|
39
|
+
checkoutUrl?: string;
|
|
40
|
+
currentPeriodStart?: string;
|
|
41
|
+
currentPeriodEnd?: string;
|
|
42
|
+
requiresPayment?: boolean;
|
|
43
|
+
changed?: boolean;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** Subscribe to a paid tier, or move an existing subscription onto another one. */
|
|
20
47
|
export function useCreateSubscription() {
|
|
21
48
|
const { client, profileId } = useForge();
|
|
22
49
|
|
|
23
|
-
return useMutation<
|
|
50
|
+
return useMutation<CreateSubscriptionResult, unknown, CreateSubscriptionInput>({
|
|
24
51
|
mutationFn: async (body) => {
|
|
25
52
|
const res = await client.post("/public/payments/subscriptions", { ...body, profileId });
|
|
26
53
|
return res.data;
|
|
@@ -40,11 +67,30 @@ export function useCreateFreeSubscription() {
|
|
|
40
67
|
});
|
|
41
68
|
}
|
|
42
69
|
|
|
43
|
-
|
|
70
|
+
export type CancelMembershipResult = {
|
|
71
|
+
membershipId: string;
|
|
72
|
+
/** The status AFTER cancelling — still `active` while a paid period runs out. */
|
|
73
|
+
status: string;
|
|
74
|
+
/** True when the cancellation was scheduled rather than applied immediately. */
|
|
75
|
+
cancelAtPeriodEnd: boolean;
|
|
76
|
+
/** ISO date benefits actually stop. Null when they already have. */
|
|
77
|
+
accessUntil: string | null;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Cancel the member's membership.
|
|
82
|
+
*
|
|
83
|
+
* Cancelling a paid, paid-up membership does NOT take effect now: the server
|
|
84
|
+
* schedules it for the end of the period they have already paid for and the
|
|
85
|
+
* member keeps their benefits until `accessUntil`. Free tiers (and any
|
|
86
|
+
* membership with nothing paid up) end immediately. Works for every membership
|
|
87
|
+
* the member can still use — including one whose card is failing, which used to
|
|
88
|
+
* be rejected with an error the UI never showed.
|
|
89
|
+
*/
|
|
44
90
|
export function useCancelMembership() {
|
|
45
91
|
const { client, profileId } = useForge();
|
|
46
92
|
|
|
47
|
-
return useMutation<
|
|
93
|
+
return useMutation<CancelMembershipResult, unknown, { membershipId: string }>({
|
|
48
94
|
mutationFn: async ({ membershipId }) => {
|
|
49
95
|
const res = await client.post("/public/payments/subscriptions/cancel", {
|
|
50
96
|
membershipId,
|
package/src/index.ts
CHANGED
|
@@ -77,6 +77,7 @@ export * from "./data/queries/useBlog";
|
|
|
77
77
|
export * from "./data/queries/usePodcast";
|
|
78
78
|
export * from "./data/queries/useCollections";
|
|
79
79
|
export * from "./data/queries/useEvents";
|
|
80
|
+
export * from "./data/queries/useMusicLink";
|
|
80
81
|
export * from "./data/queries/useEventSeries";
|
|
81
82
|
export * from "./data/queries/useInvoice";
|
|
82
83
|
export * from "./data/queries/usePaymentLink";
|
|
@@ -127,6 +128,11 @@ export * from "./utils/landing";
|
|
|
127
128
|
export * from "./utils/metaPixel";
|
|
128
129
|
export * from "./utils/cookieConsent";
|
|
129
130
|
export * from "./utils/structuredData";
|
|
131
|
+
// Paystack inline checkout: the ONE implementation of the popup, exported so a
|
|
132
|
+
// hand-rolled payment surface (or `apps/client`) drives it through the same
|
|
133
|
+
// lazy load, the same runtime fallback and the same return-leg navigation
|
|
134
|
+
// rather than growing a second copy.
|
|
135
|
+
export * from "./utils/paystackCheckout";
|
|
130
136
|
// The per-booking checkout credential + where it is kept across the payment
|
|
131
137
|
// redirect. Exported so a host that drives the booking endpoints itself can
|
|
132
138
|
// hold the secret the same way the built-in flows do.
|
package/src/server/index.ts
CHANGED
|
@@ -34,6 +34,7 @@ export type { ApiProbeResult, ForgeSsrDiagnostics, SsrFetchFailure };
|
|
|
34
34
|
import type {
|
|
35
35
|
IEvent,
|
|
36
36
|
IEventSeries,
|
|
37
|
+
IMusicLink,
|
|
37
38
|
IPublicProduct,
|
|
38
39
|
PublicCourse,
|
|
39
40
|
CoachingProduct,
|
|
@@ -54,6 +55,9 @@ import {
|
|
|
54
55
|
type ReviewSchemaReview,
|
|
55
56
|
type SiteSeoContext,
|
|
56
57
|
} from "../utils/structuredData";
|
|
58
|
+
// Also React-free, so it belongs in this entry rather than being re-derived by
|
|
59
|
+
// each route that needs share tags.
|
|
60
|
+
import { buildHeadMeta } from "../utils/headMeta";
|
|
57
61
|
export type { SiteSeoContext };
|
|
58
62
|
import type { SiteConfig } from "../data/queries/useWebsite";
|
|
59
63
|
|
|
@@ -293,6 +297,53 @@ export function fetchEventSeriesServer(opts: {
|
|
|
293
297
|
});
|
|
294
298
|
}
|
|
295
299
|
|
|
300
|
+
/**
|
|
301
|
+
* Fetch a music smart link by slug for SSR.
|
|
302
|
+
*
|
|
303
|
+
* `null` means render the not-found state: the slug does not exist for this
|
|
304
|
+
* profile, or the link is archived. Archiving is how a creator takes a link
|
|
305
|
+
* down, so a 404 is the correct answer rather than a stale page.
|
|
306
|
+
*/
|
|
307
|
+
export function fetchMusicLinkServer(opts: {
|
|
308
|
+
apiUrl: string;
|
|
309
|
+
profileId?: string;
|
|
310
|
+
slug: string;
|
|
311
|
+
}): Promise<IMusicLink | null> {
|
|
312
|
+
return getJson<IMusicLink>(opts.apiUrl, `/public/music-links`, {
|
|
313
|
+
profileId: opts.profileId,
|
|
314
|
+
slug: opts.slug,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Head tags for a music smart link.
|
|
320
|
+
*
|
|
321
|
+
* The share card matters more here than on any other page in the product. A
|
|
322
|
+
* music link exists to be pasted into an Instagram story, a WhatsApp message or
|
|
323
|
+
* a tweet, so for most of its audience the OG image and title ARE the page: they
|
|
324
|
+
* decide whether anyone taps through at all. `og:type` is `music.song` (or
|
|
325
|
+
* `music.album`) rather than the default `website`, which is what lets a
|
|
326
|
+
* platform render it as a music card instead of a generic link.
|
|
327
|
+
*/
|
|
328
|
+
export function buildMusicLinkHead(
|
|
329
|
+
link: Pick<IMusicLink, "title" | "artistName" | "description" | "artworkUrl"> | null,
|
|
330
|
+
opts?: { canonicalUrl?: string; kind?: "song" | "album" },
|
|
331
|
+
) {
|
|
332
|
+
if (!link) {
|
|
333
|
+
return buildHeadMeta({ title: "Link not found", noindex: true });
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const title = link.artistName ? `${link.artistName} - ${link.title}` : link.title;
|
|
337
|
+
|
|
338
|
+
return buildHeadMeta({
|
|
339
|
+
title,
|
|
340
|
+
description: link.description || `Listen to ${title} on your favourite streaming service.`,
|
|
341
|
+
image: link.artworkUrl || undefined,
|
|
342
|
+
canonicalUrl: opts?.canonicalUrl,
|
|
343
|
+
ogType: opts?.kind === "album" ? "music.album" : "music.song",
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
296
347
|
/** Fetch a single product by id or slug for SSR. */
|
|
297
348
|
export function fetchProductServer(opts: {
|
|
298
349
|
apiUrl: string;
|
package/src/server/platform.ts
CHANGED
|
@@ -376,7 +376,7 @@ export async function handlePlatformEvent(
|
|
|
376
376
|
),
|
|
377
377
|
budget,
|
|
378
378
|
`Handler for "${event.event}" took longer than ${budget}ms. Move the slow part into ` +
|
|
379
|
-
`enqueueAppJob() and return
|
|
379
|
+
`enqueueAppJob() and return. The platform's delivery attempt times out before this finishes.`,
|
|
380
380
|
);
|
|
381
381
|
} catch (err) {
|
|
382
382
|
// 500 so the platform retries. Writes already made carry keys derived from
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// GENERATED FILE
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
315
|
+
// swallow: a malformed push must not crash the SW
|
|
316
316
|
}
|
|
317
317
|
});
|
|
318
318
|
|
package/src/types/models.ts
CHANGED
|
@@ -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
|
|
61
|
-
* plus its own optional extras. The provider field is named
|
|
62
|
-
* across domains (`paymentProvider` vs `paymentProviderName`);
|
|
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
|
-
|
|
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;
|
|
@@ -1248,8 +1269,34 @@ export type ITicket = {
|
|
|
1248
1269
|
quantity: number;
|
|
1249
1270
|
order: number;
|
|
1250
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;
|
|
1251
1287
|
maxPerPerson: number;
|
|
1252
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;
|
|
1253
1300
|
archivedAt?: string;
|
|
1254
1301
|
/**
|
|
1255
1302
|
* This tier is not listed publicly (1.2). If you are looking at one, it is
|
|
@@ -1828,3 +1875,45 @@ export type PaymentLinkData = {
|
|
|
1828
1875
|
profileName: string;
|
|
1829
1876
|
profileSubdomain: string;
|
|
1830
1877
|
};
|
|
1878
|
+
|
|
1879
|
+
/**
|
|
1880
|
+
* A music smart link, as served publicly at `/l/<slug>` on a creator's site.
|
|
1881
|
+
*
|
|
1882
|
+
* This is the PUBLIC shape, which is narrower than the stored row: the backend
|
|
1883
|
+
* builds it field by field, drops disabled destinations, sorts the rest by the
|
|
1884
|
+
* order the creator arranged, and resolves `metaPixelId` from the route's
|
|
1885
|
+
* tracking config (falling back to the profile pixel). No CAPI token is ever
|
|
1886
|
+
* part of it.
|
|
1887
|
+
*/
|
|
1888
|
+
export interface IMusicLinkDestination {
|
|
1889
|
+
/** A key from the platform allowlist, e.g. `spotify`, `appleMusic`. */
|
|
1890
|
+
platform: string;
|
|
1891
|
+
url: string;
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
/** Per-link presentation, layered over the site theme by `MusicLinkPage`. */
|
|
1895
|
+
export interface IMusicLinkTheme {
|
|
1896
|
+
mode?: "light" | "dark";
|
|
1897
|
+
background?: string;
|
|
1898
|
+
text?: string;
|
|
1899
|
+
accent?: string;
|
|
1900
|
+
surface?: string;
|
|
1901
|
+
border?: string;
|
|
1902
|
+
/** Blurred cover behind the page. Defaults to on. */
|
|
1903
|
+
artworkBackdrop?: boolean;
|
|
1904
|
+
buttonShape?: "rounded" | "pill" | "square";
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
export interface IMusicLink {
|
|
1908
|
+
id: string;
|
|
1909
|
+
slug: string;
|
|
1910
|
+
title: string;
|
|
1911
|
+
artistName: string | null;
|
|
1912
|
+
description: string | null;
|
|
1913
|
+
artworkUrl: string | null;
|
|
1914
|
+
releaseDate: string | null;
|
|
1915
|
+
theme: IMusicLinkTheme | null;
|
|
1916
|
+
destinations: IMusicLinkDestination[];
|
|
1917
|
+
/** Public pixel id for this route, or null when neither it nor the profile has one. */
|
|
1918
|
+
metaPixelId: string | null;
|
|
1919
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -188,6 +188,17 @@ export function ForgeAnalytics({
|
|
|
188
188
|
const el = start.closest("a,button,[data-track]") as HTMLElement | null;
|
|
189
189
|
if (!el) return;
|
|
190
190
|
|
|
191
|
+
// Opt-out for elements that report their own click.
|
|
192
|
+
//
|
|
193
|
+
// This listener cannot know that a component already sent a richer event
|
|
194
|
+
// for the same click, so without a way to stand down, anything that tracks
|
|
195
|
+
// its own clicks is counted twice. Worse than the double count: this
|
|
196
|
+
// listener's payload carries no `eventId`, so the resulting CAPI `Lead`
|
|
197
|
+
// has nothing to dedupe against, and Meta sees two conversions for one
|
|
198
|
+
// action. Any element that fires its own event must carry
|
|
199
|
+
// `data-track-skip`.
|
|
200
|
+
if (el.closest("[data-track-skip]")) return;
|
|
201
|
+
|
|
191
202
|
const anchor = el.closest("a") as HTMLAnchorElement | null;
|
|
192
203
|
const text = (el.getAttribute("aria-label") || el.textContent || "").trim().slice(0, 120) || undefined;
|
|
193
204
|
track("click", {
|
|
@@ -197,6 +208,10 @@ export function ForgeAnalytics({
|
|
|
197
208
|
text,
|
|
198
209
|
href: anchor?.href || undefined,
|
|
199
210
|
id: el.id || el.getAttribute("data-track") || undefined,
|
|
211
|
+
// Page views have carried an id since this shipped; clicks did not, so a
|
|
212
|
+
// click's server-side CAPI twin had no `event_id` and could never be
|
|
213
|
+
// deduplicated against a browser pixel event of the same name.
|
|
214
|
+
eventId: newEventId(),
|
|
200
215
|
});
|
|
201
216
|
};
|
|
202
217
|
|