@tagadapay/headless-sdk 1.8.0-rc.0 → 1.9.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 (141) hide show
  1. package/README.md +216 -1
  2. package/dist/core/client.d.ts +68 -0
  3. package/dist/core/client.d.ts.map +1 -0
  4. package/dist/core/client.js +73 -0
  5. package/dist/core/client.js.map +1 -0
  6. package/dist/core/errors.d.ts +53 -0
  7. package/dist/core/errors.d.ts.map +1 -0
  8. package/dist/core/errors.js +58 -0
  9. package/dist/core/errors.js.map +1 -0
  10. package/dist/core/helpers/autocomplete.d.ts +89 -0
  11. package/dist/core/helpers/autocomplete.d.ts.map +1 -0
  12. package/dist/core/helpers/autocomplete.js +186 -0
  13. package/dist/core/helpers/autocomplete.js.map +1 -0
  14. package/dist/core/helpers/countries.d.ts +45 -0
  15. package/dist/core/helpers/countries.d.ts.map +1 -0
  16. package/dist/core/helpers/countries.js +65 -0
  17. package/dist/core/helpers/countries.js.map +1 -0
  18. package/dist/core/helpers/currency.d.ts +26 -0
  19. package/dist/core/helpers/currency.d.ts.map +1 -0
  20. package/dist/core/helpers/currency.js +72 -0
  21. package/dist/core/helpers/currency.js.map +1 -0
  22. package/dist/core/helpers/index.d.ts +6 -0
  23. package/dist/core/helpers/index.d.ts.map +1 -0
  24. package/dist/core/helpers/index.js +6 -0
  25. package/dist/core/helpers/index.js.map +1 -0
  26. package/dist/core/helpers/iso3166.d.ts +28 -0
  27. package/dist/core/helpers/iso3166.d.ts.map +1 -0
  28. package/dist/core/helpers/iso3166.js +84 -0
  29. package/dist/core/helpers/iso3166.js.map +1 -0
  30. package/dist/core/helpers/walletContact.d.ts +67 -0
  31. package/dist/core/helpers/walletContact.d.ts.map +1 -0
  32. package/dist/core/helpers/walletContact.js +71 -0
  33. package/dist/core/helpers/walletContact.js.map +1 -0
  34. package/dist/core/http.d.ts +27 -0
  35. package/dist/core/http.d.ts.map +1 -0
  36. package/dist/core/http.js +125 -0
  37. package/dist/core/http.js.map +1 -0
  38. package/dist/core/index.d.ts +15 -0
  39. package/dist/core/index.d.ts.map +1 -0
  40. package/dist/core/index.js +13 -0
  41. package/dist/core/index.js.map +1 -0
  42. package/dist/core/modules/catalog.d.ts +27 -0
  43. package/dist/core/modules/catalog.d.ts.map +1 -0
  44. package/dist/core/modules/catalog.js +38 -0
  45. package/dist/core/modules/catalog.js.map +1 -0
  46. package/dist/core/modules/checkout.d.ts +268 -0
  47. package/dist/core/modules/checkout.d.ts.map +1 -0
  48. package/dist/core/modules/checkout.js +470 -0
  49. package/dist/core/modules/checkout.js.map +1 -0
  50. package/dist/core/modules/customer.d.ts +33 -0
  51. package/dist/core/modules/customer.d.ts.map +1 -0
  52. package/dist/core/modules/customer.js +53 -0
  53. package/dist/core/modules/customer.js.map +1 -0
  54. package/dist/core/modules/funnel.d.ts +38 -0
  55. package/dist/core/modules/funnel.d.ts.map +1 -0
  56. package/dist/core/modules/funnel.js +143 -0
  57. package/dist/core/modules/funnel.js.map +1 -0
  58. package/dist/core/modules/offers.d.ts +137 -0
  59. package/dist/core/modules/offers.d.ts.map +1 -0
  60. package/dist/core/modules/offers.js +141 -0
  61. package/dist/core/modules/offers.js.map +1 -0
  62. package/dist/core/modules/payment.d.ts +353 -0
  63. package/dist/core/modules/payment.d.ts.map +1 -0
  64. package/dist/core/modules/payment.js +784 -0
  65. package/dist/core/modules/payment.js.map +1 -0
  66. package/dist/core/modules/rx.d.ts +162 -0
  67. package/dist/core/modules/rx.d.ts.map +1 -0
  68. package/dist/core/modules/rx.js +46 -0
  69. package/dist/core/modules/rx.js.map +1 -0
  70. package/dist/core/types.d.ts +532 -0
  71. package/dist/core/types.d.ts.map +1 -0
  72. package/dist/core/types.js +5 -0
  73. package/dist/core/types.js.map +1 -0
  74. package/dist/index.d.ts.map +1 -0
  75. package/dist/index.js.map +1 -0
  76. package/dist/react/hooks/useCatalog.d.ts +25 -0
  77. package/dist/react/hooks/useCatalog.d.ts.map +1 -0
  78. package/dist/react/hooks/useCatalog.js +58 -0
  79. package/dist/react/hooks/useCatalog.js.map +1 -0
  80. package/dist/react/hooks/useCheckout.d.ts +58 -0
  81. package/dist/react/hooks/useCheckout.d.ts.map +1 -0
  82. package/dist/react/hooks/useCheckout.js +177 -0
  83. package/dist/react/hooks/useCheckout.js.map +1 -0
  84. package/dist/react/hooks/useCustomer.d.ts +28 -0
  85. package/dist/react/hooks/useCustomer.d.ts.map +1 -0
  86. package/dist/react/hooks/useCustomer.js +77 -0
  87. package/dist/react/hooks/useCustomer.js.map +1 -0
  88. package/dist/react/hooks/useGoogleAutocomplete.d.ts +26 -0
  89. package/dist/react/hooks/useGoogleAutocomplete.d.ts.map +1 -0
  90. package/dist/react/hooks/useGoogleAutocomplete.js +70 -0
  91. package/dist/react/hooks/useGoogleAutocomplete.js.map +1 -0
  92. package/dist/react/hooks/useISOData.d.ts +23 -0
  93. package/dist/react/hooks/useISOData.d.ts.map +1 -0
  94. package/dist/react/hooks/useISOData.js +63 -0
  95. package/dist/react/hooks/useISOData.js.map +1 -0
  96. package/dist/react/hooks/useOffers.d.ts +134 -0
  97. package/dist/react/hooks/useOffers.d.ts.map +1 -0
  98. package/dist/react/hooks/useOffers.js +171 -0
  99. package/dist/react/hooks/useOffers.js.map +1 -0
  100. package/dist/react/hooks/usePayment.d.ts +117 -0
  101. package/dist/react/hooks/usePayment.d.ts.map +1 -0
  102. package/dist/react/hooks/usePayment.js +233 -0
  103. package/dist/react/hooks/usePayment.js.map +1 -0
  104. package/dist/react/hooks/useRx.d.ts +64 -0
  105. package/dist/react/hooks/useRx.d.ts.map +1 -0
  106. package/dist/react/hooks/useRx.js +145 -0
  107. package/dist/react/hooks/useRx.js.map +1 -0
  108. package/dist/react/index.d.ts +22 -0
  109. package/dist/react/index.d.ts.map +1 -0
  110. package/dist/react/index.js +12 -0
  111. package/dist/react/index.js.map +1 -0
  112. package/dist/react/internal/redirectGuard.d.ts +9 -0
  113. package/dist/react/internal/redirectGuard.d.ts.map +1 -0
  114. package/dist/react/internal/redirectGuard.js +25 -0
  115. package/dist/react/internal/redirectGuard.js.map +1 -0
  116. package/dist/react/provider.d.ts +31 -0
  117. package/dist/react/provider.d.ts.map +1 -0
  118. package/dist/react/provider.js +32 -0
  119. package/dist/react/provider.js.map +1 -0
  120. package/dist/stripe/StripeExpressButton.d.ts +30 -0
  121. package/dist/stripe/StripeExpressButton.d.ts.map +1 -0
  122. package/dist/stripe/StripeExpressButton.js +32 -0
  123. package/dist/stripe/StripeExpressButton.js.map +1 -0
  124. package/dist/stripe/StripeExpressCheckout.d.ts +109 -0
  125. package/dist/stripe/StripeExpressCheckout.d.ts.map +1 -0
  126. package/dist/stripe/StripeExpressCheckout.js +242 -0
  127. package/dist/stripe/StripeExpressCheckout.js.map +1 -0
  128. package/dist/stripe/index.d.ts +8 -0
  129. package/dist/stripe/index.d.ts.map +1 -0
  130. package/dist/stripe/index.js +15 -0
  131. package/dist/stripe/index.js.map +1 -0
  132. package/dist/stripe/useStripeExpress.d.ts +54 -0
  133. package/dist/stripe/useStripeExpress.d.ts.map +1 -0
  134. package/dist/stripe/useStripeExpress.js +125 -0
  135. package/dist/stripe/useStripeExpress.js.map +1 -0
  136. package/dist/tagada-headless-react.min.js +8 -0
  137. package/dist/tagada-headless-react.min.js.map +7 -0
  138. package/dist/tagada-headless.js +2423 -0
  139. package/dist/tagada-headless.min.js +8 -0
  140. package/dist/tagada-headless.min.js.map +7 -0
  141. package/package.json +79 -73
@@ -0,0 +1,784 @@
1
+ import { applePayContactToAddress, googlePayContactToAddress, } from '../helpers/walletContact.js';
2
+ /**
3
+ * Full payment lifecycle -- discover APMs, tokenize, create instruments,
4
+ * process payments, handle 3DS, and poll for status.
5
+ *
6
+ * Wraps the same REST endpoints that plugin-sdk uses internally.
7
+ *
8
+ * @remarks PCI DSS compliance notice
9
+ *
10
+ * This SDK collects card data (PAN, expiry, CVC) in the merchant's page
11
+ * context and sends it directly to Basis Theory for tokenization. Card data
12
+ * never reaches TagadaPay servers.
13
+ *
14
+ * Because card data transits through the merchant's JavaScript environment,
15
+ * merchants using this SDK are responsible for **PCI DSS SAQ-A-EP**
16
+ * compliance. This includes quarterly ASV scans, secure page delivery
17
+ * (TLS), and ensuring no unauthorized scripts can access cardholder data.
18
+ *
19
+ * TagadaPay is PCI DSS Level 1 certified. The merchant's PCI scope is
20
+ * determined by their integration method, not by TagadaPay's certification.
21
+ *
22
+ * See: https://docs.tagada.io/developer-tools/headless-sdk/pci-compliance
23
+ */
24
+ export class PaymentModule {
25
+ constructor(http, storeId, environment,
26
+ /** Used to persist wallet contacts onto the session before charging. */
27
+ checkout) {
28
+ this.http = http;
29
+ this.storeId = storeId;
30
+ this.environment = environment;
31
+ this.checkout = checkout;
32
+ }
33
+ // ---------------------------------------------------------------------------
34
+ // Discovery: what payment methods are available?
35
+ // ---------------------------------------------------------------------------
36
+ /**
37
+ * Get the full payment setup config for a checkout session.
38
+ * Returns a map of payment methods with their configuration
39
+ * (card, apple_pay, google_pay, klarna, etc.).
40
+ *
41
+ * Priority chain:
42
+ * 1. Explicit `paymentSetupConfig` parameter (developer override)
43
+ * 2. `window.__TGD_STEP_CONFIG__` injection (Tagada routing infra)
44
+ * 3. API fallback (`GET /api/v1/payment-methods`)
45
+ */
46
+ async getPaymentSetup(checkoutSessionId, opts) {
47
+ if (opts?.paymentSetupConfig) {
48
+ return opts.paymentSetupConfig;
49
+ }
50
+ const injected = this.readInjectedStepConfig();
51
+ if (injected) {
52
+ return injected;
53
+ }
54
+ const methods = await this.http.get('/api/v1/payment-methods', { params: { checkoutSessionId } });
55
+ const config = {};
56
+ for (const m of methods.methods ?? []) {
57
+ const key = m.provider ? `${m.method}:${m.provider}` : m.method;
58
+ config[key] = m;
59
+ }
60
+ return config;
61
+ }
62
+ /**
63
+ * Get express payment methods (Apple Pay, Google Pay, PayPal, etc.)
64
+ * filtered to only those available in the current browser.
65
+ */
66
+ async getExpressMethods(checkoutSessionId, opts) {
67
+ const setup = await this.getPaymentSetup(checkoutSessionId, opts);
68
+ const result = {};
69
+ for (const [key, config] of Object.entries(setup)) {
70
+ if (!config.enabled)
71
+ continue;
72
+ // Handle express_checkout:processorId entries (from paymentSetupConfig injection)
73
+ // These have a `methods` map like { apple_pay: { enabled: true }, google_pay: { enabled: true } }
74
+ if (key.startsWith('express_checkout:') && config.methods) {
75
+ const base = {
76
+ available: true,
77
+ processorId: config.processorId,
78
+ publishableKey: config.publishableKey,
79
+ metadata: config.metadata,
80
+ };
81
+ if (config.methods.apple_pay?.enabled) {
82
+ const hasApplePay = typeof window !== 'undefined' &&
83
+ 'ApplePaySession' in window &&
84
+ window
85
+ .ApplePaySession?.canMakePayments?.();
86
+ result.applePay = { ...base, type: 'apple_pay', available: !!hasApplePay };
87
+ }
88
+ if (config.methods.google_pay?.enabled) {
89
+ result.googlePay = { ...base, type: 'google_pay' };
90
+ }
91
+ if (config.methods.link?.enabled) {
92
+ result.link = { ...base, type: 'link' };
93
+ }
94
+ if (config.methods.klarna_express?.enabled) {
95
+ result.klarna = { ...base, type: 'klarna_express' };
96
+ }
97
+ continue;
98
+ }
99
+ if (!config.express)
100
+ continue;
101
+ const method = config.method;
102
+ const base = {
103
+ type: method,
104
+ available: true,
105
+ processorId: config.processorId,
106
+ publishableKey: config.publishableKey,
107
+ metadata: config.metadata,
108
+ };
109
+ if (method === 'apple_pay' || config.type === 'apple_pay') {
110
+ const hasApplePay = typeof window !== 'undefined' &&
111
+ 'ApplePaySession' in window &&
112
+ window
113
+ .ApplePaySession?.canMakePayments?.();
114
+ result.applePay = { ...base, available: !!hasApplePay };
115
+ }
116
+ else if (method === 'google_pay' || config.type === 'google_pay') {
117
+ result.googlePay = base;
118
+ }
119
+ else if (method === 'paypal') {
120
+ result.paypal = base;
121
+ }
122
+ else if (method === 'klarna' || method === 'klarna_express') {
123
+ result.klarna = base;
124
+ }
125
+ else if (method === 'link') {
126
+ result.link = base;
127
+ }
128
+ }
129
+ return result;
130
+ }
131
+ /**
132
+ * Get store-level payment config (3DS flags, default flow ID, etc.).
133
+ */
134
+ async getStoreConfig() {
135
+ const res = await this.http.get('/api/v1/checkout/config', { params: { storeId: this.storeId } });
136
+ return res.config ?? res;
137
+ }
138
+ /**
139
+ * Convenience: return only enabled payment method keys.
140
+ */
141
+ async getEnabledMethods(checkoutSessionId, opts) {
142
+ const setup = await this.getPaymentSetup(checkoutSessionId, opts);
143
+ return Object.entries(setup)
144
+ .filter(([, c]) => c.enabled)
145
+ .map(([key]) => key);
146
+ }
147
+ // ---------------------------------------------------------------------------
148
+ // Tokenization (delegates to @tagadapay/core-js if available)
149
+ // ---------------------------------------------------------------------------
150
+ /**
151
+ * Tokenize a card using @tagadapay/core-js.
152
+ * Requires `@tagadapay/core-js` to be installed as a peer dependency.
153
+ *
154
+ * Card data is sent directly to Basis Theory for tokenization and never
155
+ * reaches TagadaPay servers. The returned `tagadaToken` is safe to pass
156
+ * to the TagadaPay API.
157
+ *
158
+ * **PCI note:** Using this method means card data passes through your
159
+ * JavaScript environment. You are responsible for PCI DSS SAQ-A-EP
160
+ * compliance. See https://docs.tagada.io/developer-tools/headless-sdk/pci-compliance
161
+ *
162
+ * @returns tagadaToken (base64 string) and rawToken with SCA metadata.
163
+ */
164
+ async tokenizeCard(card) {
165
+ const coreJs = await this.loadCoreJs();
166
+ const tokenizer = new coreJs.Tokenizer({ environment: this.environment });
167
+ await tokenizer.initialize();
168
+ const sanitized = {
169
+ cardNumber: card.cardNumber,
170
+ expiryDate: card.expiryDate,
171
+ cvc: card.cvc,
172
+ };
173
+ if (card.cardholderName)
174
+ sanitized.cardholderName = card.cardholderName;
175
+ // Tokenize ONCE — both `tokenizer.tokenizeCard` and
176
+ // `tokenizer.tokenizeCardRaw` internally call BasisTheory's
177
+ // `POST /tokens`. Running them in `Promise.all` charged the
178
+ // customer's network with two identical tokenization requests
179
+ // (visible in the browser as duplicated `tokens/` calls). Get
180
+ // the raw response once, then derive the encoded TagadaToken
181
+ // locally via `createTagadaToken`.
182
+ const rawToken = await tokenizer.tokenizeCardRaw(sanitized);
183
+ const tagadaToken = coreJs.createTagadaToken(rawToken, tokenizer.getProvider().getProviderName());
184
+ return { tagadaToken, rawToken };
185
+ }
186
+ // ---------------------------------------------------------------------------
187
+ // Instrument creation
188
+ // ---------------------------------------------------------------------------
189
+ /**
190
+ * Create a reusable payment instrument from a TagadaToken.
191
+ * Session-authenticated (uses the checkout session JWT).
192
+ */
193
+ async createInstrument(opts) {
194
+ return this.http.post('/api/v1/payment/create-instrument-from-token', {
195
+ tagadaToken: opts.tagadaToken,
196
+ });
197
+ }
198
+ // ---------------------------------------------------------------------------
199
+ // 3DS
200
+ // ---------------------------------------------------------------------------
201
+ /**
202
+ * Create a 3DS session on the TagadaPay backend.
203
+ * This persists the session so the processor can use it during payment.
204
+ */
205
+ async create3dsSession(opts) {
206
+ return this.http.post('/api/public/v1/threeds/create-session', {
207
+ provider: opts.provider ?? 'basis_theory',
208
+ storeId: this.storeId,
209
+ paymentInstrumentId: opts.paymentInstrumentId,
210
+ sessionData: opts.sessionData,
211
+ });
212
+ }
213
+ // ---------------------------------------------------------------------------
214
+ // Payment processing
215
+ // ---------------------------------------------------------------------------
216
+ /**
217
+ * High-level pay: single API call that decodes tagadaToken server-side,
218
+ * creates the payment instrument, and processes payment.
219
+ *
220
+ * Also accepts `paymentInstrumentId` directly for pre-created instruments.
221
+ */
222
+ async pay(opts) {
223
+ if (opts.tagadaToken && !opts.paymentInstrumentId) {
224
+ return this.http.post('/api/v1/payment/pay-with-token', {
225
+ tagadaToken: opts.tagadaToken,
226
+ checkoutSessionId: opts.checkoutSessionId,
227
+ returnUrl: opts.returnUrl,
228
+ paymentFlowId: opts.paymentFlowId,
229
+ processorId: opts.processorId,
230
+ paymentMethod: opts.paymentMethod,
231
+ isExpress: opts.isExpress,
232
+ metadata: opts.metadata,
233
+ });
234
+ }
235
+ return this.http.post('/api/public/v1/checkout/pay-v2', {
236
+ checkoutSessionId: opts.checkoutSessionId,
237
+ paymentInstrumentId: opts.paymentInstrumentId,
238
+ storeId: this.storeId,
239
+ paymentFlowId: opts.paymentFlowId,
240
+ threedsSessionId: opts.threedsSessionId,
241
+ processorId: opts.processorId,
242
+ paymentMethod: opts.paymentMethod,
243
+ isExpress: opts.isExpress,
244
+ metadata: opts.metadata,
245
+ returnUrl: opts.returnUrl,
246
+ });
247
+ }
248
+ /**
249
+ * Low-level pay: process payment with an existing instrument or for APM/express flows
250
+ * where no instrument is needed (redirect-based methods).
251
+ */
252
+ async payDirect(opts) {
253
+ return this.http.post('/api/public/v1/checkout/pay-v2', {
254
+ checkoutSessionId: opts.checkoutSessionId,
255
+ paymentInstrumentId: opts.paymentInstrumentId ?? '',
256
+ storeId: this.storeId,
257
+ paymentFlowId: opts.paymentFlowId,
258
+ processorId: opts.processorId,
259
+ paymentMethod: opts.paymentMethod,
260
+ isExpress: opts.isExpress,
261
+ threedsSessionId: opts.threedsSessionId,
262
+ returnUrl: opts.returnUrl,
263
+ });
264
+ }
265
+ /**
266
+ * Persist a wallet contact onto the session before the charge.
267
+ *
268
+ * Express wallets are the one checkout where the buyer never fills an address
269
+ * form: they approve one inside the sheet, and it comes back on the
270
+ * authorization event. Studio's `ExpressPaymentWidget` writes it to the
271
+ * session; the headless path used to drop it, which is how a year of orders
272
+ * reached merchants with NULL shipping, billing and customer addresses
273
+ * (#TAG-1989).
274
+ *
275
+ * Never invents data: a contact with no street line or no country maps to
276
+ * `undefined` and the session is left exactly as it was. Failures are NOT
277
+ * swallowed — if we hold an address and cannot store it, the charge must not
278
+ * proceed, or we are back to the bug this fixes.
279
+ */
280
+ async syncWalletContact(checkoutSessionId, contact) {
281
+ const { shippingAddress, billingAddress } = contact;
282
+ if (!shippingAddress && !billingAddress)
283
+ return;
284
+ if (!this.checkout) {
285
+ throw new Error('PaymentModule cannot persist the wallet contact: no CheckoutModule was provided. ' +
286
+ 'Build the SDK through createTagadaClient() rather than constructing PaymentModule directly.');
287
+ }
288
+ const customer = {};
289
+ if (contact.email)
290
+ customer.email = contact.email;
291
+ if (contact.phone)
292
+ customer.phone = contact.phone;
293
+ if (contact.firstName)
294
+ customer.firstName = contact.firstName;
295
+ if (contact.lastName)
296
+ customer.lastName = contact.lastName;
297
+ await this.checkout.updateCustomerAndAddress(checkoutSessionId, {
298
+ customer,
299
+ shippingAddress,
300
+ billingAddress,
301
+ });
302
+ }
303
+ /**
304
+ * Process an Apple Pay payment.
305
+ * Tokenizes the Apple Pay token and processes payment in one call.
306
+ *
307
+ * Pass the contacts from `onpaymentauthorized` — `event.payment.billingContact`
308
+ * and `event.payment.shippingContact` — and they are written to the session
309
+ * before the charge, so the address the buyer approved in the wallet sheet
310
+ * lands on the order (#TAG-1989).
311
+ *
312
+ * Apple only returns them when the payment request asked for them, so the
313
+ * `ApplePaySession` request needs:
314
+ *
315
+ * ```js
316
+ * requiredBillingContactFields: ['postalAddress'],
317
+ * requiredShippingContactFields: ['name', 'phone', 'email', 'postalAddress'],
318
+ * ```
319
+ *
320
+ * @example
321
+ * session.onpaymentauthorized = async (event) => {
322
+ * await tagada.payment.processApplePay({
323
+ * checkoutSessionId,
324
+ * applePayToken: event.payment.token,
325
+ * billingContact: event.payment.billingContact,
326
+ * shippingContact: event.payment.shippingContact,
327
+ * });
328
+ * };
329
+ */
330
+ async processApplePay(opts) {
331
+ await this.syncWalletContact(opts.checkoutSessionId, {
332
+ shippingAddress: applePayContactToAddress(opts.shippingContact),
333
+ billingAddress: applePayContactToAddress(opts.billingContact),
334
+ email: opts.shippingContact?.emailAddress ?? opts.billingContact?.emailAddress,
335
+ phone: opts.shippingContact?.phoneNumber ?? opts.billingContact?.phoneNumber,
336
+ firstName: opts.shippingContact?.givenName ?? opts.billingContact?.givenName,
337
+ lastName: opts.shippingContact?.familyName ?? opts.billingContact?.familyName,
338
+ });
339
+ const coreJs = await this.loadCoreJs();
340
+ const tokenizer = new coreJs.Tokenizer({ environment: this.environment });
341
+ await tokenizer.initialize();
342
+ const { tagadaToken } = await tokenizer.tokenizeApplePay(opts.applePayToken);
343
+ return this.pay({
344
+ checkoutSessionId: opts.checkoutSessionId,
345
+ tagadaToken,
346
+ paymentFlowId: opts.paymentFlowId,
347
+ processorId: opts.processorId,
348
+ paymentMethod: 'apple_pay',
349
+ isExpress: true,
350
+ });
351
+ }
352
+ /**
353
+ * Process a Google Pay payment.
354
+ * Tokenizes the Google Pay token and processes payment in one call.
355
+ *
356
+ * Pass `paymentData.shippingAddress` / the billing address off
357
+ * `paymentMethodData.info.billingAddress` and they are persisted to the
358
+ * session before the charge (#TAG-1989). Google only returns them when the
359
+ * request sets `shippingAddressRequired` / `billingAddressRequired`.
360
+ */
361
+ async processGooglePay(opts) {
362
+ await this.syncWalletContact(opts.checkoutSessionId, {
363
+ shippingAddress: googlePayContactToAddress(opts.shippingAddress),
364
+ billingAddress: googlePayContactToAddress(opts.billingAddress),
365
+ email: opts.email ?? opts.shippingAddress?.emailAddress ?? opts.billingAddress?.emailAddress,
366
+ phone: opts.shippingAddress?.phoneNumber ?? opts.billingAddress?.phoneNumber,
367
+ });
368
+ const coreJs = await this.loadCoreJs();
369
+ const tokenizer = new coreJs.Tokenizer({ environment: this.environment });
370
+ await tokenizer.initialize();
371
+ const { tagadaToken } = await tokenizer.tokenizeGooglePay(opts.googlePayToken);
372
+ return this.pay({
373
+ checkoutSessionId: opts.checkoutSessionId,
374
+ tagadaToken,
375
+ paymentFlowId: opts.paymentFlowId,
376
+ processorId: opts.processorId,
377
+ paymentMethod: 'google_pay',
378
+ isExpress: true,
379
+ });
380
+ }
381
+ /**
382
+ * Process an APM payment (Klarna, iDEAL, etc.) via redirect.
383
+ * These typically don't require a payment instrument -- the processor handles the redirect.
384
+ */
385
+ async processApm(opts) {
386
+ return this.payDirect({
387
+ checkoutSessionId: opts.checkoutSessionId,
388
+ paymentMethod: opts.paymentMethod,
389
+ processorId: opts.processorId,
390
+ paymentFlowId: opts.paymentFlowId,
391
+ });
392
+ }
393
+ /**
394
+ * Initiate a Stripe Express Checkout payment (Apple Pay, Google Pay, Link, etc.).
395
+ *
396
+ * Calls `pay-v2` with `isExpress: true`, which returns a `clientSecret` in
397
+ * `payment.requireActionData.metadata.stripeExpressCheckout.clientSecret`.
398
+ * The caller must then call `stripe.confirmPayment()` with that secret.
399
+ *
400
+ * Used by the `StripeExpressCheckout` class and `useStripeExpress` hook.
401
+ * Not typically called directly — use the Stripe integration classes instead.
402
+ */
403
+ async processStripeExpress(opts) {
404
+ return this.payDirect({
405
+ checkoutSessionId: opts.checkoutSessionId,
406
+ processorId: opts.processorId,
407
+ paymentMethod: opts.paymentMethod,
408
+ isExpress: true,
409
+ });
410
+ }
411
+ // ---------------------------------------------------------------------------
412
+ // High-level orchestrator — the ONE function beginners should call
413
+ // ---------------------------------------------------------------------------
414
+ /**
415
+ * Process a payment end-to-end: pay, handle 3DS / redirects, poll until done.
416
+ *
417
+ * This is the **recommended** way to process payments. It wraps `pay()`, detects
418
+ * `requireAction`, runs 3DS challenges (if `@tagadapay/core-js` is installed),
419
+ * and polls until a terminal status.
420
+ *
421
+ * Returns a discriminated union so you can pattern-match on `result.status`:
422
+ *
423
+ * @example
424
+ * ```ts
425
+ * const result = await payment.processPayment({
426
+ * checkoutSessionId: session.id,
427
+ * tagadaToken,
428
+ * });
429
+ *
430
+ * switch (result.status) {
431
+ * case 'succeeded':
432
+ * console.log('Paid!', result.payment.id);
433
+ * break;
434
+ * case 'requires_redirect':
435
+ * // In browser: window.location.href = result.redirectUrl
436
+ * // The React hook handles this automatically.
437
+ * break;
438
+ * case 'failed':
439
+ * console.error(result.error);
440
+ * break;
441
+ * }
442
+ * ```
443
+ */
444
+ async processPayment(opts) {
445
+ if (!opts.returnUrl && typeof window !== 'undefined') {
446
+ opts = { ...opts, returnUrl: window.location.href };
447
+ }
448
+ const payResult = await this.pay(opts);
449
+ return this.finalize(payResult, { returnUrl: opts.returnUrl });
450
+ }
451
+ /**
452
+ * Resolve a raw `PayResult` (from any pay endpoint — `pay()`, `payDirect()`,
453
+ * `processApplePay()`, `offers.payPreviewedOffer()`, …) into a typed
454
+ * `ProcessPaymentResult` (`succeeded` / `requires_redirect` / `failed`
455
+ * / `pending`).
456
+ *
457
+ * Handles 3DS routing (modal challenge via core-js, or bank redirect)
458
+ * and async polling until the payment reaches a terminal status.
459
+ *
460
+ * This is the same logic `processPayment()` runs after `pay()` — exposed
461
+ * so callers (and the offers module) that already have a raw `PayResult`
462
+ * don't need to re-pay.
463
+ */
464
+ async finalize(input, opts) {
465
+ const payment = input.payment;
466
+ const returnUrl = opts?.returnUrl ?? (typeof window !== 'undefined' ? window.location.href : undefined);
467
+ if (payment.status === 'succeeded') {
468
+ return { status: 'succeeded', payment, order: input.order };
469
+ }
470
+ if (payment.status === 'failed' || payment.status === 'declined') {
471
+ const msg = payment.requireActionData?.message
472
+ || payment.requireActionData?.errorCode
473
+ || `Payment ${payment.status}`;
474
+ return { status: 'failed', error: msg, payment };
475
+ }
476
+ if (payment.requireAction && payment.requireAction !== 'none' && payment.requireActionData) {
477
+ return this.handlePaymentAction(payment, { returnUrl });
478
+ }
479
+ // 'pending' or 'requires_action' without explicit actionData — poll
480
+ return this.pollUntilDone(payment.id);
481
+ }
482
+ /**
483
+ * Resume a payment after the user returns from a redirect (3DS, processor auth, APM).
484
+ *
485
+ * Called automatically by `usePayment()` in React when it detects return-URL params.
486
+ * You can also call it manually in non-React apps.
487
+ */
488
+ async resumeAfterRedirect(paymentId) {
489
+ return this.pollUntilDone(paymentId);
490
+ }
491
+ /**
492
+ * Detect a return from a 3DS / processor redirect by inspecting the
493
+ * current page URL, then poll the payment to completion.
494
+ *
495
+ * The post-3DS URL convention used by Tagada-routed processors is
496
+ * `?paymentAction=requireAction&paymentActionStatus=completed&paymentId=pay_xxx`.
497
+ * If those params are present, this method polls the payment, cleans the
498
+ * URL (so a refresh doesn't re-trigger), and returns the resolved
499
+ * `ProcessPaymentResult`. Otherwise it returns `null`.
500
+ *
501
+ * Use this in vanilla apps as the first thing on page load. The React
502
+ * `usePayment` hook does the same automatically — don't call this when
503
+ * also using `usePayment`, you'll double-poll.
504
+ *
505
+ * @example
506
+ * ```ts
507
+ * const tagada = createHeadlessClient({ storeId, environment });
508
+ *
509
+ * const resumed = await tagada.payment.maybeResumeFromUrl();
510
+ * if (resumed?.status === 'succeeded') {
511
+ * window.location.href = `/thank-you?orderId=${resumed.order?.id}`;
512
+ * return;
513
+ * }
514
+ * if (resumed?.status === 'failed') {
515
+ * showError(resumed.error);
516
+ * return;
517
+ * }
518
+ * // No 3DS return — render the normal page.
519
+ * ```
520
+ */
521
+ async maybeResumeFromUrl(opts) {
522
+ if (typeof window === 'undefined' && !opts?.url)
523
+ return null;
524
+ const href = opts?.url ?? window.location.href;
525
+ let url;
526
+ try {
527
+ url = new URL(href);
528
+ }
529
+ catch {
530
+ return null;
531
+ }
532
+ const action = url.searchParams.get('paymentAction');
533
+ const actionStatus = url.searchParams.get('paymentActionStatus');
534
+ const paymentId = url.searchParams.get('paymentId');
535
+ if (action !== 'requireAction' || actionStatus !== 'completed' || !paymentId) {
536
+ return null;
537
+ }
538
+ const result = await this.resumeAfterRedirect(paymentId);
539
+ // Strip the redirect params so a refresh doesn't re-trigger.
540
+ if (opts?.cleanUrl !== false && typeof window !== 'undefined' && !opts?.url) {
541
+ const cleanParams = new URLSearchParams(window.location.search);
542
+ [
543
+ 'paymentAction',
544
+ 'paymentActionStatus',
545
+ 'paymentId',
546
+ 'payment_intent',
547
+ 'payment_intent_client_secret',
548
+ 'source_type',
549
+ 'redirect_status',
550
+ 'sessionId',
551
+ ].forEach((k) => cleanParams.delete(k));
552
+ const search = cleanParams.toString();
553
+ try {
554
+ window.history.replaceState({}, document.title, window.location.pathname + (search ? '?' + search : ''));
555
+ }
556
+ catch {
557
+ // Best-effort.
558
+ }
559
+ }
560
+ return result;
561
+ }
562
+ // ---------------------------------------------------------------------------
563
+ // Status polling
564
+ // ---------------------------------------------------------------------------
565
+ /** Get current status of a payment. */
566
+ async getPaymentStatus(paymentId) {
567
+ return this.http.get(`/api/v1/payments/${paymentId}`);
568
+ }
569
+ /**
570
+ * Poll a payment until it reaches a terminal status.
571
+ * Returns the final status or throws on timeout.
572
+ */
573
+ async pollPaymentStatus(paymentId, opts) {
574
+ const interval = opts?.intervalMs ?? 1500;
575
+ const timeout = opts?.timeoutMs ?? 60000;
576
+ const start = Date.now();
577
+ while (Date.now() - start < timeout) {
578
+ const status = await this.getPaymentStatus(paymentId);
579
+ if (status.status === 'succeeded' || status.status === 'failed' || status.status === 'declined') {
580
+ return status;
581
+ }
582
+ await new Promise((r) => setTimeout(r, interval));
583
+ }
584
+ throw new Error(`Payment ${paymentId} did not reach terminal status within ${timeout}ms.`);
585
+ }
586
+ /** Finalize a payment after a 3DS challenge completes. */
587
+ async completeAfter3ds(paymentId) {
588
+ return this.http.post('/api/v1/payments/complete-after-3ds', {
589
+ paymentId,
590
+ });
591
+ }
592
+ /** Tell the server this action has been seen / is being processed. */
593
+ async markActionProcessed(paymentId) {
594
+ try {
595
+ await this.http.post('/api/v1/payments/require-action/processed', {
596
+ paymentId,
597
+ });
598
+ }
599
+ catch {
600
+ // Best-effort — don't block the payment flow if this fails
601
+ }
602
+ }
603
+ // ---------------------------------------------------------------------------
604
+ // Internal: action routing
605
+ // ---------------------------------------------------------------------------
606
+ /**
607
+ * Route a payment action to the appropriate handler.
608
+ * Returns `requires_redirect` for browser-level redirects,
609
+ * or attempts 3DS inline and polls to completion.
610
+ */
611
+ async handlePaymentAction(payment, opts) {
612
+ const actionData = payment.requireActionData;
613
+ await this.markActionProcessed(payment.id);
614
+ const actionType = actionData.type;
615
+ switch (actionType) {
616
+ case 'threeds_auth':
617
+ return this.handleThreedsAuth(payment, opts);
618
+ case 'processor_auth':
619
+ case 'redirect_to_payment':
620
+ return this.extractRedirect(payment, actionData);
621
+ case 'redirect':
622
+ return this.extractRedirect(payment, actionData);
623
+ case 'error':
624
+ return {
625
+ status: 'failed',
626
+ error: actionData.message || actionData.errorCode || 'Payment failed',
627
+ payment,
628
+ };
629
+ default:
630
+ // Unknown action type — try to extract a redirect URL (forward-compatible)
631
+ return this.extractRedirect(payment, actionData);
632
+ }
633
+ }
634
+ /**
635
+ * Extract redirect info from actionData.
636
+ * Works for processor_auth (GET or POST), redirect, and any unknown type with a URL.
637
+ */
638
+ extractRedirect(payment, actionData) {
639
+ const redirect = actionData.metadata?.redirect;
640
+ const url = redirect?.redirectUrl || actionData.redirectUrl;
641
+ if (!url) {
642
+ return {
643
+ status: 'failed',
644
+ error: `Payment requires action (${actionData.type}) but no redirect URL was provided`,
645
+ payment,
646
+ };
647
+ }
648
+ return {
649
+ status: 'requires_redirect',
650
+ redirectUrl: url,
651
+ method: redirect?.method?.toUpperCase() || 'GET',
652
+ postData: redirect?.data,
653
+ paymentId: payment.id,
654
+ };
655
+ }
656
+ /**
657
+ * Run a 3DS challenge using @tagadapay/core-js.
658
+ * If core-js is not installed, falls back to redirect.
659
+ */
660
+ async handleThreedsAuth(payment, opts) {
661
+ const session = payment.requireActionData?.metadata?.threedsSession;
662
+ // If no 3DS session data, fall back to redirect (acsChallengeUrl)
663
+ if (!session?.externalSessionId) {
664
+ return this.extractRedirect(payment, payment.requireActionData);
665
+ }
666
+ // Try to run the challenge inline via core-js
667
+ let coreJs; // eslint-disable-line @typescript-eslint/no-explicit-any
668
+ try {
669
+ coreJs = await import('@tagadapay/core-js');
670
+ }
671
+ catch {
672
+ // core-js not installed — fall back to redirect if possible
673
+ if (session.acsChallengeUrl) {
674
+ return {
675
+ status: 'requires_redirect',
676
+ redirectUrl: session.acsChallengeUrl,
677
+ paymentId: payment.id,
678
+ };
679
+ }
680
+ return this.extractRedirect(payment, payment.requireActionData);
681
+ }
682
+ // Run BasisTheory 3DS challenge in a modal iframe
683
+ try {
684
+ const manager = new coreJs.ThreedsManager({ provider: 'basis_theory' });
685
+ await manager.initialize({ apiKey: undefined });
686
+ await manager.startChallenge({
687
+ sessionId: session.externalSessionId,
688
+ acsChallengeUrl: session.acsChallengeUrl,
689
+ acsTransactionId: session.acsTransID,
690
+ threeDSVersion: session.messageVersion,
691
+ });
692
+ // Challenge done — tell backend and poll
693
+ await this.completeAfter3ds(payment.id);
694
+ return this.pollUntilDone(payment.id);
695
+ }
696
+ catch {
697
+ // 3DS challenge failed — fall back to redirect
698
+ if (session.acsChallengeUrl) {
699
+ return {
700
+ status: 'requires_redirect',
701
+ redirectUrl: session.acsChallengeUrl,
702
+ paymentId: payment.id,
703
+ };
704
+ }
705
+ return {
706
+ status: 'failed',
707
+ error: '3D Secure authentication failed',
708
+ payment,
709
+ };
710
+ }
711
+ }
712
+ /**
713
+ * Poll until the payment reaches a terminal status, then return a ProcessPaymentResult.
714
+ * Also handles new requireAction that may appear during polling.
715
+ */
716
+ async pollUntilDone(paymentId, opts) {
717
+ const interval = opts?.intervalMs ?? 1500;
718
+ const timeout = opts?.timeoutMs ?? 60000;
719
+ const start = Date.now();
720
+ let consecutiveErrors = 0;
721
+ while (Date.now() - start < timeout) {
722
+ let status;
723
+ try {
724
+ status = await this.getPaymentStatus(paymentId);
725
+ consecutiveErrors = 0;
726
+ }
727
+ catch {
728
+ consecutiveErrors++;
729
+ if (consecutiveErrors >= 3) {
730
+ return { status: 'failed', error: 'Payment verification failed due to network errors' };
731
+ }
732
+ await new Promise((r) => setTimeout(r, interval));
733
+ continue;
734
+ }
735
+ if (status.status === 'succeeded' ||
736
+ (status.status === 'pending' && status.subStatus === 'authorized')) {
737
+ return { status: 'succeeded', payment: status, order: status.order };
738
+ }
739
+ if (status.status === 'failed' || status.status === 'declined') {
740
+ return { status: 'failed', error: status.status, payment: status };
741
+ }
742
+ // New unprocessed action appeared during polling
743
+ if (status.requireAction &&
744
+ status.requireAction !== 'none' &&
745
+ status.requireActionData &&
746
+ !status.requireActionData.processed) {
747
+ return this.handlePaymentAction(status, {});
748
+ }
749
+ await new Promise((r) => setTimeout(r, interval));
750
+ }
751
+ return { status: 'pending', paymentId };
752
+ }
753
+ // ---------------------------------------------------------------------------
754
+ // Internal helpers
755
+ // ---------------------------------------------------------------------------
756
+ /**
757
+ * Read paymentSetupConfig from the Tagada routing injection.
758
+ * Only available when running on Tagada infra (not on external hosting like Vercel).
759
+ */
760
+ readInjectedStepConfig() {
761
+ if (typeof window === 'undefined')
762
+ return null;
763
+ const raw = window.__TGD_STEP_CONFIG__;
764
+ if (!raw)
765
+ return null;
766
+ try {
767
+ const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
768
+ return parsed?.paymentSetupConfig ?? null;
769
+ }
770
+ catch {
771
+ return null;
772
+ }
773
+ }
774
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
775
+ async loadCoreJs() {
776
+ try {
777
+ return await import('@tagadapay/core-js');
778
+ }
779
+ catch {
780
+ throw new Error('This method requires @tagadapay/core-js. Install it: npm install @tagadapay/core-js');
781
+ }
782
+ }
783
+ }
784
+ //# sourceMappingURL=payment.js.map