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