@tribe-nest/forge 3.53.0 → 3.57.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/contexts/AppAuthContext.tsx +3 -2
- package/src/contexts/AudioPlayerContext.tsx +3 -2
- package/src/contexts/CartContext.tsx +3 -2
- package/src/contexts/PublicAuthContext.tsx +3 -2
- package/src/data/queries/useCoachingProducts.ts +15 -1
- package/src/data/queries/useCourses.ts +15 -1
- package/src/data/queries/useEvents.ts +26 -1
- package/src/data/queries/useMembership.ts +176 -4
- package/src/data/queries/useMyTickets.ts +49 -0
- package/src/data/queries/usePaymentFlow.ts +8 -0
- package/src/data/queries/useProducts.ts +15 -1
- package/src/data/queries/useSubscriptions.ts +21 -1
- package/src/i18n/de.json +116 -1
- package/src/i18n/en.json +116 -1
- package/src/i18n/index.ts +3 -2
- package/src/index.ts +22 -0
- package/src/provider/ForgeAppProvider.tsx +8 -1
- package/src/provider/ForgeProvider.tsx +3 -2
- package/src/provider/SiteConfigProvider.tsx +3 -2
- package/src/runtime/RemotePage.tsx +110 -0
- package/src/runtime/hostRuntime.ts +84 -0
- package/src/runtime/pages.spec.ts +41 -0
- package/src/runtime/pages.ts +183 -0
- package/src/runtime/pagesClient.ts +127 -0
- package/src/runtime/registry.spec.ts +45 -0
- package/src/runtime/registry.ts +102 -0
- package/src/server/index.ts +74 -0
- package/src/server/jobs.ts +4 -1
- package/src/server/platformEvents.generated.ts +152 -0
- package/src/types/models.ts +185 -0
- package/src/ui/headless/event/_tests/ticketApproval.spec.ts +137 -0
- package/src/ui/headless/event/_tests/useEventCheckoutApproval.spec.tsx +250 -0
- package/src/ui/headless/event/ticketApproval.ts +109 -0
- package/src/ui/headless/event/useEventCheckout.ts +74 -4
- package/src/ui/headless/index.ts +34 -0
- package/src/ui/headless/membership/_tests/membershipApplication.spec.ts +85 -0
- package/src/ui/headless/membership/_tests/membershipCheckoutRefetch.spec.tsx +2 -0
- package/src/ui/headless/membership/_tests/membershipTrial.spec.ts +61 -0
- package/src/ui/headless/membership/_tests/useMembershipCheckoutApplication.spec.tsx +253 -0
- package/src/ui/headless/membership/_tests/useMembershipCheckoutQuestionnaire.spec.tsx +241 -0
- package/src/ui/headless/membership/_tests/useMembershipCheckoutTrial.spec.tsx +165 -0
- package/src/ui/headless/membership/membershipApplication.ts +109 -0
- package/src/ui/headless/membership/membershipQuestionnaire.ts +44 -0
- package/src/ui/headless/membership/membershipTrial.ts +53 -0
- package/src/ui/headless/membership/useMembershipCheckout.ts +227 -8
- package/src/ui/index.ts +27 -0
- package/src/ui/payment/ForgePaymentProvider.tsx +29 -0
- package/src/ui/payment/ForgeStripePayment.tsx +110 -5
- package/src/ui/payment/_tests/ForgeStripePaymentTrial.spec.tsx +101 -0
- package/src/ui/payment/_tests/stripeConfirmOutcome.spec.ts +61 -0
- package/src/ui/payment/_tests/stripeIntentKind.spec.ts +43 -0
- package/src/ui/payment/stripeConfirmOutcome.ts +50 -0
- package/src/ui/payment/stripeIntentKind.ts +32 -0
- package/src/ui/shell/TribeNestApp.tsx +6 -0
- package/src/ui/styled/AccountDashboard.tsx +239 -15
- package/src/ui/styled/DocumentSigningPage.tsx +516 -0
- package/src/ui/styled/EventConfirmation.tsx +106 -9
- package/src/ui/styled/EventTickets.tsx +107 -13
- package/src/ui/styled/LoginForm.tsx +5 -3
- package/src/ui/styled/MembershipCheckout.tsx +596 -256
- package/src/ui/styled/MembershipTierCallout.tsx +17 -3
- package/src/ui/styled/MembershipTiers.tsx +148 -6
- package/src/ui/styled/ProductGrid.tsx +14 -2
- package/src/ui/styled/ReviewRequestPage.tsx +214 -0
- package/src/ui/styled/SignupForm.tsx +4 -2
- package/src/ui/styled/_tests/AccountDashboardTrial.spec.tsx +105 -0
- package/src/ui/styled/_tests/MembershipCheckout.spec.tsx +106 -0
- package/src/ui/styled/_tests/membershipTiersCuratedAccess.spec.tsx +139 -0
- package/src/ui/styled/_tests/membershipTiersTrial.spec.tsx +97 -0
- package/src/ui/styled/forge-utilities.css +310 -0
- package/src/ui/theme/ForgeThemeProvider.tsx +3 -2
- package/src/utils/_tests/ticketOrderOutcome.spec.ts +82 -0
- package/src/utils/ticketOrderOutcome.ts +57 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { createContext, type Context } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The runtime a built creator site shares with the central pages bundle.
|
|
5
|
+
*
|
|
6
|
+
* A site and the hosted `/i/*` pages are two separately built bundles that have
|
|
7
|
+
* to behave as ONE app. If each carried its own React, its own router and its
|
|
8
|
+
* own copies of Forge's contexts, a fan would hold two carts, be logged in on
|
|
9
|
+
* one half of the page and anonymous on the other, and hooks would throw the
|
|
10
|
+
* moment a bundled block tried to read a provider the host had mounted.
|
|
11
|
+
*
|
|
12
|
+
* So the host publishes its runtime on a global object and the bundle reads it
|
|
13
|
+
* rather than carrying its own. First writer wins, always: the site boots
|
|
14
|
+
* first, registers, and the bundle then finds what is already there.
|
|
15
|
+
*
|
|
16
|
+
* That rule is what makes a CONTEXT shareable, which is the part worth
|
|
17
|
+
* understanding. `createContext` called twice produces two unrelated objects,
|
|
18
|
+
* and a provider mounted from one is invisible to a consumer reading the other.
|
|
19
|
+
* `createSharedContext` gives both bundles the same object instead, so the
|
|
20
|
+
* host's `<CartProvider>` is the cart the bundle's `useCart()` reads.
|
|
21
|
+
*
|
|
22
|
+
* ## The contract
|
|
23
|
+
*
|
|
24
|
+
* What lives here is a contract between a site built at one moment and a bundle
|
|
25
|
+
* published later, so it is ADDITIVE FOR EVER. A key, once published under a
|
|
26
|
+
* contract, never changes meaning and is never removed. Adding one is safe:
|
|
27
|
+
* an older bundle simply does not ask for it. Removing or repurposing one is a
|
|
28
|
+
* contract bump (`rc1` to `rc2`), which leaves old sites on the old bundle
|
|
29
|
+
* until their next build.
|
|
30
|
+
*
|
|
31
|
+
* Nothing INSIDE the bundle is covered by this. Forge's UI blocks ship in the
|
|
32
|
+
* bundle precisely so a block fix needs no contract change and no rebuild.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Names the set of things the registry exposes. It rides in the bundle URL
|
|
37
|
+
* (`forge-pages/<contract>/latest`), so a site always asks for the bundle that
|
|
38
|
+
* matches the runtime it was built with.
|
|
39
|
+
*/
|
|
40
|
+
export const PAGES_RUNTIME_CONTRACT = "rc1";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A single global, not a module-level variable. A module-level one would give
|
|
44
|
+
* each bundle its own copy, which is the exact failure this exists to prevent.
|
|
45
|
+
* The name is deliberately awkward to discourage anyone reading it directly.
|
|
46
|
+
*/
|
|
47
|
+
const GLOBAL_KEY = "__tribenestPagesRuntime__";
|
|
48
|
+
|
|
49
|
+
type Registry = Record<string, unknown>;
|
|
50
|
+
|
|
51
|
+
function registry(): Registry {
|
|
52
|
+
const scope = globalThis as unknown as Record<string, Registry | undefined>;
|
|
53
|
+
const existing = scope[GLOBAL_KEY];
|
|
54
|
+
if (existing) return existing;
|
|
55
|
+
const created: Registry = {};
|
|
56
|
+
scope[GLOBAL_KEY] = created;
|
|
57
|
+
return created;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Publish a value under `key`, or return the one already published.
|
|
62
|
+
*
|
|
63
|
+
* `create` runs only when nobody has claimed the key yet, so the host's React
|
|
64
|
+
* and the host's QueryClient are the ones everybody ends up with, whichever
|
|
65
|
+
* bundle happens to call first. Registration is therefore safe to run more than
|
|
66
|
+
* once and in any order.
|
|
67
|
+
*/
|
|
68
|
+
export function shareRuntimeValue<T>(key: string, create: () => T): T {
|
|
69
|
+
const reg = registry();
|
|
70
|
+
if (!(key in reg)) reg[key] = create();
|
|
71
|
+
return reg[key] as T;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Read a published value, or `undefined` when the host never published it. */
|
|
75
|
+
export function readRuntimeValue<T>(key: string): T | undefined {
|
|
76
|
+
return registry()[key] as T | undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A React context that is the SAME object in every bundle that asks for it.
|
|
81
|
+
*
|
|
82
|
+
* Use this instead of `createContext` for anything a hosted page consumes.
|
|
83
|
+
* `defaultValue` is only used by the bundle that gets there first; the others
|
|
84
|
+
* receive that same context and its default, which is correct, since a context
|
|
85
|
+
* with two different defaults across bundles would be two contexts again.
|
|
86
|
+
*/
|
|
87
|
+
export function createSharedContext<T>(key: string, defaultValue: T): Context<T> {
|
|
88
|
+
return shareRuntimeValue(`context:${key}`, () => createContext<T>(defaultValue));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Which of `required` the host never published.
|
|
93
|
+
*
|
|
94
|
+
* The bundle calls this BEFORE it renders anything. A missing key means the
|
|
95
|
+
* site was built against a runtime older than this bundle expects, and the
|
|
96
|
+
* honest response is the reload notice: rendering anyway produces a blank page
|
|
97
|
+
* or a thrown hook, and neither tells anyone what went wrong.
|
|
98
|
+
*/
|
|
99
|
+
export function missingRuntimeKeys(required: readonly string[]): string[] {
|
|
100
|
+
const reg = registry();
|
|
101
|
+
return required.filter((key) => !(key in reg));
|
|
102
|
+
}
|
package/src/server/index.ts
CHANGED
|
@@ -46,6 +46,7 @@ import type {
|
|
|
46
46
|
CollectionQueryParams,
|
|
47
47
|
CollectionQuery,
|
|
48
48
|
CollectionSearchResult,
|
|
49
|
+
PaginatedData,
|
|
49
50
|
} from "../types/models";
|
|
50
51
|
import { collectionParamsToQuery, splitCollectionQuery } from "../data/collectionParams";
|
|
51
52
|
// SEO context for structured data. Pure functions over plain data, so this is
|
|
@@ -274,6 +275,18 @@ export async function fetchSiteBootstrap(opts: {
|
|
|
274
275
|
}
|
|
275
276
|
|
|
276
277
|
/** Fetch a single event by id or slug for SSR. */
|
|
278
|
+
/**
|
|
279
|
+
* Every published event, for SSR of the events list.
|
|
280
|
+
*
|
|
281
|
+
* The list page had no server fetch at all: it rendered a spinner into the
|
|
282
|
+
* document and filled itself in from the browser, so a crawler saw an empty
|
|
283
|
+
* page and a fan saw a flash of nothing. It is the same endpoint the client
|
|
284
|
+
* hook reads, so the markup and the first client render agree.
|
|
285
|
+
*/
|
|
286
|
+
export async function fetchEventsServer(opts: { apiUrl: string; profileId?: string }): Promise<IEvent[]> {
|
|
287
|
+
return (await getJson<IEvent[]>(opts.apiUrl, `/public/events`, { profileId: opts.profileId })) ?? [];
|
|
288
|
+
}
|
|
289
|
+
|
|
277
290
|
export function fetchEventServer(opts: {
|
|
278
291
|
apiUrl: string;
|
|
279
292
|
profileId?: string;
|
|
@@ -351,6 +364,67 @@ export function buildMusicLinkHead(
|
|
|
351
364
|
}
|
|
352
365
|
|
|
353
366
|
/** Fetch a single product by id or slug for SSR. */
|
|
367
|
+
/**
|
|
368
|
+
* The catalogue pages, server-rendered.
|
|
369
|
+
*
|
|
370
|
+
* Each of these list pages used to render a spinner into the document and
|
|
371
|
+
* fill itself in from the browser, so a crawler indexed an empty page and a
|
|
372
|
+
* fan saw nothing until the fetch landed. They are the pages search engines
|
|
373
|
+
* actually index, which makes an empty server render the expensive kind.
|
|
374
|
+
*
|
|
375
|
+
* Each mirrors its client hook exactly, same endpoint and same page size, so
|
|
376
|
+
* the markup and the first client render agree and nothing is fetched twice.
|
|
377
|
+
* An empty page is a real answer here (a creator who sells no courses), so a
|
|
378
|
+
* failed fetch resolves to an empty page rather than throwing.
|
|
379
|
+
*/
|
|
380
|
+
const emptyPage = <T,>(): PaginatedData<T> => ({
|
|
381
|
+
data: [],
|
|
382
|
+
total: 0,
|
|
383
|
+
hasNextPage: false,
|
|
384
|
+
page: 1,
|
|
385
|
+
nextPage: null,
|
|
386
|
+
pageSize: 10,
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
export async function fetchProductsServer(opts: {
|
|
390
|
+
apiUrl: string;
|
|
391
|
+
profileId?: string;
|
|
392
|
+
}): Promise<PaginatedData<IPublicProduct>> {
|
|
393
|
+
return (
|
|
394
|
+
(await getJson<PaginatedData<IPublicProduct>>(opts.apiUrl, `/public/products`, {
|
|
395
|
+
profileId: opts.profileId,
|
|
396
|
+
page: "1",
|
|
397
|
+
limit: "10",
|
|
398
|
+
})) ?? emptyPage<IPublicProduct>()
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export async function fetchCoursesServer(opts: {
|
|
403
|
+
apiUrl: string;
|
|
404
|
+
profileId?: string;
|
|
405
|
+
}): Promise<PaginatedData<PublicCourse>> {
|
|
406
|
+
return (
|
|
407
|
+
(await getJson<PaginatedData<PublicCourse>>(opts.apiUrl, `/public/courses`, {
|
|
408
|
+
profileId: opts.profileId,
|
|
409
|
+
page: "1",
|
|
410
|
+
limit: "10",
|
|
411
|
+
})) ?? emptyPage<PublicCourse>()
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export async function fetchCoachingProductsServer(opts: {
|
|
416
|
+
apiUrl: string;
|
|
417
|
+
profileId?: string;
|
|
418
|
+
}): Promise<PaginatedData<CoachingProduct>> {
|
|
419
|
+
return (
|
|
420
|
+
(await getJson<PaginatedData<CoachingProduct>>(opts.apiUrl, `/public/coaching/products`, {
|
|
421
|
+
profileId: opts.profileId,
|
|
422
|
+
page: "1",
|
|
423
|
+
limit: "10",
|
|
424
|
+
})) ?? emptyPage<CoachingProduct>()
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
354
428
|
export function fetchProductServer(opts: {
|
|
355
429
|
apiUrl: string;
|
|
356
430
|
profileId?: string;
|
package/src/server/jobs.ts
CHANGED
|
@@ -246,7 +246,10 @@ export async function handleAppJobRun(
|
|
|
246
246
|
headers: { "content-type": "application/json" },
|
|
247
247
|
body: JSON.stringify({ token }),
|
|
248
248
|
});
|
|
249
|
-
|
|
249
|
+
// Typed rather than left to inference: under Cloudflare's types `json()`
|
|
250
|
+
// resolves to `{}`, so the property read does not compile in a Worker.
|
|
251
|
+
const verified = (await res.json()) as { valid?: boolean } | null;
|
|
252
|
+
valid = verified?.valid === true;
|
|
250
253
|
} catch {
|
|
251
254
|
valid = false;
|
|
252
255
|
}
|
|
@@ -220,6 +220,151 @@ export type PlatformEventMap = {
|
|
|
220
220
|
unitPrice: number | null;
|
|
221
221
|
}>;
|
|
222
222
|
};
|
|
223
|
+
/**
|
|
224
|
+
* Event ticket request approved: The organiser approved a ticket request; the sale settled and the tickets were issued.
|
|
225
|
+
*
|
|
226
|
+
* Requires the `events.read` grant.
|
|
227
|
+
*/
|
|
228
|
+
"event_ticket_request.approved": {
|
|
229
|
+
orderId: string;
|
|
230
|
+
eventId: string | null;
|
|
231
|
+
status: string | null;
|
|
232
|
+
total: number | null;
|
|
233
|
+
currency: string | null;
|
|
234
|
+
buyer: {
|
|
235
|
+
contactId: string | null;
|
|
236
|
+
email: string | null;
|
|
237
|
+
name: string | null;
|
|
238
|
+
};
|
|
239
|
+
tickets: Array<{
|
|
240
|
+
ticketId: string | null;
|
|
241
|
+
quantity: number;
|
|
242
|
+
unitPrice: number | null;
|
|
243
|
+
}>;
|
|
244
|
+
};
|
|
245
|
+
/**
|
|
246
|
+
* Event ticket request declined: The organiser declined a ticket request; the hold was released.
|
|
247
|
+
*
|
|
248
|
+
* Requires the `events.read` grant.
|
|
249
|
+
*/
|
|
250
|
+
"event_ticket_request.declined": {
|
|
251
|
+
orderId: string;
|
|
252
|
+
eventId: string | null;
|
|
253
|
+
status: string | null;
|
|
254
|
+
total: number | null;
|
|
255
|
+
currency: string | null;
|
|
256
|
+
buyer: {
|
|
257
|
+
contactId: string | null;
|
|
258
|
+
email: string | null;
|
|
259
|
+
name: string | null;
|
|
260
|
+
};
|
|
261
|
+
tickets: Array<{
|
|
262
|
+
ticketId: string | null;
|
|
263
|
+
quantity: number;
|
|
264
|
+
unitPrice: number | null;
|
|
265
|
+
}>;
|
|
266
|
+
};
|
|
267
|
+
/**
|
|
268
|
+
* Event ticket request expired: A ticket request lapsed undecided; the hold and the seats were released.
|
|
269
|
+
*
|
|
270
|
+
* Requires the `events.read` grant.
|
|
271
|
+
*/
|
|
272
|
+
"event_ticket_request.expired": {
|
|
273
|
+
orderId: string;
|
|
274
|
+
eventId: string | null;
|
|
275
|
+
status: string | null;
|
|
276
|
+
total: number | null;
|
|
277
|
+
currency: string | null;
|
|
278
|
+
buyer: {
|
|
279
|
+
contactId: string | null;
|
|
280
|
+
email: string | null;
|
|
281
|
+
name: string | null;
|
|
282
|
+
};
|
|
283
|
+
tickets: Array<{
|
|
284
|
+
ticketId: string | null;
|
|
285
|
+
quantity: number;
|
|
286
|
+
unitPrice: number | null;
|
|
287
|
+
}>;
|
|
288
|
+
};
|
|
289
|
+
/**
|
|
290
|
+
* Event ticket requested: Someone requested tickets to an event that needs the organiser's approval. Their card is held, not charged.
|
|
291
|
+
*
|
|
292
|
+
* Requires the `events.read` grant.
|
|
293
|
+
*/
|
|
294
|
+
"event_ticket_request.submitted": {
|
|
295
|
+
orderId: string;
|
|
296
|
+
eventId: string | null;
|
|
297
|
+
status: string | null;
|
|
298
|
+
total: number | null;
|
|
299
|
+
currency: string | null;
|
|
300
|
+
buyer: {
|
|
301
|
+
contactId: string | null;
|
|
302
|
+
email: string | null;
|
|
303
|
+
name: string | null;
|
|
304
|
+
};
|
|
305
|
+
tickets: Array<{
|
|
306
|
+
ticketId: string | null;
|
|
307
|
+
quantity: number;
|
|
308
|
+
unitPrice: number | null;
|
|
309
|
+
}>;
|
|
310
|
+
};
|
|
311
|
+
/**
|
|
312
|
+
* Membership application approved: An application was approved. `status` is `joined`, or `payment_failed` when the saved card was declined.
|
|
313
|
+
*
|
|
314
|
+
* Requires the `membership.application_approved` grant.
|
|
315
|
+
*/
|
|
316
|
+
"membership_application.approved": {
|
|
317
|
+
applicationId: string;
|
|
318
|
+
membershipTierId: string | null;
|
|
319
|
+
status: string | null;
|
|
320
|
+
billingCycle: string | null;
|
|
321
|
+
amount: number | null;
|
|
322
|
+
currency: string | null;
|
|
323
|
+
membershipId: string | null;
|
|
324
|
+
applicant: {
|
|
325
|
+
contactId: string | null;
|
|
326
|
+
email: string | null;
|
|
327
|
+
name: string | null;
|
|
328
|
+
};
|
|
329
|
+
};
|
|
330
|
+
/**
|
|
331
|
+
* Membership application declined: An application to join a membership tier was declined.
|
|
332
|
+
*
|
|
333
|
+
* Requires the `membership.application_declined` grant.
|
|
334
|
+
*/
|
|
335
|
+
"membership_application.declined": {
|
|
336
|
+
applicationId: string;
|
|
337
|
+
membershipTierId: string | null;
|
|
338
|
+
status: string | null;
|
|
339
|
+
billingCycle: string | null;
|
|
340
|
+
amount: number | null;
|
|
341
|
+
currency: string | null;
|
|
342
|
+
membershipId: string | null;
|
|
343
|
+
applicant: {
|
|
344
|
+
contactId: string | null;
|
|
345
|
+
email: string | null;
|
|
346
|
+
name: string | null;
|
|
347
|
+
};
|
|
348
|
+
};
|
|
349
|
+
/**
|
|
350
|
+
* Membership application submitted: Someone applied to join a membership tier that needs approval. Nothing is charged until approved.
|
|
351
|
+
*
|
|
352
|
+
* Requires the `membership.application_submitted` grant.
|
|
353
|
+
*/
|
|
354
|
+
"membership_application.submitted": {
|
|
355
|
+
applicationId: string;
|
|
356
|
+
membershipTierId: string | null;
|
|
357
|
+
status: string | null;
|
|
358
|
+
billingCycle: string | null;
|
|
359
|
+
amount: number | null;
|
|
360
|
+
currency: string | null;
|
|
361
|
+
membershipId: string | null;
|
|
362
|
+
applicant: {
|
|
363
|
+
contactId: string | null;
|
|
364
|
+
email: string | null;
|
|
365
|
+
name: string | null;
|
|
366
|
+
};
|
|
367
|
+
};
|
|
223
368
|
/**
|
|
224
369
|
* Membership tier archived: A membership tier was archived and can no longer be joined.
|
|
225
370
|
*
|
|
@@ -403,6 +548,13 @@ export const PLATFORM_EVENT_PERMISSIONS: Record<PlatformEventName, string> = {
|
|
|
403
548
|
"email.clicked": "emails.read",
|
|
404
549
|
"email.opened": "emails.read",
|
|
405
550
|
"event_ticket_order.paid": "events.read",
|
|
551
|
+
"event_ticket_request.approved": "events.read",
|
|
552
|
+
"event_ticket_request.declined": "events.read",
|
|
553
|
+
"event_ticket_request.expired": "events.read",
|
|
554
|
+
"event_ticket_request.submitted": "events.read",
|
|
555
|
+
"membership_application.approved": "membership.application_approved",
|
|
556
|
+
"membership_application.declined": "membership.application_declined",
|
|
557
|
+
"membership_application.submitted": "membership.application_submitted",
|
|
406
558
|
"membership_tier.archived": "membership.tier_archived",
|
|
407
559
|
"membership_tier.created": "membership.tier_created",
|
|
408
560
|
"message.received": "contacts.read",
|
package/src/types/models.ts
CHANGED
|
@@ -66,6 +66,12 @@ export type PublicTaxQuote = {
|
|
|
66
66
|
* Stripe always populates `paymentSecret` and never the Paystack pair; Paystack
|
|
67
67
|
* always populates `accessCode` + `checkoutUrl` and never `paymentSecret`.
|
|
68
68
|
*/
|
|
69
|
+
/** Whether a started charge is taken now or only once the organiser approves. */
|
|
70
|
+
export type PaymentSettlement = "immediate" | "deferred";
|
|
71
|
+
|
|
72
|
+
/** How a provider holds a deferred charge until the decision. */
|
|
73
|
+
export type DeferredChargeStrategy = "authorization" | "saved_instrument";
|
|
74
|
+
|
|
69
75
|
export type PaymentStartResponse = {
|
|
70
76
|
/**
|
|
71
77
|
* STRIPE ONLY: the PaymentIntent client secret fed to Stripe Elements.
|
|
@@ -101,6 +107,19 @@ export type PaymentStartResponse = {
|
|
|
101
107
|
shippingCosts?: { deliveryGroupId: string; amount: number; currency: string }[];
|
|
102
108
|
/** Authoritative sales-tax quote for this charge (all wired pillars). */
|
|
103
109
|
taxQuote?: PublicTaxQuote;
|
|
110
|
+
/**
|
|
111
|
+
* EVENT TICKET REQUESTS: `"deferred"` when this charge is committed now and
|
|
112
|
+
* settled only if the organiser approves. Absent (or `"immediate"`) on every
|
|
113
|
+
* ordinary sale. Drives the "Authorize" wording on the pay button.
|
|
114
|
+
*/
|
|
115
|
+
settlement?: PaymentSettlement;
|
|
116
|
+
/**
|
|
117
|
+
* How the provider defers the charge. `"authorization"` places a temporary
|
|
118
|
+
* hold the fan can see on their statement; `"saved_instrument"` stores the
|
|
119
|
+
* card and takes nothing until approval. Only meaningful with
|
|
120
|
+
* `settlement: "deferred"`.
|
|
121
|
+
*/
|
|
122
|
+
deferredStrategy?: DeferredChargeStrategy | null;
|
|
104
123
|
/**
|
|
105
124
|
* COHORT ENROLMENT ONLY: what THIS charge takes, when the plan is paid in
|
|
106
125
|
* instalments. `totalAmount` above stays the whole plan, so a checkout that
|
|
@@ -288,6 +307,116 @@ export type MembershipTier = {
|
|
|
288
307
|
welcomeMessageContent?: string | null;
|
|
289
308
|
cancellationMessageSubject?: string | null;
|
|
290
309
|
cancellationMessageContent?: string | null;
|
|
310
|
+
/**
|
|
311
|
+
* How a fan gets onto this tier (curated access). `open` is the ordinary
|
|
312
|
+
* checkout; `invite` needs a live invite for the signed-in email (and the
|
|
313
|
+
* tier is only listed when the request carried that invite's token);
|
|
314
|
+
* `application` means the fan applies and the business approves. Optional,
|
|
315
|
+
* because an older API build sends nothing, which reads as `open`.
|
|
316
|
+
*/
|
|
317
|
+
joinMode?: MembershipJoinMode;
|
|
318
|
+
/**
|
|
319
|
+
* The signed-in fan's most recent open application on this tier (or their
|
|
320
|
+
* latest declined one), so the tier card can show where it stands instead of
|
|
321
|
+
* a join button. Absent when signed out or on an older API build.
|
|
322
|
+
*/
|
|
323
|
+
myApplication?: MembershipTierMyApplication | null;
|
|
324
|
+
/**
|
|
325
|
+
* Free trial length in days on a paid tier (1 to 90), or null for no trial.
|
|
326
|
+
* A card is still collected; the first charge is on today + trialDays.
|
|
327
|
+
*/
|
|
328
|
+
trialDays?: number | null;
|
|
329
|
+
/**
|
|
330
|
+
* False once the signed-in fan has had a trial with this business (one per
|
|
331
|
+
* fan per business, any tier). Absent when signed out: treat as eligible.
|
|
332
|
+
*/
|
|
333
|
+
trialEligible?: boolean;
|
|
334
|
+
/**
|
|
335
|
+
* Questions the fan answers before paying or joining, on every join mode.
|
|
336
|
+
* Null or absent when the tier asks nothing.
|
|
337
|
+
*/
|
|
338
|
+
questionnaire?: QuestionnaireQuestion[] | null;
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
/** How a fan gets onto a membership tier. */
|
|
342
|
+
export type MembershipJoinMode = "open" | "invite" | "application";
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Where an application to a membership tier stands.
|
|
346
|
+
*
|
|
347
|
+
* card_pending paid tier: card step started, not confirmed. Not yet visible to the business.
|
|
348
|
+
* pending lodged and waiting for a decision; holds a seat
|
|
349
|
+
* approved approved; the subscription is being created
|
|
350
|
+
* payment_failed approved, but the saved card was declined; the fan can finish at checkout
|
|
351
|
+
* joined the membership exists
|
|
352
|
+
* declined not approved
|
|
353
|
+
* withdrawn withdrawn by the fan, or closed because the tier filled (`closedReason: "tier_full"`)
|
|
354
|
+
* expired the window to finish a failed payment lapsed
|
|
355
|
+
*/
|
|
356
|
+
export type MembershipApplicationStatus =
|
|
357
|
+
| "card_pending"
|
|
358
|
+
| "pending"
|
|
359
|
+
| "approved"
|
|
360
|
+
| "payment_failed"
|
|
361
|
+
| "joined"
|
|
362
|
+
| "declined"
|
|
363
|
+
| "withdrawn"
|
|
364
|
+
| "expired";
|
|
365
|
+
|
|
366
|
+
/** The slice of an application the public tier list carries per tier. */
|
|
367
|
+
export type MembershipTierMyApplication = {
|
|
368
|
+
id: string;
|
|
369
|
+
status: MembershipApplicationStatus;
|
|
370
|
+
paymentWindowExpiresAt: string | null;
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
export type MembershipApplication = {
|
|
374
|
+
id: string;
|
|
375
|
+
profileId: string;
|
|
376
|
+
membershipTierId: string;
|
|
377
|
+
/** Present on list payloads. */
|
|
378
|
+
membershipTierName?: string;
|
|
379
|
+
accountId: string;
|
|
380
|
+
status: MembershipApplicationStatus;
|
|
381
|
+
/** Null on a free tier. */
|
|
382
|
+
billingCycle: "month" | "year" | null;
|
|
383
|
+
/** What will be charged per cycle, in major units. Null on a free tier. */
|
|
384
|
+
amount: number | null;
|
|
385
|
+
currency: string | null;
|
|
386
|
+
/** A note from an older checkout. Current checkouts ask the tier's questions instead. */
|
|
387
|
+
message: string | null;
|
|
388
|
+
/** The fan's answers to the tier's questions, as given when applying. */
|
|
389
|
+
questionnaire?: QuestionnaireAnswer[] | null;
|
|
390
|
+
/** e.g. "Visa •••• 4242", set once the card is confirmed. */
|
|
391
|
+
instrumentLabel: string | null;
|
|
392
|
+
decidedAt: string | null;
|
|
393
|
+
decisionNote: string | null;
|
|
394
|
+
/** `"tier_full"`, `"expired"`, or the provider's decline reason on `payment_failed`. */
|
|
395
|
+
closedReason: string | null;
|
|
396
|
+
/** The deadline to finish a `payment_failed` application. */
|
|
397
|
+
paymentWindowExpiresAt: string | null;
|
|
398
|
+
membershipId: string | null;
|
|
399
|
+
createdAt: string;
|
|
400
|
+
updatedAt: string;
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
/** What `POST /public/membership-applications` answers. */
|
|
404
|
+
export type ApplyForMembershipResult = {
|
|
405
|
+
application: MembershipApplication;
|
|
406
|
+
/** True on a paid tier until the card is confirmed. */
|
|
407
|
+
requiresCard: boolean;
|
|
408
|
+
/** A Stripe SetupIntent secret (`seti_..._secret_...`) when `requiresCard`. */
|
|
409
|
+
clientSecret: string | null;
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
/** What `GET /public/membership-invites/info` answers for a live invite. */
|
|
413
|
+
export type MembershipInviteInfo = {
|
|
414
|
+
membershipTierId: string;
|
|
415
|
+
membershipTierName: string;
|
|
416
|
+
profileId: string;
|
|
417
|
+
/** The address the invite was sent to; the fan must be signed in with it. */
|
|
418
|
+
email: string;
|
|
419
|
+
expiresAt: string;
|
|
291
420
|
};
|
|
292
421
|
|
|
293
422
|
/**
|
|
@@ -327,6 +456,10 @@ export interface Membership {
|
|
|
327
456
|
subscriptionAmount: number;
|
|
328
457
|
subscriptionCurrency: string;
|
|
329
458
|
billingCycle: string;
|
|
459
|
+
/** Set when the membership started with a free trial: when the first charge is due. */
|
|
460
|
+
trialEndsAt?: string | null;
|
|
461
|
+
/** The fan's answers to the tier's questions, as given when joining. */
|
|
462
|
+
questionnaire?: QuestionnaireAnswer[] | null;
|
|
330
463
|
}
|
|
331
464
|
|
|
332
465
|
// ---- Public auth -------------------------------------------------------------
|
|
@@ -892,6 +1025,19 @@ export type QuestionnaireQuestion = {
|
|
|
892
1025
|
optional?: boolean;
|
|
893
1026
|
};
|
|
894
1027
|
|
|
1028
|
+
/**
|
|
1029
|
+
* A fan's answer to one {@link QuestionnaireQuestion}, as sent at checkout and
|
|
1030
|
+
* as stored on what the checkout created. `answer` is `""` for an optional
|
|
1031
|
+
* question left blank.
|
|
1032
|
+
*/
|
|
1033
|
+
export type QuestionnaireAnswer = {
|
|
1034
|
+
id: string;
|
|
1035
|
+
question: string;
|
|
1036
|
+
type: string;
|
|
1037
|
+
options?: string[];
|
|
1038
|
+
answer: string;
|
|
1039
|
+
};
|
|
1040
|
+
|
|
895
1041
|
/**
|
|
896
1042
|
* S.6 — the cancellation terms a buyer is entitled to read BEFORE they pay.
|
|
897
1043
|
*
|
|
@@ -1145,6 +1291,12 @@ export interface IEvent {
|
|
|
1145
1291
|
createdAt: string;
|
|
1146
1292
|
updatedAt: string;
|
|
1147
1293
|
tickets: ITicket[];
|
|
1294
|
+
/**
|
|
1295
|
+
* At least one tier on this event needs the organiser's approval. Optional
|
|
1296
|
+
* on the wire; derive it from `tickets` with `eventHasApprovalTiers` rather
|
|
1297
|
+
* than trusting its absence.
|
|
1298
|
+
*/
|
|
1299
|
+
hasApprovalTiers?: boolean;
|
|
1148
1300
|
media: IMedia[];
|
|
1149
1301
|
slug: string;
|
|
1150
1302
|
/**
|
|
@@ -1339,6 +1491,17 @@ export type ITicket = {
|
|
|
1339
1491
|
* {@link PublicMembershipGate}.
|
|
1340
1492
|
*/
|
|
1341
1493
|
membershipGate?: PublicMembershipGate | null;
|
|
1494
|
+
/**
|
|
1495
|
+
* The organiser decides who gets this tier (curated access, feature 3).
|
|
1496
|
+
*
|
|
1497
|
+
* A request is lodged instead of a sale: the card is authorised, not charged,
|
|
1498
|
+
* the seat is held, and the organiser has a window to approve or decline.
|
|
1499
|
+
* Sent to every client. A site on an older Forge shows such a tier with its
|
|
1500
|
+
* ordinary buy button; that is accepted until the site takes the update.
|
|
1501
|
+
*/
|
|
1502
|
+
requiresApproval?: boolean;
|
|
1503
|
+
/** How long the organiser has to decide, in hours. `null` means the platform default. */
|
|
1504
|
+
approvalWindowHours?: number | null;
|
|
1342
1505
|
};
|
|
1343
1506
|
|
|
1344
1507
|
// ---- Invoices ----------------------------------------------------------------
|
|
@@ -1506,6 +1669,12 @@ export enum OrderStatus {
|
|
|
1506
1669
|
Delivered = "delivered",
|
|
1507
1670
|
Cancelled = "cancelled",
|
|
1508
1671
|
Processed = "processed",
|
|
1672
|
+
/** Ticket request lodged: charge committed (authorised), seat held, nothing captured, no passes. */
|
|
1673
|
+
PendingApproval = "pending_approval",
|
|
1674
|
+
/** Transient: the organiser approved and settlement is in flight. */
|
|
1675
|
+
Approved = "approved",
|
|
1676
|
+
/** The organiser declined the request; the authorisation was released. */
|
|
1677
|
+
Declined = "declined",
|
|
1509
1678
|
}
|
|
1510
1679
|
|
|
1511
1680
|
export type IPublicOrderItem = {
|
|
@@ -1620,6 +1789,22 @@ export type ITicketOrder = {
|
|
|
1620
1789
|
refundedAmountCents?: number | string | null;
|
|
1621
1790
|
refundState?: "none" | "partial" | "full" | string | null;
|
|
1622
1791
|
lastRefundedAt?: string | null;
|
|
1792
|
+
/**
|
|
1793
|
+
* Ticket-request columns (curated access, feature 3). All absent on an
|
|
1794
|
+
* ordinary sale and on an older API build.
|
|
1795
|
+
*
|
|
1796
|
+
* `approvalRequestExpiresAt` is the "decide by" instant a fan is shown while
|
|
1797
|
+
* the request is `pending_approval`. `approvalFailureReason` is set when a
|
|
1798
|
+
* request ended without a sale: `"expired"` on a `cancelled` row whose window
|
|
1799
|
+
* lapsed undecided, and the provider's reason on a `payment_failed` row the
|
|
1800
|
+
* organiser approved but could not settle. See `getTicketOrderOutcome` and
|
|
1801
|
+
* `ticketRequestFailureKind`.
|
|
1802
|
+
*/
|
|
1803
|
+
approvalRequestedAt?: string | null;
|
|
1804
|
+
approvalRequestExpiresAt?: string | null;
|
|
1805
|
+
approvalDecidedAt?: string | null;
|
|
1806
|
+
approvalDecisionNote?: string | null;
|
|
1807
|
+
approvalFailureReason?: string | null;
|
|
1623
1808
|
customerName: string;
|
|
1624
1809
|
customerEmail: string;
|
|
1625
1810
|
createdAt: string;
|