@tickean/checkout-js 0.1.0 → 0.2.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/README.md +10 -1
- package/dist/index.d.mts +281 -3
- package/dist/index.d.ts +281 -3
- package/dist/index.js +726 -47
- package/dist/index.mjs +719 -47
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var TickeanError = class extends Error {
|
|
3
|
+
constructor(payload, status, requestId) {
|
|
4
|
+
super(payload.message);
|
|
5
|
+
this.name = "TickeanError";
|
|
6
|
+
this.code = payload.code;
|
|
7
|
+
this.details = payload.details;
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.requestId = requestId;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
1
13
|
// src/demo.ts
|
|
2
14
|
var demoEvent = {
|
|
3
15
|
id: "evt_demo",
|
|
@@ -71,10 +83,26 @@ function createDemoTransport() {
|
|
|
71
83
|
let unlocked = [];
|
|
72
84
|
let purchaseId = "purchase_demo";
|
|
73
85
|
let cartRef = "cart_demo";
|
|
86
|
+
let lastPayment = null;
|
|
87
|
+
let paymentConfirmed = false;
|
|
88
|
+
const publicEvent = () => ({
|
|
89
|
+
...demoEvent,
|
|
90
|
+
shows: demoEvent.shows.map((show) => ({
|
|
91
|
+
...show,
|
|
92
|
+
showOptions: [
|
|
93
|
+
...show.showOptions.filter((o) => o.catalogVisibility !== "PROMO_GATED"),
|
|
94
|
+
...unlocked.filter((u) => show.showOptions.some((o) => o.id === u.id))
|
|
95
|
+
]
|
|
96
|
+
}))
|
|
97
|
+
});
|
|
74
98
|
return {
|
|
75
99
|
async request(path, init) {
|
|
76
100
|
if (path === "/v1/checkout/sessions" && init.method === "POST") {
|
|
77
101
|
sessionToken = `demo_${Date.now()}`;
|
|
102
|
+
buyer = null;
|
|
103
|
+
unlocked = [];
|
|
104
|
+
paymentConfirmed = false;
|
|
105
|
+
lastPayment = null;
|
|
78
106
|
return {
|
|
79
107
|
sessionId: "sess_demo",
|
|
80
108
|
sessionToken,
|
|
@@ -93,25 +121,34 @@ function createDemoTransport() {
|
|
|
93
121
|
discounts: true,
|
|
94
122
|
transfer: true,
|
|
95
123
|
onlinePayments: true
|
|
96
|
-
}
|
|
124
|
+
},
|
|
125
|
+
phase: "browsing"
|
|
97
126
|
};
|
|
98
127
|
}
|
|
99
|
-
if (path === "/v1/checkout/
|
|
128
|
+
if (path === "/v1/checkout/session") {
|
|
100
129
|
return {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
130
|
+
sessionId: "sess_demo",
|
|
131
|
+
sessionToken,
|
|
132
|
+
expiresAt: new Date(Date.now() + 36e5).toISOString(),
|
|
133
|
+
event: publicEvent(),
|
|
134
|
+
capabilities: {
|
|
135
|
+
tickets: true,
|
|
136
|
+
discounts: true,
|
|
137
|
+
transfer: true,
|
|
138
|
+
onlinePayments: true
|
|
139
|
+
},
|
|
140
|
+
status: "ACTIVE",
|
|
141
|
+
otpVerified: Boolean(buyer),
|
|
142
|
+
buyerId: buyer?.id || null,
|
|
143
|
+
purchaseId: purchaseId.startsWith("purchase_") ? purchaseId : null,
|
|
144
|
+
shoppingCartReference: cartRef,
|
|
145
|
+
nextAction: lastPayment?.nextAction || { type: "none" },
|
|
146
|
+
phase: paymentConfirmed ? "completed" : lastPayment ? "requires_action" : buyer ? "ready_to_purchase" : "browsing"
|
|
113
147
|
};
|
|
114
148
|
}
|
|
149
|
+
if (path === "/v1/checkout/catalog") {
|
|
150
|
+
return publicEvent();
|
|
151
|
+
}
|
|
115
152
|
if (path === "/v1/checkout/quote" && init.method === "POST") {
|
|
116
153
|
const body = init.body;
|
|
117
154
|
if (body.discountCode?.toUpperCase() === "DEMO2X1") {
|
|
@@ -145,8 +182,9 @@ function createDemoTransport() {
|
|
|
145
182
|
}
|
|
146
183
|
if (path === "/v1/checkout/purchases") {
|
|
147
184
|
if (!buyer) {
|
|
148
|
-
throw
|
|
149
|
-
code: "checkout_otp_required"
|
|
185
|
+
throw new TickeanError({
|
|
186
|
+
code: "checkout_otp_required",
|
|
187
|
+
message: "OTP required"
|
|
150
188
|
});
|
|
151
189
|
}
|
|
152
190
|
const body = init.body;
|
|
@@ -171,47 +209,81 @@ function createDemoTransport() {
|
|
|
171
209
|
};
|
|
172
210
|
}
|
|
173
211
|
if (path === "/v1/checkout/payments") {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
212
|
+
const body = init.body;
|
|
213
|
+
const method = String(body?.paymentMethod || "TRANSFER").toUpperCase();
|
|
214
|
+
if (method === "TRANSFER") {
|
|
215
|
+
lastPayment = {
|
|
216
|
+
id: `pay_${Date.now()}`,
|
|
217
|
+
paymentStatus: "PENDING",
|
|
218
|
+
paymentMethod: method,
|
|
219
|
+
paymentInstructions: {
|
|
220
|
+
alias: "tickean.demo",
|
|
221
|
+
cvu: "0000003100010000000001",
|
|
222
|
+
amount: body?.amount
|
|
223
|
+
},
|
|
224
|
+
redirectUrl: void 0,
|
|
225
|
+
returnUrl: null,
|
|
226
|
+
nextAction: {
|
|
227
|
+
type: "display_instructions",
|
|
228
|
+
paymentInstructions: {
|
|
229
|
+
alias: "tickean.demo",
|
|
230
|
+
cvu: "0000003100010000000001",
|
|
231
|
+
amount: body?.amount
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
requiresAction: false
|
|
235
|
+
};
|
|
236
|
+
} else {
|
|
237
|
+
lastPayment = {
|
|
238
|
+
id: `pay_${Date.now()}`,
|
|
239
|
+
paymentStatus: "PENDING",
|
|
240
|
+
paymentMethod: method,
|
|
241
|
+
redirectUrl: "https://example.com/pay/demo",
|
|
242
|
+
returnUrl: null,
|
|
243
|
+
nextAction: {
|
|
244
|
+
type: "redirect",
|
|
245
|
+
url: "https://example.com/pay/demo"
|
|
246
|
+
},
|
|
247
|
+
requiresAction: true
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
return lastPayment;
|
|
251
|
+
}
|
|
252
|
+
if (path === "/v1/checkout/payments/confirm" && init.method === "POST") {
|
|
253
|
+
paymentConfirmed = true;
|
|
254
|
+
lastPayment = {
|
|
255
|
+
...lastPayment || { id: `pay_${Date.now()}` },
|
|
256
|
+
paymentStatus: "COMPLETED",
|
|
257
|
+
nextAction: { type: "none" },
|
|
258
|
+
requiresAction: false
|
|
185
259
|
};
|
|
260
|
+
return lastPayment;
|
|
186
261
|
}
|
|
187
262
|
if (path === "/v1/checkout/payments/status") {
|
|
188
263
|
return {
|
|
189
|
-
status: "PENDING",
|
|
264
|
+
status: paymentConfirmed ? "COMPLETED" : "PENDING",
|
|
265
|
+
requiresAction: Boolean(
|
|
266
|
+
lastPayment?.nextAction && lastPayment.nextAction.type !== "none" && !paymentConfirmed
|
|
267
|
+
),
|
|
268
|
+
phase: paymentConfirmed ? "completed" : lastPayment ? "requires_action" : "browsing",
|
|
269
|
+
nextAction: lastPayment?.nextAction || { type: "none" },
|
|
270
|
+
payment: lastPayment,
|
|
190
271
|
purchase: {
|
|
191
272
|
id: purchaseId,
|
|
192
|
-
status: "PENDING",
|
|
273
|
+
status: paymentConfirmed ? "COMPLETED" : "PENDING",
|
|
193
274
|
totalPrice: 0,
|
|
194
275
|
currency: "ARS",
|
|
195
276
|
shoppingCartReference: cartRef
|
|
196
277
|
}
|
|
197
278
|
};
|
|
198
279
|
}
|
|
199
|
-
throw new Error(
|
|
280
|
+
throw new Error(
|
|
281
|
+
`Demo transport: unhandled ${init.method || "GET"} ${path}`
|
|
282
|
+
);
|
|
200
283
|
}
|
|
201
284
|
};
|
|
202
285
|
}
|
|
203
286
|
|
|
204
|
-
// src/types.ts
|
|
205
|
-
var TickeanError = class extends Error {
|
|
206
|
-
constructor(payload, status) {
|
|
207
|
-
super(payload.message);
|
|
208
|
-
this.name = "TickeanError";
|
|
209
|
-
this.code = payload.code;
|
|
210
|
-
this.details = payload.details;
|
|
211
|
-
this.status = status;
|
|
212
|
-
}
|
|
213
|
-
};
|
|
214
|
-
|
|
215
287
|
// src/http.ts
|
|
216
288
|
function createHttpTransport(options) {
|
|
217
289
|
const fetchImpl = options.fetchImpl || fetch;
|
|
@@ -224,6 +296,9 @@ function createHttpTransport(options) {
|
|
|
224
296
|
if (init.sessionToken) {
|
|
225
297
|
headers["X-Tickean-Checkout-Session"] = init.sessionToken;
|
|
226
298
|
}
|
|
299
|
+
if (init.idempotencyKey) {
|
|
300
|
+
headers["Idempotency-Key"] = init.idempotencyKey;
|
|
301
|
+
}
|
|
227
302
|
const response = await fetchImpl(
|
|
228
303
|
`${options.apiBaseUrl.replace(/\/$/, "")}${path}`,
|
|
229
304
|
{
|
|
@@ -233,6 +308,7 @@ function createHttpTransport(options) {
|
|
|
233
308
|
credentials: "omit"
|
|
234
309
|
}
|
|
235
310
|
);
|
|
311
|
+
const requestId = response.headers.get("X-Request-Id") || response.headers.get("x-request-id") || void 0;
|
|
236
312
|
const json = await response.json().catch(() => ({}));
|
|
237
313
|
if (!response.ok) {
|
|
238
314
|
const err = json?.error || {};
|
|
@@ -242,7 +318,8 @@ function createHttpTransport(options) {
|
|
|
242
318
|
message: err.message || response.statusText || "Request failed",
|
|
243
319
|
details: err.details
|
|
244
320
|
},
|
|
245
|
-
response.status
|
|
321
|
+
response.status,
|
|
322
|
+
requestId
|
|
246
323
|
);
|
|
247
324
|
}
|
|
248
325
|
return json;
|
|
@@ -250,7 +327,7 @@ function createHttpTransport(options) {
|
|
|
250
327
|
};
|
|
251
328
|
}
|
|
252
329
|
|
|
253
|
-
// src/
|
|
330
|
+
// src/client.ts
|
|
254
331
|
function createTickean(options) {
|
|
255
332
|
if (!options.publishableKey && !options.demo) {
|
|
256
333
|
throw new TickeanError({
|
|
@@ -277,6 +354,9 @@ function createTickean(options) {
|
|
|
277
354
|
get session() {
|
|
278
355
|
return session;
|
|
279
356
|
},
|
|
357
|
+
set session(value) {
|
|
358
|
+
session = value;
|
|
359
|
+
},
|
|
280
360
|
async createSession(params) {
|
|
281
361
|
session = await transport.request("/v1/checkout/sessions", {
|
|
282
362
|
method: "POST",
|
|
@@ -284,6 +364,22 @@ function createTickean(options) {
|
|
|
284
364
|
});
|
|
285
365
|
return session;
|
|
286
366
|
},
|
|
367
|
+
async getSession() {
|
|
368
|
+
const active = requireSession();
|
|
369
|
+
const result = await transport.request(
|
|
370
|
+
"/v1/checkout/session",
|
|
371
|
+
{
|
|
372
|
+
sessionToken: active.sessionToken
|
|
373
|
+
}
|
|
374
|
+
);
|
|
375
|
+
session = {
|
|
376
|
+
...active,
|
|
377
|
+
...result,
|
|
378
|
+
sessionToken: active.sessionToken,
|
|
379
|
+
event: result.event || active.event
|
|
380
|
+
};
|
|
381
|
+
return session;
|
|
382
|
+
},
|
|
287
383
|
async getCatalog() {
|
|
288
384
|
const active = requireSession();
|
|
289
385
|
return transport.request("/v1/checkout/catalog", {
|
|
@@ -319,7 +415,8 @@ function createTickean(options) {
|
|
|
319
415
|
return transport.request("/v1/checkout/purchases", {
|
|
320
416
|
method: "POST",
|
|
321
417
|
body: params,
|
|
322
|
-
sessionToken: active.sessionToken
|
|
418
|
+
sessionToken: active.sessionToken,
|
|
419
|
+
idempotencyKey: params.idempotencyKey
|
|
323
420
|
});
|
|
324
421
|
},
|
|
325
422
|
async createPayment(params) {
|
|
@@ -327,7 +424,17 @@ function createTickean(options) {
|
|
|
327
424
|
return transport.request("/v1/checkout/payments", {
|
|
328
425
|
method: "POST",
|
|
329
426
|
body: params,
|
|
330
|
-
sessionToken: active.sessionToken
|
|
427
|
+
sessionToken: active.sessionToken,
|
|
428
|
+
idempotencyKey: params.idempotencyKey
|
|
429
|
+
});
|
|
430
|
+
},
|
|
431
|
+
async confirmPayment(params) {
|
|
432
|
+
const active = requireSession();
|
|
433
|
+
return transport.request("/v1/checkout/payments/confirm", {
|
|
434
|
+
method: "POST",
|
|
435
|
+
body: params || {},
|
|
436
|
+
sessionToken: active.sessionToken,
|
|
437
|
+
idempotencyKey: params?.idempotencyKey
|
|
331
438
|
});
|
|
332
439
|
},
|
|
333
440
|
async getPaymentStatus() {
|
|
@@ -348,13 +455,16 @@ function createTickean(options) {
|
|
|
348
455
|
});
|
|
349
456
|
}
|
|
350
457
|
const status = await this.getPaymentStatus();
|
|
351
|
-
if (["COMPLETED", "CONFIRMED", "PAID"].includes(
|
|
458
|
+
if (["COMPLETED", "CONFIRMED", "PAID", "SUCCESS"].includes(
|
|
352
459
|
String(status.status || "").toUpperCase()
|
|
353
|
-
) || ["COMPLETED", "CONFIRMED", "PAID"].includes(
|
|
460
|
+
) || ["COMPLETED", "CONFIRMED", "PAID", "SUCCESS"].includes(
|
|
354
461
|
String(status.purchase?.status || "").toUpperCase()
|
|
355
462
|
)) {
|
|
356
463
|
return status;
|
|
357
464
|
}
|
|
465
|
+
if (status.nextAction && status.nextAction.type !== "none" && status.requiresAction) {
|
|
466
|
+
return status;
|
|
467
|
+
}
|
|
358
468
|
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
359
469
|
}
|
|
360
470
|
throw new TickeanError({
|
|
@@ -364,7 +474,569 @@ function createTickean(options) {
|
|
|
364
474
|
}
|
|
365
475
|
};
|
|
366
476
|
}
|
|
477
|
+
|
|
478
|
+
// src/state.ts
|
|
479
|
+
function createInitialState(partial) {
|
|
480
|
+
return {
|
|
481
|
+
phase: "initializing",
|
|
482
|
+
session: null,
|
|
483
|
+
event: null,
|
|
484
|
+
cart: [],
|
|
485
|
+
discountCode: null,
|
|
486
|
+
quote: null,
|
|
487
|
+
isQuoting: false,
|
|
488
|
+
buyer: null,
|
|
489
|
+
buyerVerified: false,
|
|
490
|
+
otpSent: false,
|
|
491
|
+
purchase: null,
|
|
492
|
+
payment: null,
|
|
493
|
+
nextAction: { type: "none" },
|
|
494
|
+
error: null,
|
|
495
|
+
loading: true,
|
|
496
|
+
...partial
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
function derivePhase(state) {
|
|
500
|
+
if (state.phase === "completed" || state.phase === "failed" || state.phase === "expired" || state.phase === "purchasing" || state.phase === "processing" || state.phase === "requires_action" || state.phase === "initializing") {
|
|
501
|
+
return state.phase;
|
|
502
|
+
}
|
|
503
|
+
if (state.isQuoting) return "quoting";
|
|
504
|
+
if (state.otpSent && !state.buyerVerified) return "verifying_buyer";
|
|
505
|
+
if (state.buyerVerified && state.cart.length > 0) return "ready_to_purchase";
|
|
506
|
+
return "browsing";
|
|
507
|
+
}
|
|
508
|
+
function checkoutReducer(state, action) {
|
|
509
|
+
switch (action.type) {
|
|
510
|
+
case "INIT_START":
|
|
511
|
+
return { ...state, loading: true, error: null, phase: "initializing" };
|
|
512
|
+
case "INIT_SUCCESS": {
|
|
513
|
+
const next = {
|
|
514
|
+
...state,
|
|
515
|
+
loading: false,
|
|
516
|
+
error: null,
|
|
517
|
+
session: action.session,
|
|
518
|
+
event: action.event,
|
|
519
|
+
phase: "browsing"
|
|
520
|
+
};
|
|
521
|
+
return { ...next, phase: derivePhase(next) };
|
|
522
|
+
}
|
|
523
|
+
case "INIT_FAILURE":
|
|
524
|
+
return {
|
|
525
|
+
...state,
|
|
526
|
+
loading: false,
|
|
527
|
+
error: action.error,
|
|
528
|
+
phase: "failed"
|
|
529
|
+
};
|
|
530
|
+
case "SET_CART": {
|
|
531
|
+
const next = { ...state, cart: action.cart, error: null };
|
|
532
|
+
return { ...next, phase: derivePhase(next) };
|
|
533
|
+
}
|
|
534
|
+
case "SET_DISCOUNT":
|
|
535
|
+
return { ...state, discountCode: action.discountCode };
|
|
536
|
+
case "QUOTE_START": {
|
|
537
|
+
const next = { ...state, isQuoting: true, error: null };
|
|
538
|
+
return { ...next, phase: "quoting" };
|
|
539
|
+
}
|
|
540
|
+
case "QUOTE_SUCCESS": {
|
|
541
|
+
const next = {
|
|
542
|
+
...state,
|
|
543
|
+
isQuoting: false,
|
|
544
|
+
quote: action.quote
|
|
545
|
+
};
|
|
546
|
+
return { ...next, phase: derivePhase(next) };
|
|
547
|
+
}
|
|
548
|
+
case "QUOTE_FAILURE":
|
|
549
|
+
return {
|
|
550
|
+
...state,
|
|
551
|
+
isQuoting: false,
|
|
552
|
+
error: action.error,
|
|
553
|
+
phase: derivePhase({ ...state, isQuoting: false })
|
|
554
|
+
};
|
|
555
|
+
case "OTP_SENT": {
|
|
556
|
+
const next = { ...state, otpSent: true, error: null };
|
|
557
|
+
return { ...next, phase: "verifying_buyer" };
|
|
558
|
+
}
|
|
559
|
+
case "OTP_VERIFIED": {
|
|
560
|
+
const next = {
|
|
561
|
+
...state,
|
|
562
|
+
buyer: action.buyer,
|
|
563
|
+
buyerVerified: true,
|
|
564
|
+
otpSent: true,
|
|
565
|
+
error: null
|
|
566
|
+
};
|
|
567
|
+
return { ...next, phase: derivePhase(next) };
|
|
568
|
+
}
|
|
569
|
+
case "SET_EVENT":
|
|
570
|
+
return { ...state, event: action.event };
|
|
571
|
+
case "PURCHASE_START":
|
|
572
|
+
return { ...state, phase: "purchasing", error: null };
|
|
573
|
+
case "PURCHASE_SUCCESS": {
|
|
574
|
+
const requiresProviderAction = action.nextAction.type !== "none" && action.nextAction.type !== "display_instructions";
|
|
575
|
+
return {
|
|
576
|
+
...state,
|
|
577
|
+
purchase: action.purchase,
|
|
578
|
+
payment: action.payment,
|
|
579
|
+
nextAction: action.nextAction,
|
|
580
|
+
phase: requiresProviderAction ? "requires_action" : "processing",
|
|
581
|
+
error: null
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
case "PURCHASE_FAILURE":
|
|
585
|
+
return { ...state, phase: "failed", error: action.error };
|
|
586
|
+
case "SET_NEXT_ACTION":
|
|
587
|
+
return {
|
|
588
|
+
...state,
|
|
589
|
+
nextAction: action.nextAction,
|
|
590
|
+
payment: action.payment === void 0 ? state.payment : action.payment,
|
|
591
|
+
phase: action.nextAction.type !== "none" && action.nextAction.type !== "display_instructions" ? "requires_action" : state.phase === "requires_action" ? "processing" : state.phase
|
|
592
|
+
};
|
|
593
|
+
case "PROCESSING":
|
|
594
|
+
return { ...state, phase: "processing" };
|
|
595
|
+
case "COMPLETED":
|
|
596
|
+
return {
|
|
597
|
+
...state,
|
|
598
|
+
phase: "completed",
|
|
599
|
+
purchase: action.purchase ?? state.purchase,
|
|
600
|
+
nextAction: { type: "none" },
|
|
601
|
+
error: null
|
|
602
|
+
};
|
|
603
|
+
case "FAILED":
|
|
604
|
+
return { ...state, phase: "failed", error: action.error };
|
|
605
|
+
case "EXPIRED":
|
|
606
|
+
return { ...state, phase: "expired" };
|
|
607
|
+
case "CLEAR_ERROR":
|
|
608
|
+
return { ...state, error: null };
|
|
609
|
+
case "REHYDRATE":
|
|
610
|
+
return { ...state, ...action.partial };
|
|
611
|
+
default:
|
|
612
|
+
return state;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// src/persistence.ts
|
|
617
|
+
function createMemoryPersistence() {
|
|
618
|
+
const store = /* @__PURE__ */ new Map();
|
|
619
|
+
return {
|
|
620
|
+
get(key) {
|
|
621
|
+
return store.get(key) ?? null;
|
|
622
|
+
},
|
|
623
|
+
set(key, value) {
|
|
624
|
+
store.set(key, { ...value });
|
|
625
|
+
},
|
|
626
|
+
remove(key) {
|
|
627
|
+
store.delete(key);
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
function createSessionStoragePersistence(storage) {
|
|
632
|
+
const getStorage = () => {
|
|
633
|
+
if (storage) return storage;
|
|
634
|
+
try {
|
|
635
|
+
if (typeof sessionStorage !== "undefined") return sessionStorage;
|
|
636
|
+
} catch {
|
|
637
|
+
}
|
|
638
|
+
return null;
|
|
639
|
+
};
|
|
640
|
+
return {
|
|
641
|
+
get(key) {
|
|
642
|
+
const s = getStorage();
|
|
643
|
+
if (!s) return null;
|
|
644
|
+
try {
|
|
645
|
+
const raw = s.getItem(key);
|
|
646
|
+
if (!raw) return null;
|
|
647
|
+
return JSON.parse(raw);
|
|
648
|
+
} catch {
|
|
649
|
+
return null;
|
|
650
|
+
}
|
|
651
|
+
},
|
|
652
|
+
set(key, value) {
|
|
653
|
+
const s = getStorage();
|
|
654
|
+
if (!s) return;
|
|
655
|
+
try {
|
|
656
|
+
const safe = {
|
|
657
|
+
sessionToken: value.sessionToken,
|
|
658
|
+
eventSlug: value.eventSlug,
|
|
659
|
+
cart: value.cart,
|
|
660
|
+
discountCode: value.discountCode,
|
|
661
|
+
phase: value.phase,
|
|
662
|
+
buyerVerified: value.buyerVerified,
|
|
663
|
+
purchaseId: value.purchaseId
|
|
664
|
+
};
|
|
665
|
+
s.setItem(key, JSON.stringify(safe));
|
|
666
|
+
} catch {
|
|
667
|
+
}
|
|
668
|
+
},
|
|
669
|
+
remove(key) {
|
|
670
|
+
const s = getStorage();
|
|
671
|
+
if (!s) return;
|
|
672
|
+
try {
|
|
673
|
+
s.removeItem(key);
|
|
674
|
+
} catch {
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// src/telemetry.ts
|
|
681
|
+
function createNoopTelemetry() {
|
|
682
|
+
return {
|
|
683
|
+
track() {
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
function createEventEmitterTelemetry(listeners = []) {
|
|
688
|
+
const subs = new Set(listeners);
|
|
689
|
+
return {
|
|
690
|
+
track(event) {
|
|
691
|
+
for (const listener of subs) {
|
|
692
|
+
try {
|
|
693
|
+
listener(event);
|
|
694
|
+
} catch {
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
},
|
|
698
|
+
subscribe(listener) {
|
|
699
|
+
subs.add(listener);
|
|
700
|
+
return () => {
|
|
701
|
+
subs.delete(listener);
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// src/controller.ts
|
|
708
|
+
function generateIdempotencyKey() {
|
|
709
|
+
if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
|
|
710
|
+
return crypto.randomUUID();
|
|
711
|
+
}
|
|
712
|
+
return `idem_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
713
|
+
}
|
|
714
|
+
function toTickeanError(err) {
|
|
715
|
+
if (err instanceof TickeanError) return err;
|
|
716
|
+
const anyErr = err;
|
|
717
|
+
return new TickeanError(
|
|
718
|
+
{
|
|
719
|
+
code: anyErr?.code || "checkout_unknown_error",
|
|
720
|
+
message: anyErr?.message || "Unexpected checkout error",
|
|
721
|
+
details: anyErr?.details
|
|
722
|
+
},
|
|
723
|
+
anyErr?.status
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
function createCheckoutController(options) {
|
|
727
|
+
const client = createTickean(options);
|
|
728
|
+
const telemetry = options.telemetry || createNoopTelemetry();
|
|
729
|
+
const persistenceKey = options.persistenceKey || `tickean.checkout.${options.eventSlug}.${options.publishableKey || "demo"}`;
|
|
730
|
+
const persistence = options.persistence === false ? null : options.persistence || (typeof sessionStorage !== "undefined" ? createSessionStoragePersistence() : createMemoryPersistence());
|
|
731
|
+
let state = createInitialState();
|
|
732
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
733
|
+
let quoteTimer = null;
|
|
734
|
+
let quoteGeneration = 0;
|
|
735
|
+
const quoteDebounceMs = options.quoteDebounceMs ?? 300;
|
|
736
|
+
let disposed = false;
|
|
737
|
+
const emit = (name, properties) => {
|
|
738
|
+
telemetry.track({ name, timestamp: Date.now(), properties });
|
|
739
|
+
};
|
|
740
|
+
const notify = () => {
|
|
741
|
+
const snapshot = getSnapshot();
|
|
742
|
+
for (const listener of listeners) {
|
|
743
|
+
try {
|
|
744
|
+
listener(snapshot);
|
|
745
|
+
} catch {
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
const dispatch = (action) => {
|
|
750
|
+
state = checkoutReducer(state, action);
|
|
751
|
+
persist();
|
|
752
|
+
notify();
|
|
753
|
+
};
|
|
754
|
+
const persist = () => {
|
|
755
|
+
if (!persistence) return;
|
|
756
|
+
persistence.set(persistenceKey, {
|
|
757
|
+
sessionToken: state.session?.sessionToken,
|
|
758
|
+
eventSlug: options.eventSlug,
|
|
759
|
+
cart: state.cart,
|
|
760
|
+
discountCode: state.discountCode,
|
|
761
|
+
phase: state.phase,
|
|
762
|
+
buyerVerified: state.buyerVerified,
|
|
763
|
+
purchaseId: state.purchase?.id ?? null
|
|
764
|
+
});
|
|
765
|
+
};
|
|
766
|
+
const getSnapshot = () => state;
|
|
767
|
+
const subscribe = (listener) => {
|
|
768
|
+
listeners.add(listener);
|
|
769
|
+
return () => {
|
|
770
|
+
listeners.delete(listener);
|
|
771
|
+
};
|
|
772
|
+
};
|
|
773
|
+
const scheduleQuote = () => {
|
|
774
|
+
if (quoteTimer) clearTimeout(quoteTimer);
|
|
775
|
+
if (!state.session || state.cart.length === 0) {
|
|
776
|
+
dispatch({
|
|
777
|
+
type: "QUOTE_SUCCESS",
|
|
778
|
+
quote: {
|
|
779
|
+
valid: true,
|
|
780
|
+
totalPrice: 0,
|
|
781
|
+
pricingBreakdown: { subtotal: 0 }
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
const gen = ++quoteGeneration;
|
|
787
|
+
quoteTimer = setTimeout(async () => {
|
|
788
|
+
dispatch({ type: "QUOTE_START" });
|
|
789
|
+
emit("checkout.quote.start", { items: state.cart.length });
|
|
790
|
+
try {
|
|
791
|
+
const quote = await client.quote({
|
|
792
|
+
items: state.cart,
|
|
793
|
+
discountCode: state.discountCode || void 0
|
|
794
|
+
});
|
|
795
|
+
if (disposed || gen !== quoteGeneration) return;
|
|
796
|
+
dispatch({ type: "QUOTE_SUCCESS", quote });
|
|
797
|
+
emit("checkout.quote.success", { totalPrice: quote.totalPrice });
|
|
798
|
+
if ((quote.unlockedShowOptions || []).length > 0) {
|
|
799
|
+
const catalog = await client.getCatalog();
|
|
800
|
+
if (!disposed && gen === quoteGeneration) {
|
|
801
|
+
dispatch({ type: "SET_EVENT", event: catalog });
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
} catch (err) {
|
|
805
|
+
if (disposed || gen !== quoteGeneration) return;
|
|
806
|
+
const error = toTickeanError(err);
|
|
807
|
+
dispatch({ type: "QUOTE_FAILURE", error });
|
|
808
|
+
emit("checkout.quote.error", { code: error.code });
|
|
809
|
+
}
|
|
810
|
+
}, quoteDebounceMs);
|
|
811
|
+
};
|
|
812
|
+
const setCartItem = (showOptionId, amount) => {
|
|
813
|
+
const next = state.cart.filter((item) => item.showOptionId !== showOptionId);
|
|
814
|
+
if (amount > 0) next.push({ showOptionId, amount });
|
|
815
|
+
dispatch({ type: "SET_CART", cart: next });
|
|
816
|
+
emit("checkout.cart.change", { showOptionId, amount });
|
|
817
|
+
scheduleQuote();
|
|
818
|
+
};
|
|
819
|
+
const setCart = (cart) => {
|
|
820
|
+
dispatch({
|
|
821
|
+
type: "SET_CART",
|
|
822
|
+
cart: cart.filter((item) => item.amount > 0)
|
|
823
|
+
});
|
|
824
|
+
emit("checkout.cart.change", { count: cart.length });
|
|
825
|
+
scheduleQuote();
|
|
826
|
+
};
|
|
827
|
+
const applyDiscountCode = async (code) => {
|
|
828
|
+
dispatch({ type: "SET_DISCOUNT", discountCode: code || null });
|
|
829
|
+
if (!state.session) {
|
|
830
|
+
throw new TickeanError({
|
|
831
|
+
code: "checkout_session_required",
|
|
832
|
+
message: "Session required"
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
dispatch({ type: "QUOTE_START" });
|
|
836
|
+
try {
|
|
837
|
+
const quote = await client.quote({
|
|
838
|
+
items: state.cart,
|
|
839
|
+
discountCode: code || void 0
|
|
840
|
+
});
|
|
841
|
+
dispatch({ type: "QUOTE_SUCCESS", quote });
|
|
842
|
+
emit("checkout.discount.applied", { code: code || null });
|
|
843
|
+
if ((quote.unlockedShowOptions || []).length > 0) {
|
|
844
|
+
const catalog = await client.getCatalog();
|
|
845
|
+
dispatch({ type: "SET_EVENT", event: catalog });
|
|
846
|
+
}
|
|
847
|
+
return quote;
|
|
848
|
+
} catch (err) {
|
|
849
|
+
const error = toTickeanError(err);
|
|
850
|
+
dispatch({ type: "QUOTE_FAILURE", error });
|
|
851
|
+
throw error;
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
const sendOtp = async (phone, channel) => {
|
|
855
|
+
await client.sendOtp({ phone, channel });
|
|
856
|
+
dispatch({ type: "OTP_SENT" });
|
|
857
|
+
emit("checkout.otp.sent");
|
|
858
|
+
};
|
|
859
|
+
const verifyOtp = async (params) => {
|
|
860
|
+
const result = await client.verifyOtp(params);
|
|
861
|
+
const buyer = result.buyer || {
|
|
862
|
+
id: "buyer",
|
|
863
|
+
phone: params.phone,
|
|
864
|
+
name: params.name,
|
|
865
|
+
email: params.email
|
|
866
|
+
};
|
|
867
|
+
dispatch({ type: "OTP_VERIFIED", buyer });
|
|
868
|
+
emit("checkout.otp.verified");
|
|
869
|
+
};
|
|
870
|
+
const purchaseAndPay = async (params) => {
|
|
871
|
+
dispatch({ type: "PURCHASE_START" });
|
|
872
|
+
const idempotencyKey = params.idempotencyKey || generateIdempotencyKey();
|
|
873
|
+
emit("checkout.purchase.start", { paymentMethod: params.paymentMethod });
|
|
874
|
+
try {
|
|
875
|
+
const purchase = await client.createPurchase({
|
|
876
|
+
items: state.cart,
|
|
877
|
+
paymentMethod: params.paymentMethod,
|
|
878
|
+
currency: params.currency,
|
|
879
|
+
discountCode: params.discountCode ?? state.discountCode ?? void 0,
|
|
880
|
+
expectedTotal: state.quote?.totalPrice,
|
|
881
|
+
showId: params.showId,
|
|
882
|
+
idempotencyKey
|
|
883
|
+
});
|
|
884
|
+
const payment = await client.createPayment({
|
|
885
|
+
orderId: purchase.purchase.id,
|
|
886
|
+
paymentMethod: params.paymentMethod,
|
|
887
|
+
currency: params.currency,
|
|
888
|
+
amount: purchase.purchase.totalPrice,
|
|
889
|
+
idempotencyKey: `${idempotencyKey}:payment`
|
|
890
|
+
});
|
|
891
|
+
const nextAction = payment.nextAction || (payment.paymentInstructions ? {
|
|
892
|
+
type: "display_instructions",
|
|
893
|
+
paymentInstructions: payment.paymentInstructions
|
|
894
|
+
} : payment.redirectUrl ? { type: "redirect", url: payment.redirectUrl } : { type: "none" });
|
|
895
|
+
dispatch({
|
|
896
|
+
type: "PURCHASE_SUCCESS",
|
|
897
|
+
purchase: purchase.purchase,
|
|
898
|
+
payment,
|
|
899
|
+
nextAction
|
|
900
|
+
});
|
|
901
|
+
emit("checkout.purchase.success", {
|
|
902
|
+
purchaseId: purchase.purchase.id,
|
|
903
|
+
nextActionType: nextAction.type
|
|
904
|
+
});
|
|
905
|
+
return { purchaseId: purchase.purchase.id, payment, nextAction };
|
|
906
|
+
} catch (err) {
|
|
907
|
+
const error = toTickeanError(err);
|
|
908
|
+
dispatch({ type: "PURCHASE_FAILURE", error });
|
|
909
|
+
emit("checkout.purchase.error", { code: error.code });
|
|
910
|
+
throw error;
|
|
911
|
+
}
|
|
912
|
+
};
|
|
913
|
+
const confirmPayment = async (params) => {
|
|
914
|
+
const payment = await client.confirmPayment({
|
|
915
|
+
...params,
|
|
916
|
+
idempotencyKey: params?.idempotencyKey || generateIdempotencyKey()
|
|
917
|
+
});
|
|
918
|
+
const nextAction = payment.nextAction || { type: "none" };
|
|
919
|
+
dispatch({ type: "SET_NEXT_ACTION", nextAction, payment });
|
|
920
|
+
emit("checkout.payment.confirmed", { nextActionType: nextAction.type });
|
|
921
|
+
return payment;
|
|
922
|
+
};
|
|
923
|
+
const watchPayment = async (watchOptions) => {
|
|
924
|
+
dispatch({ type: "PROCESSING" });
|
|
925
|
+
emit("checkout.payment.watch.start");
|
|
926
|
+
try {
|
|
927
|
+
const status = await client.watchPayment(watchOptions);
|
|
928
|
+
if (["COMPLETED", "CONFIRMED", "PAID", "SUCCESS"].includes(
|
|
929
|
+
String(status.status || "").toUpperCase()
|
|
930
|
+
)) {
|
|
931
|
+
dispatch({ type: "COMPLETED", purchase: status.purchase });
|
|
932
|
+
emit("checkout.completed");
|
|
933
|
+
} else if (status.nextAction && status.nextAction.type !== "none") {
|
|
934
|
+
dispatch({
|
|
935
|
+
type: "SET_NEXT_ACTION",
|
|
936
|
+
nextAction: status.nextAction,
|
|
937
|
+
payment: status.payment
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
return status;
|
|
941
|
+
} catch (err) {
|
|
942
|
+
const error = toTickeanError(err);
|
|
943
|
+
if (error.code !== "checkout_payment_timeout") {
|
|
944
|
+
dispatch({ type: "FAILED", error });
|
|
945
|
+
}
|
|
946
|
+
emit("checkout.payment.watch.error", { code: error.code });
|
|
947
|
+
throw error;
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
const refreshCatalog = async () => {
|
|
951
|
+
const catalog = await client.getCatalog();
|
|
952
|
+
dispatch({ type: "SET_EVENT", event: catalog });
|
|
953
|
+
return catalog;
|
|
954
|
+
};
|
|
955
|
+
const initialize = async () => {
|
|
956
|
+
dispatch({ type: "INIT_START" });
|
|
957
|
+
emit("checkout.init.start", { eventSlug: options.eventSlug });
|
|
958
|
+
try {
|
|
959
|
+
const stored = persistence?.get(persistenceKey);
|
|
960
|
+
let session = await client.createSession({
|
|
961
|
+
eventSlug: options.eventSlug,
|
|
962
|
+
returnUrl: options.returnUrl
|
|
963
|
+
});
|
|
964
|
+
if (stored?.sessionToken && stored.sessionToken !== session.sessionToken) {
|
|
965
|
+
try {
|
|
966
|
+
client.session = {
|
|
967
|
+
...session,
|
|
968
|
+
sessionToken: stored.sessionToken
|
|
969
|
+
};
|
|
970
|
+
const resumed = await client.getSession();
|
|
971
|
+
session = {
|
|
972
|
+
...session,
|
|
973
|
+
...resumed,
|
|
974
|
+
sessionToken: stored.sessionToken,
|
|
975
|
+
event: resumed.event || session.event
|
|
976
|
+
};
|
|
977
|
+
} catch {
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
dispatch({
|
|
981
|
+
type: "INIT_SUCCESS",
|
|
982
|
+
session,
|
|
983
|
+
event: session.event
|
|
984
|
+
});
|
|
985
|
+
if (stored?.cart?.length) {
|
|
986
|
+
dispatch({ type: "SET_CART", cart: stored.cart });
|
|
987
|
+
if (stored.discountCode) {
|
|
988
|
+
dispatch({ type: "SET_DISCOUNT", discountCode: stored.discountCode });
|
|
989
|
+
}
|
|
990
|
+
if (stored.buyerVerified) {
|
|
991
|
+
dispatch({
|
|
992
|
+
type: "REHYDRATE",
|
|
993
|
+
partial: { buyerVerified: true }
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
scheduleQuote();
|
|
997
|
+
}
|
|
998
|
+
emit("checkout.init.success", { sessionId: session.sessionId });
|
|
999
|
+
} catch (err) {
|
|
1000
|
+
const error = toTickeanError(err);
|
|
1001
|
+
dispatch({ type: "INIT_FAILURE", error });
|
|
1002
|
+
emit("checkout.init.error", { code: error.code });
|
|
1003
|
+
throw error;
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
const dispose = () => {
|
|
1007
|
+
disposed = true;
|
|
1008
|
+
if (quoteTimer) clearTimeout(quoteTimer);
|
|
1009
|
+
listeners.clear();
|
|
1010
|
+
};
|
|
1011
|
+
const ready = initialize().catch(() => {
|
|
1012
|
+
});
|
|
1013
|
+
return {
|
|
1014
|
+
client,
|
|
1015
|
+
ready,
|
|
1016
|
+
getSnapshot,
|
|
1017
|
+
subscribe,
|
|
1018
|
+
setCartItem,
|
|
1019
|
+
setCart,
|
|
1020
|
+
applyDiscountCode,
|
|
1021
|
+
sendOtp,
|
|
1022
|
+
verifyOtp,
|
|
1023
|
+
purchaseAndPay,
|
|
1024
|
+
confirmPayment,
|
|
1025
|
+
watchPayment,
|
|
1026
|
+
refreshCatalog,
|
|
1027
|
+
dispose,
|
|
1028
|
+
/** @internal test helper */
|
|
1029
|
+
_dispatch: dispatch
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
367
1032
|
export {
|
|
368
1033
|
TickeanError,
|
|
1034
|
+
checkoutReducer,
|
|
1035
|
+
createCheckoutController,
|
|
1036
|
+
createEventEmitterTelemetry,
|
|
1037
|
+
createInitialState,
|
|
1038
|
+
createMemoryPersistence,
|
|
1039
|
+
createNoopTelemetry,
|
|
1040
|
+
createSessionStoragePersistence,
|
|
369
1041
|
createTickean
|
|
370
1042
|
};
|