@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/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/catalog") {
128
+ if (path === "/v1/checkout/session") {
100
129
  return {
101
- ...demoEvent,
102
- shows: demoEvent.shows.map((show) => ({
103
- ...show,
104
- showOptions: [
105
- ...show.showOptions.filter(
106
- (o) => o.catalogVisibility !== "PROMO_GATED"
107
- ),
108
- ...unlocked.filter(
109
- (u) => show.showOptions.some((o) => o.id === u.id)
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 Object.assign(new Error("OTP required"), {
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
- return {
175
- id: `pay_${Date.now()}`,
176
- paymentStatus: "PENDING",
177
- paymentMethod: init.body?.paymentMethod,
178
- paymentInstructions: {
179
- alias: "tickean.demo",
180
- cvu: "0000003100010000000001",
181
- amount: init.body?.amount
182
- },
183
- redirectUrl: void 0,
184
- returnUrl: null
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(`Demo transport: unhandled ${init.method || "GET"} ${path}`);
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/index.ts
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
  };