@tagadapay/headless-sdk 1.3.2 → 1.5.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.
Files changed (50) hide show
  1. package/README.md +84 -7
  2. package/dist/core/client.d.ts +6 -0
  3. package/dist/core/client.d.ts.map +1 -1
  4. package/dist/core/client.js +6 -0
  5. package/dist/core/client.js.map +1 -1
  6. package/dist/core/http.js +2 -2
  7. package/dist/core/http.js.map +1 -1
  8. package/dist/core/index.d.ts +1 -1
  9. package/dist/core/index.d.ts.map +1 -1
  10. package/dist/core/modules/checkout.d.ts +113 -12
  11. package/dist/core/modules/checkout.d.ts.map +1 -1
  12. package/dist/core/modules/checkout.js +201 -55
  13. package/dist/core/modules/checkout.js.map +1 -1
  14. package/dist/core/modules/payment.d.ts +120 -4
  15. package/dist/core/modules/payment.d.ts.map +1 -1
  16. package/dist/core/modules/payment.js +336 -7
  17. package/dist/core/modules/payment.js.map +1 -1
  18. package/dist/core/types.d.ts +112 -2
  19. package/dist/core/types.d.ts.map +1 -1
  20. package/dist/react/hooks/useCheckout.d.ts.map +1 -1
  21. package/dist/react/hooks/useCheckout.js +9 -15
  22. package/dist/react/hooks/useCheckout.js.map +1 -1
  23. package/dist/react/hooks/usePayment.d.ts +58 -13
  24. package/dist/react/hooks/usePayment.d.ts.map +1 -1
  25. package/dist/react/hooks/usePayment.js +124 -11
  26. package/dist/react/hooks/usePayment.js.map +1 -1
  27. package/dist/react/index.d.ts +2 -2
  28. package/dist/react/index.d.ts.map +1 -1
  29. package/dist/stripe/StripeExpressButton.d.ts +30 -0
  30. package/dist/stripe/StripeExpressButton.d.ts.map +1 -0
  31. package/dist/stripe/StripeExpressButton.js +32 -0
  32. package/dist/stripe/StripeExpressButton.js.map +1 -0
  33. package/dist/stripe/StripeExpressCheckout.d.ts +98 -0
  34. package/dist/stripe/StripeExpressCheckout.d.ts.map +1 -0
  35. package/dist/stripe/StripeExpressCheckout.js +198 -0
  36. package/dist/stripe/StripeExpressCheckout.js.map +1 -0
  37. package/dist/stripe/index.d.ts +8 -0
  38. package/dist/stripe/index.d.ts.map +1 -0
  39. package/dist/stripe/index.js +15 -0
  40. package/dist/stripe/index.js.map +1 -0
  41. package/dist/stripe/useStripeExpress.d.ts +54 -0
  42. package/dist/stripe/useStripeExpress.d.ts.map +1 -0
  43. package/dist/stripe/useStripeExpress.js +126 -0
  44. package/dist/stripe/useStripeExpress.js.map +1 -0
  45. package/dist/tagada-headless-react.min.js +2 -2
  46. package/dist/tagada-headless-react.min.js.map +3 -3
  47. package/dist/tagada-headless.js +483 -66
  48. package/dist/tagada-headless.min.js +2 -2
  49. package/dist/tagada-headless.min.js.map +3 -3
  50. package/package.json +9 -3
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @tagadapay/headless-sdk v1.3.2
2
+ * @tagadapay/headless-sdk v1.5.0
3
3
  * Headless checkout, payments, offers, and CRM
4
4
  * @license MIT
5
5
  */
@@ -123,8 +123,8 @@ var TagadaHeadlessBundle = (() => {
123
123
 
124
124
  // src/core/http.ts
125
125
  var BASE_URLS = {
126
- production: "https://app.tagadapay.com",
127
- development: "https://app.dev.tagadapay.com",
126
+ production: "https://api.tagada.io",
127
+ development: "https://api.tagada.dev",
128
128
  local: "http://localhost:3000"
129
129
  };
130
130
  var HttpClient = class {
@@ -241,20 +241,27 @@ var TagadaHeadlessBundle = (() => {
241
241
  *
242
242
  * @param checkoutToken The checkout token (from URL param `checkoutToken`).
243
243
  * @param sessionToken The JWT session token (from URL param `token`). Required for auth.
244
+ * @param opts.currency Optional presentment currency override (e.g. 'EUR').
245
+ * Only applied when the session has no currency set yet (first load).
246
+ * Once set, currency is locked to the session and this param is ignored.
244
247
  *
245
- * Both values come from the redirect URL returned by `createSession()`.
248
+ * Both `checkoutToken` and `sessionToken` come from the redirect URL returned by `createSession()`.
246
249
  * Example URL: `/checkout?checkoutToken=abc123&token=eyJ...`
247
250
  */
248
- async loadSession(checkoutToken, sessionToken) {
251
+ async loadSession(checkoutToken, sessionToken, opts) {
249
252
  var _a;
250
253
  if (sessionToken) {
251
254
  this.sessionToken = sessionToken;
252
255
  (_a = this.onTokenChange) == null ? void 0 : _a.call(this, sessionToken);
253
256
  }
257
+ const params = { skipAsyncWait: "false" };
258
+ if (opts == null ? void 0 : opts.currency) {
259
+ params.currency = opts.currency;
260
+ }
254
261
  const raw = await this.http.get(
255
262
  `/api/v1/checkout-sessions/${checkoutToken}/v2`,
256
263
  {
257
- params: { skipAsyncWait: "false" },
264
+ params,
258
265
  token: this.sessionToken
259
266
  }
260
267
  );
@@ -277,17 +284,18 @@ var TagadaHeadlessBundle = (() => {
277
284
  storeId: this.storeId,
278
285
  lineItems
279
286
  };
280
- if (opts.currency)
281
- body.currency = opts.currency;
282
287
  if (opts.returnUrl)
283
288
  body.returnUrl = opts.returnUrl;
289
+ const customerData = {};
290
+ if (opts.currency)
291
+ customerData.currency = opts.currency;
284
292
  if (opts.customerEmail) {
285
- body.customer = {
286
- email: opts.customerEmail,
287
- firstName: opts.customerFirstName,
288
- lastName: opts.customerLastName,
289
- currency: opts.currency
290
- };
293
+ customerData.email = opts.customerEmail;
294
+ customerData.firstName = opts.customerFirstName;
295
+ customerData.lastName = opts.customerLastName;
296
+ }
297
+ if (Object.keys(customerData).length > 0) {
298
+ body.customer = customerData;
291
299
  }
292
300
  const res = await this.http.post("/api/v1/checkout/session/init", body);
293
301
  const url = new URL(res.checkoutUrl);
@@ -302,7 +310,18 @@ var TagadaHeadlessBundle = (() => {
302
310
  customerId: res.customerId
303
311
  };
304
312
  }
305
- /** Replace the cart items for an existing session. */
313
+ /**
314
+ * Replace the cart items for an existing session.
315
+ *
316
+ * Returns a server-side mutation summary (counts of created/updated rows).
317
+ * It does NOT return the updated `CheckoutSession` — the underlying
318
+ * `POST /checkout-sessions/:id/line-items` endpoint never has and the
319
+ * previous typing was a lie that quietly assigned `{ success, updated, ... }`
320
+ * into a `CheckoutSession` slot in `useCheckout`.
321
+ *
322
+ * Call `loadSession()` after this if you need the refreshed session
323
+ * (the React `useCheckout` hook does this for you).
324
+ */
306
325
  async updateCart(sessionId, items) {
307
326
  return this.http.post(
308
327
  `/api/v1/checkout-sessions/${sessionId}/line-items`,
@@ -311,59 +330,117 @@ var TagadaHeadlessBundle = (() => {
311
330
  );
312
331
  }
313
332
  /**
314
- * Update customer + address information on the session.
315
- * Uses the combined endpoint with `partial: true` so missing fields are tolerated.
333
+ * Update customer identity (email, name, phone).
334
+ *
335
+ * Call this before `updateAddress()` — the SDK remembers the values and
336
+ * includes them automatically when addresses are submitted.
337
+ *
338
+ * If you prefer a single call, use `updateCustomerAndAddress()` instead.
316
339
  */
317
340
  async updateCustomer(sessionId, customer) {
341
+ if (customer.email)
342
+ this.customerEmail = customer.email;
343
+ if (customer.firstName)
344
+ this.customerFirstName = customer.firstName;
345
+ if (customer.lastName)
346
+ this.customerLastName = customer.lastName;
347
+ if (customer.phone)
348
+ this.customerPhone = customer.phone;
349
+ }
350
+ /**
351
+ * Update shipping and/or billing address on the session.
352
+ *
353
+ * Automatically includes customer name/phone stored from `updateCustomer()`.
354
+ * Uses the same atomic API call as the production checkout, so email,
355
+ * customer name, and addresses are all persisted together.
356
+ *
357
+ * **Shipped products**: pass `shippingAddress`. If `billingAddress` is omitted,
358
+ * the shipping address is automatically used as billing.
359
+ *
360
+ * **Digital products**: pass only `billingAddress` (no shipping needed).
361
+ *
362
+ * @example
363
+ * // Shipped product — billing = shipping automatically
364
+ * await checkout.updateAddress(sessionId, {
365
+ * shippingAddress: { line1: '10 Rue de la Paix', city: 'Paris', postalCode: '75002', country: 'FR' },
366
+ * });
367
+ *
368
+ * @example
369
+ * // Digital product — billing only
370
+ * await checkout.updateAddress(sessionId, {
371
+ * billingAddress: { country: 'FR' },
372
+ * });
373
+ */
374
+ async updateAddress(sessionId, addresses) {
375
+ const shipping = this.mapAddress(addresses.shippingAddress);
376
+ const billing = this.mapAddress(addresses.billingAddress);
377
+ const effectiveShipping = shipping ?? billing ?? {};
378
+ const effectiveBilling = billing ?? shipping;
379
+ const differentBilling = !!(addresses.billingAddress && addresses.shippingAddress);
318
380
  await this.http.post(
319
381
  `/api/v1/checkout-sessions/${sessionId}/customer-and-session-info`,
320
382
  {
321
383
  checkoutSessionId: sessionId,
322
- partial: true,
323
384
  customerData: {
324
- email: customer.email
325
- },
326
- shippingAddress: {
327
- firstName: customer.firstName,
328
- lastName: customer.lastName,
329
- phone: customer.phone
385
+ email: this.customerEmail
330
386
  },
331
- billingAddress: {},
332
- differentBillingAddress: false
387
+ shippingAddress: effectiveShipping,
388
+ billingAddress: effectiveBilling ?? {},
389
+ differentBillingAddress: differentBilling
333
390
  },
334
391
  { token: this.sessionToken }
335
392
  );
336
393
  }
337
394
  /**
338
- * Update shipping and/or billing address on the session.
395
+ * Set customer info + addresses in one call (mirrors production checkout).
339
396
  *
340
- * Address fields use the API's naming: `address1`, `postal`, etc.
397
+ * Equivalent to calling `updateCustomer()` then `updateAddress()`, but
398
+ * in a single network request.
341
399
  */
342
- async updateAddress(sessionId, addresses) {
343
- const mapAddress = (a) => {
344
- if (!a)
345
- return void 0;
346
- return {
347
- address1: a.line1,
348
- address2: a.line2,
349
- city: a.city,
350
- state: a.state,
351
- postal: a.postalCode,
352
- country: a.country
353
- };
354
- };
400
+ async updateCustomerAndAddress(sessionId, data) {
401
+ if (data.customer.email)
402
+ this.customerEmail = data.customer.email;
403
+ if (data.customer.firstName)
404
+ this.customerFirstName = data.customer.firstName;
405
+ if (data.customer.lastName)
406
+ this.customerLastName = data.customer.lastName;
407
+ if (data.customer.phone)
408
+ this.customerPhone = data.customer.phone;
409
+ const shipping = this.mapAddress(data.shippingAddress);
410
+ const billing = this.mapAddress(data.billingAddress);
411
+ const effectiveShipping = shipping ?? billing ?? {};
412
+ const effectiveBilling = billing ?? shipping;
413
+ const differentBilling = !!(data.billingAddress && data.shippingAddress);
355
414
  await this.http.post(
356
- `/api/v1/checkout-sessions/${sessionId}/address`,
415
+ `/api/v1/checkout-sessions/${sessionId}/customer-and-session-info`,
357
416
  {
358
417
  checkoutSessionId: sessionId,
359
- data: {
360
- shippingAddress: mapAddress(addresses.shippingAddress) ?? {},
361
- billingAddress: mapAddress(addresses.billingAddress)
362
- }
418
+ customerData: {
419
+ email: this.customerEmail
420
+ },
421
+ shippingAddress: effectiveShipping,
422
+ billingAddress: effectiveBilling ?? {},
423
+ differentBillingAddress: differentBilling
363
424
  },
364
425
  { token: this.sessionToken }
365
426
  );
366
427
  }
428
+ /** Map an SDK Address to the API shape, injecting stored customer fields. */
429
+ mapAddress(a) {
430
+ if (!a)
431
+ return void 0;
432
+ return stripEmpty({
433
+ firstName: a.firstName ?? this.customerFirstName,
434
+ lastName: a.lastName ?? this.customerLastName,
435
+ phone: a.phone ?? this.customerPhone,
436
+ address1: a.line1,
437
+ address2: a.line2,
438
+ city: a.city,
439
+ state: a.state,
440
+ postal: a.postalCode,
441
+ country: a.country
442
+ });
443
+ }
367
444
  /** Apply a promotion code to the session. */
368
445
  async applyPromoCode(sessionId, code) {
369
446
  await this.http.post(
@@ -380,7 +457,13 @@ var TagadaHeadlessBundle = (() => {
380
457
  { token: this.sessionToken }
381
458
  );
382
459
  }
383
- /** Fetch available shipping rates for the session's address. */
460
+ /**
461
+ * Fetch available shipping rates for the session's address.
462
+ * Only relevant for **shipped products** — digital products can skip this step.
463
+ *
464
+ * Requires a shipping address to be set first via `updateAddress()`.
465
+ * Returns an empty array if no rates are configured or address is missing.
466
+ */
384
467
  async getShippingRates(sessionId) {
385
468
  const res = await this.http.get(
386
469
  `/api/v1/checkout-sessions/${sessionId}/shipping-rates`,
@@ -388,7 +471,10 @@ var TagadaHeadlessBundle = (() => {
388
471
  );
389
472
  return res.rates ?? [];
390
473
  }
391
- /** Select a shipping rate for the session. */
474
+ /**
475
+ * Select a shipping rate for the session.
476
+ * Only relevant for **shipped products** — digital products can skip this step.
477
+ */
392
478
  async selectShippingRate(sessionId, rateId) {
393
479
  await this.http.post(
394
480
  `/api/v1/checkout-sessions/${sessionId}/shipping-rate`,
@@ -401,17 +487,20 @@ var TagadaHeadlessBundle = (() => {
401
487
  * The raw response has nested `checkoutSession` + `summary` -- we flatten it.
402
488
  */
403
489
  normalizeSession(raw) {
490
+ var _a;
404
491
  const cs = raw.checkoutSession;
405
492
  const orderSummaries = cs.orderSummaries ?? [];
406
493
  const summary = raw.summary ?? cs.summary ?? orderSummaries[0] ?? {};
407
494
  const lineItems = cs.sessionLineItems ?? [];
408
495
  const customer = cs.customer;
496
+ const sessionCurrency = (_a = summary.currency ?? cs.selectedPresentmentCurrency) == null ? void 0 : _a.toUpperCase();
409
497
  const items = lineItems.map((li) => {
410
498
  const price = li.price;
411
499
  const variant = (price == null ? void 0 : price.variant) ?? {};
412
500
  const product = variant.product ?? {};
413
501
  const currencyOpts = (price == null ? void 0 : price.currencyOptions) ?? {};
414
- const usd = currencyOpts.USD ?? currencyOpts[Object.keys(currencyOpts)[0]];
502
+ const priceEntry = (sessionCurrency ? currencyOpts[sessionCurrency] : void 0) ?? currencyOpts[Object.keys(currencyOpts)[0]];
503
+ const amount = (priceEntry == null ? void 0 : priceEntry.amount) ?? 0;
415
504
  return {
416
505
  id: li.id,
417
506
  variantId: li.variantId,
@@ -419,9 +508,9 @@ var TagadaHeadlessBundle = (() => {
419
508
  productName: product.name ?? "",
420
509
  variantName: variant.name ?? "",
421
510
  quantity: li.quantity ?? 1,
422
- unitAmount: (usd == null ? void 0 : usd.amount) ?? 0,
423
- totalAmount: ((usd == null ? void 0 : usd.amount) ?? 0) * (li.quantity ?? 1),
424
- currency: cs.selectedPresentmentCurrency ?? "USD",
511
+ unitAmount: amount,
512
+ totalAmount: amount * (li.quantity ?? 1),
513
+ currency: sessionCurrency ?? "USD",
425
514
  imageUrl: variant.imageUrl ?? ""
426
515
  };
427
516
  });
@@ -430,7 +519,7 @@ var TagadaHeadlessBundle = (() => {
430
519
  id: cs.id,
431
520
  storeId: cs.storeId,
432
521
  checkoutToken: cs.checkoutToken,
433
- currency: cs.selectedPresentmentCurrency ?? "USD",
522
+ currency: sessionCurrency ?? "USD",
434
523
  items,
435
524
  totals: {
436
525
  subtotal: summary.subtotalAmount ?? 0,
@@ -438,20 +527,55 @@ var TagadaHeadlessBundle = (() => {
438
527
  shipping: summary.shippingCost ?? 0,
439
528
  tax: summary.totalTaxAmount ?? 0,
440
529
  total: summary.totalAmount ?? summaryItems.reduce((s, i) => s + (i.amount ?? 0), 0),
441
- currency: summary.currency ?? cs.selectedPresentmentCurrency ?? "USD"
530
+ currency: sessionCurrency ?? "USD"
442
531
  },
443
532
  customer: customer ? {
444
533
  id: customer.id,
445
534
  email: customer.email,
446
535
  firstName: customer.firstName,
447
- lastName: customer.lastName
536
+ lastName: customer.lastName,
537
+ phone: customer.phone
448
538
  } : void 0,
449
- shippingAddress: cs.shippingAddress,
450
- billingAddress: cs.billingAddress,
539
+ shippingAddress: this.normalizeAddress(cs.shippingAddress),
540
+ billingAddress: this.normalizeAddress(cs.billingAddress),
541
+ selectedShippingRateId: cs.shippingRateId ?? null,
451
542
  metadata: cs.metadata
452
543
  };
453
544
  }
545
+ /**
546
+ * Map the raw API address shape (`address1`, `address2`, `postal`) onto
547
+ * the SDK's `Address` shape (`line1`, `line2`, `postalCode`). Without
548
+ * this, `session.shippingAddress.line1` is silently undefined after a
549
+ * reload — the type was lying about the actual key names — which is
550
+ * what makes the demo lose form state on refresh.
551
+ */
552
+ normalizeAddress(raw) {
553
+ if (!raw || typeof raw !== "object")
554
+ return void 0;
555
+ const a = raw;
556
+ const out = {
557
+ firstName: a.firstName,
558
+ lastName: a.lastName,
559
+ line1: a.line1 ?? a.address1,
560
+ line2: a.line2 ?? a.address2,
561
+ city: a.city,
562
+ state: a.state,
563
+ postalCode: a.postalCode ?? a.postal,
564
+ country: a.country,
565
+ phone: a.phone
566
+ };
567
+ return Object.values(out).some((v) => v !== void 0 && v !== "") ? out : void 0;
568
+ }
454
569
  };
570
+ function stripEmpty(obj) {
571
+ const result = {};
572
+ for (const [k, v] of Object.entries(obj)) {
573
+ if (v !== void 0 && v !== null && v !== "") {
574
+ result[k] = v;
575
+ }
576
+ }
577
+ return Object.keys(result).length > 0 ? result : void 0;
578
+ }
455
579
 
456
580
  // src/core/modules/payment.ts
457
581
  var PaymentModule = class {
@@ -467,8 +591,20 @@ var TagadaHeadlessBundle = (() => {
467
591
  * Get the full payment setup config for a checkout session.
468
592
  * Returns a map of payment methods with their configuration
469
593
  * (card, apple_pay, google_pay, klarna, etc.).
594
+ *
595
+ * Priority chain:
596
+ * 1. Explicit `paymentSetupConfig` parameter (developer override)
597
+ * 2. `window.__TGD_STEP_CONFIG__` injection (Tagada routing infra)
598
+ * 3. API fallback (`GET /api/v1/payment-methods`)
470
599
  */
471
- async getPaymentSetup(checkoutSessionId) {
600
+ async getPaymentSetup(checkoutSessionId, opts) {
601
+ if (opts == null ? void 0 : opts.paymentSetupConfig) {
602
+ return opts.paymentSetupConfig;
603
+ }
604
+ const injected = this.readInjectedStepConfig();
605
+ if (injected) {
606
+ return injected;
607
+ }
472
608
  const methods = await this.http.get(
473
609
  "/api/v1/payment-methods",
474
610
  { params: { checkoutSessionId } }
@@ -484,12 +620,39 @@ var TagadaHeadlessBundle = (() => {
484
620
  * Get express payment methods (Apple Pay, Google Pay, PayPal, etc.)
485
621
  * filtered to only those available in the current browser.
486
622
  */
487
- async getExpressMethods(checkoutSessionId) {
488
- var _a, _b;
489
- const setup = await this.getPaymentSetup(checkoutSessionId);
623
+ async getExpressMethods(checkoutSessionId, opts) {
624
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
625
+ const setup = await this.getPaymentSetup(checkoutSessionId, opts);
490
626
  const result = {};
491
- for (const [, config] of Object.entries(setup)) {
492
- if (!config.enabled || !config.express)
627
+ for (const [key, config] of Object.entries(setup)) {
628
+ if (!config.enabled)
629
+ continue;
630
+ if (key.startsWith("express_checkout:") && config.methods) {
631
+ const base2 = {
632
+ available: true,
633
+ processorId: config.processorId,
634
+ publishableKey: config.publishableKey,
635
+ metadata: config.metadata
636
+ };
637
+ if ((_a = config.methods.apple_pay) == null ? void 0 : _a.enabled) {
638
+ const hasApplePay = typeof window !== "undefined" && "ApplePaySession" in window && ((_c = (_b = window.ApplePaySession) == null ? void 0 : _b.canMakePayments) == null ? void 0 : _c.call(_b));
639
+ result.applePay = { ...base2, type: "apple_pay", available: !!hasApplePay };
640
+ }
641
+ if ((_d = config.methods.google_pay) == null ? void 0 : _d.enabled) {
642
+ result.googlePay = { ...base2, type: "google_pay" };
643
+ }
644
+ if ((_e = config.methods.paypal) == null ? void 0 : _e.enabled) {
645
+ result.paypal = { ...base2, type: "paypal" };
646
+ }
647
+ if ((_f = config.methods.link) == null ? void 0 : _f.enabled) {
648
+ result.link = { ...base2, type: "link" };
649
+ }
650
+ if ((_g = config.methods.klarna_express) == null ? void 0 : _g.enabled) {
651
+ result.klarna = { ...base2, type: "klarna_express" };
652
+ }
653
+ continue;
654
+ }
655
+ if (!config.express)
493
656
  continue;
494
657
  const method = config.method;
495
658
  const base = {
@@ -500,7 +663,7 @@ var TagadaHeadlessBundle = (() => {
500
663
  metadata: config.metadata
501
664
  };
502
665
  if (method === "apple_pay" || config.type === "apple_pay") {
503
- const hasApplePay = typeof window !== "undefined" && "ApplePaySession" in window && ((_b = (_a = window.ApplePaySession) == null ? void 0 : _a.canMakePayments) == null ? void 0 : _b.call(_a));
666
+ const hasApplePay = typeof window !== "undefined" && "ApplePaySession" in window && ((_i = (_h = window.ApplePaySession) == null ? void 0 : _h.canMakePayments) == null ? void 0 : _i.call(_h));
504
667
  result.applePay = { ...base, available: !!hasApplePay };
505
668
  } else if (method === "google_pay" || config.type === "google_pay") {
506
669
  result.googlePay = base;
@@ -527,8 +690,8 @@ var TagadaHeadlessBundle = (() => {
527
690
  /**
528
691
  * Convenience: return only enabled payment method keys.
529
692
  */
530
- async getEnabledMethods(checkoutSessionId) {
531
- const setup = await this.getPaymentSetup(checkoutSessionId);
693
+ async getEnabledMethods(checkoutSessionId, opts) {
694
+ const setup = await this.getPaymentSetup(checkoutSessionId, opts);
532
695
  return Object.entries(setup).filter(([, c]) => c.enabled).map(([key]) => key);
533
696
  }
534
697
  // ---------------------------------------------------------------------------
@@ -538,6 +701,14 @@ var TagadaHeadlessBundle = (() => {
538
701
  * Tokenize a card using @tagadapay/core-js.
539
702
  * Requires `@tagadapay/core-js` to be installed as a peer dependency.
540
703
  *
704
+ * Card data is sent directly to Basis Theory for tokenization and never
705
+ * reaches TagadaPay servers. The returned `tagadaToken` is safe to pass
706
+ * to the TagadaPay API.
707
+ *
708
+ * **PCI note:** Using this method means card data passes through your
709
+ * JavaScript environment. You are responsible for PCI DSS SAQ-A-EP
710
+ * compliance. See https://docs.tagada.io/developer-tools/headless-sdk/pci-compliance
711
+ *
541
712
  * @returns tagadaToken (base64 string) and rawToken with SCA metadata.
542
713
  */
543
714
  async tokenizeCard(card) {
@@ -598,6 +769,7 @@ var TagadaHeadlessBundle = (() => {
598
769
  return this.http.post("/api/v1/payment/pay-with-token", {
599
770
  tagadaToken: opts.tagadaToken,
600
771
  checkoutSessionId: opts.checkoutSessionId,
772
+ returnUrl: opts.returnUrl,
601
773
  paymentFlowId: opts.paymentFlowId,
602
774
  processorId: opts.processorId,
603
775
  paymentMethod: opts.paymentMethod,
@@ -681,6 +853,85 @@ var TagadaHeadlessBundle = (() => {
681
853
  paymentFlowId: opts.paymentFlowId
682
854
  });
683
855
  }
856
+ /**
857
+ * Initiate a Stripe Express Checkout payment (Apple Pay, Google Pay, Link, etc.).
858
+ *
859
+ * Calls `pay-v2` with `isExpress: true`, which returns a `clientSecret` in
860
+ * `payment.requireActionData.metadata.stripeExpressCheckout.clientSecret`.
861
+ * The caller must then call `stripe.confirmPayment()` with that secret.
862
+ *
863
+ * Used by the `StripeExpressCheckout` class and `useStripeExpress` hook.
864
+ * Not typically called directly — use the Stripe integration classes instead.
865
+ */
866
+ async processStripeExpress(opts) {
867
+ return this.payDirect({
868
+ checkoutSessionId: opts.checkoutSessionId,
869
+ processorId: opts.processorId,
870
+ paymentMethod: opts.paymentMethod,
871
+ isExpress: true
872
+ });
873
+ }
874
+ // ---------------------------------------------------------------------------
875
+ // High-level orchestrator — the ONE function beginners should call
876
+ // ---------------------------------------------------------------------------
877
+ /**
878
+ * Process a payment end-to-end: pay, handle 3DS / redirects, poll until done.
879
+ *
880
+ * This is the **recommended** way to process payments. It wraps `pay()`, detects
881
+ * `requireAction`, runs 3DS challenges (if `@tagadapay/core-js` is installed),
882
+ * and polls until a terminal status.
883
+ *
884
+ * Returns a discriminated union so you can pattern-match on `result.status`:
885
+ *
886
+ * @example
887
+ * ```ts
888
+ * const result = await payment.processPayment({
889
+ * checkoutSessionId: session.id,
890
+ * tagadaToken,
891
+ * });
892
+ *
893
+ * switch (result.status) {
894
+ * case 'succeeded':
895
+ * console.log('Paid!', result.payment.id);
896
+ * break;
897
+ * case 'requires_redirect':
898
+ * // In browser: window.location.href = result.redirectUrl
899
+ * // The React hook handles this automatically.
900
+ * break;
901
+ * case 'failed':
902
+ * console.error(result.error);
903
+ * break;
904
+ * }
905
+ * ```
906
+ */
907
+ async processPayment(opts) {
908
+ var _a, _b;
909
+ if (!opts.returnUrl && typeof window !== "undefined") {
910
+ opts = { ...opts, returnUrl: window.location.href };
911
+ }
912
+ const payResult = await this.pay(opts);
913
+ const payment = payResult.payment;
914
+ if (payment.status === "succeeded") {
915
+ return { status: "succeeded", payment, order: payResult.order };
916
+ }
917
+ if (payment.status === "failed" || payment.status === "declined") {
918
+ const msg = ((_a = payment.requireActionData) == null ? void 0 : _a.message) || ((_b = payment.requireActionData) == null ? void 0 : _b.errorCode) || `Payment ${payment.status}`;
919
+ return { status: "failed", error: msg, payment };
920
+ }
921
+ if (payment.requireAction && payment.requireAction !== "none" && payment.requireActionData) {
922
+ return this.handlePaymentAction(payment, opts);
923
+ }
924
+ return this.pollUntilDone(payment.id);
925
+ }
926
+ /**
927
+ * Resume a payment after the user returns from a redirect (3DS, processor auth, APM).
928
+ *
929
+ * Called automatically by `usePayment()` in React when it detects return-URL params.
930
+ * You can also call it manually in non-React apps.
931
+ */
932
+ async resumeAfterRedirect(paymentId) {
933
+ return this.pollUntilDone(paymentId);
934
+ }
684
935
  // ---------------------------------------------------------------------------
685
936
  // Status polling
686
937
  // ---------------------------------------------------------------------------
@@ -711,9 +962,175 @@ var TagadaHeadlessBundle = (() => {
711
962
  paymentId
712
963
  });
713
964
  }
965
+ /** Tell the server this action has been seen / is being processed. */
966
+ async markActionProcessed(paymentId) {
967
+ try {
968
+ await this.http.post("/api/v1/payments/require-action/processed", {
969
+ paymentId
970
+ });
971
+ } catch {
972
+ }
973
+ }
974
+ // ---------------------------------------------------------------------------
975
+ // Internal: action routing
976
+ // ---------------------------------------------------------------------------
977
+ /**
978
+ * Route a payment action to the appropriate handler.
979
+ * Returns `requires_redirect` for browser-level redirects,
980
+ * or attempts 3DS inline and polls to completion.
981
+ */
982
+ async handlePaymentAction(payment, opts) {
983
+ const actionData = payment.requireActionData;
984
+ await this.markActionProcessed(payment.id);
985
+ const actionType = actionData.type;
986
+ switch (actionType) {
987
+ case "threeds_auth":
988
+ return this.handleThreedsAuth(payment, opts);
989
+ case "processor_auth":
990
+ case "redirect_to_payment":
991
+ return this.extractRedirect(payment, actionData);
992
+ case "redirect":
993
+ return this.extractRedirect(payment, actionData);
994
+ case "error":
995
+ return {
996
+ status: "failed",
997
+ error: actionData.message || actionData.errorCode || "Payment failed",
998
+ payment
999
+ };
1000
+ default:
1001
+ return this.extractRedirect(payment, actionData);
1002
+ }
1003
+ }
1004
+ /**
1005
+ * Extract redirect info from actionData.
1006
+ * Works for processor_auth (GET or POST), redirect, and any unknown type with a URL.
1007
+ */
1008
+ extractRedirect(payment, actionData) {
1009
+ var _a, _b;
1010
+ const redirect = (_a = actionData.metadata) == null ? void 0 : _a.redirect;
1011
+ const url = (redirect == null ? void 0 : redirect.redirectUrl) || actionData.redirectUrl;
1012
+ if (!url) {
1013
+ return {
1014
+ status: "failed",
1015
+ error: `Payment requires action (${actionData.type}) but no redirect URL was provided`,
1016
+ payment
1017
+ };
1018
+ }
1019
+ return {
1020
+ status: "requires_redirect",
1021
+ redirectUrl: url,
1022
+ method: ((_b = redirect == null ? void 0 : redirect.method) == null ? void 0 : _b.toUpperCase()) || "GET",
1023
+ postData: redirect == null ? void 0 : redirect.data,
1024
+ paymentId: payment.id
1025
+ };
1026
+ }
1027
+ /**
1028
+ * Run a 3DS challenge using @tagadapay/core-js.
1029
+ * If core-js is not installed, falls back to redirect.
1030
+ */
1031
+ async handleThreedsAuth(payment, opts) {
1032
+ var _a, _b;
1033
+ const session = (_b = (_a = payment.requireActionData) == null ? void 0 : _a.metadata) == null ? void 0 : _b.threedsSession;
1034
+ if (!(session == null ? void 0 : session.externalSessionId)) {
1035
+ return this.extractRedirect(payment, payment.requireActionData);
1036
+ }
1037
+ let coreJs;
1038
+ try {
1039
+ coreJs = await import("@tagadapay/core-js");
1040
+ } catch {
1041
+ if (session.acsChallengeUrl) {
1042
+ return {
1043
+ status: "requires_redirect",
1044
+ redirectUrl: session.acsChallengeUrl,
1045
+ paymentId: payment.id
1046
+ };
1047
+ }
1048
+ return this.extractRedirect(payment, payment.requireActionData);
1049
+ }
1050
+ try {
1051
+ const manager = new coreJs.ThreedsManager({ provider: "basis_theory" });
1052
+ await manager.initialize({ apiKey: void 0 });
1053
+ await manager.startChallenge({
1054
+ sessionId: session.externalSessionId,
1055
+ acsChallengeUrl: session.acsChallengeUrl,
1056
+ acsTransactionId: session.acsTransID,
1057
+ threeDSVersion: session.messageVersion
1058
+ });
1059
+ await this.completeAfter3ds(payment.id);
1060
+ return this.pollUntilDone(payment.id);
1061
+ } catch {
1062
+ if (session.acsChallengeUrl) {
1063
+ return {
1064
+ status: "requires_redirect",
1065
+ redirectUrl: session.acsChallengeUrl,
1066
+ paymentId: payment.id
1067
+ };
1068
+ }
1069
+ return {
1070
+ status: "failed",
1071
+ error: "3D Secure authentication failed",
1072
+ payment
1073
+ };
1074
+ }
1075
+ }
1076
+ /**
1077
+ * Poll until the payment reaches a terminal status, then return a ProcessPaymentResult.
1078
+ * Also handles new requireAction that may appear during polling.
1079
+ */
1080
+ async pollUntilDone(paymentId, opts) {
1081
+ const interval = (opts == null ? void 0 : opts.intervalMs) ?? 1500;
1082
+ const timeout = (opts == null ? void 0 : opts.timeoutMs) ?? 6e4;
1083
+ const start = Date.now();
1084
+ let consecutiveErrors = 0;
1085
+ while (Date.now() - start < timeout) {
1086
+ let status;
1087
+ try {
1088
+ status = await this.getPaymentStatus(paymentId);
1089
+ consecutiveErrors = 0;
1090
+ } catch {
1091
+ consecutiveErrors++;
1092
+ if (consecutiveErrors >= 3) {
1093
+ return { status: "failed", error: "Payment verification failed due to network errors" };
1094
+ }
1095
+ await new Promise((r) => setTimeout(r, interval));
1096
+ continue;
1097
+ }
1098
+ if (status.status === "succeeded" || status.status === "pending" && status.subStatus === "authorized") {
1099
+ return { status: "succeeded", payment: status, order: status.order };
1100
+ }
1101
+ if (status.status === "failed" || status.status === "declined") {
1102
+ return { status: "failed", error: status.status, payment: status };
1103
+ }
1104
+ if (status.requireAction && status.requireAction !== "none" && status.requireActionData && !status.requireActionData.processed) {
1105
+ return this.handlePaymentAction(
1106
+ status,
1107
+ {}
1108
+ );
1109
+ }
1110
+ await new Promise((r) => setTimeout(r, interval));
1111
+ }
1112
+ return { status: "pending", paymentId };
1113
+ }
714
1114
  // ---------------------------------------------------------------------------
715
1115
  // Internal helpers
716
1116
  // ---------------------------------------------------------------------------
1117
+ /**
1118
+ * Read paymentSetupConfig from the Tagada routing injection.
1119
+ * Only available when running on Tagada infra (not on external hosting like Vercel).
1120
+ */
1121
+ readInjectedStepConfig() {
1122
+ if (typeof window === "undefined")
1123
+ return null;
1124
+ const raw = window.__TGD_STEP_CONFIG__;
1125
+ if (!raw)
1126
+ return null;
1127
+ try {
1128
+ const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
1129
+ return (parsed == null ? void 0 : parsed.paymentSetupConfig) ?? null;
1130
+ } catch {
1131
+ return null;
1132
+ }
1133
+ }
717
1134
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
718
1135
  async loadCoreJs() {
719
1136
  try {