@tagadapay/plugin-sdk 4.0.0 → 4.0.4

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 (65) hide show
  1. package/README.md +1129 -1129
  2. package/build-cdn.js +499 -499
  3. package/dist/external-tracker.js +156 -2
  4. package/dist/external-tracker.min.js +2 -2
  5. package/dist/external-tracker.min.js.map +4 -4
  6. package/dist/react/providers/TagadaProvider.js +5 -5
  7. package/dist/tagada-react-sdk-minimal.min.js +2 -2
  8. package/dist/tagada-react-sdk-minimal.min.js.map +4 -4
  9. package/dist/tagada-react-sdk.js +707 -253
  10. package/dist/tagada-react-sdk.min.js +2 -2
  11. package/dist/tagada-react-sdk.min.js.map +4 -4
  12. package/dist/tagada-sdk.js +2922 -102
  13. package/dist/tagada-sdk.min.js +2 -2
  14. package/dist/tagada-sdk.min.js.map +4 -4
  15. package/dist/v2/core/funnelClient.d.ts +40 -0
  16. package/dist/v2/core/funnelClient.js +30 -0
  17. package/dist/v2/core/pixelTracker.d.ts +51 -0
  18. package/dist/v2/core/pixelTracker.js +425 -0
  19. package/dist/v2/core/resources/checkout.d.ts +45 -1
  20. package/dist/v2/core/resources/checkout.js +13 -3
  21. package/dist/v2/core/resources/offers.d.ts +3 -3
  22. package/dist/v2/core/resources/offers.js +11 -3
  23. package/dist/v2/core/resources/promotionEvents.d.ts +5 -0
  24. package/dist/v2/core/resources/promotionEvents.js +2 -0
  25. package/dist/v2/core/resources/promotions.d.ts +6 -1
  26. package/dist/v2/core/resources/promotions.js +6 -1
  27. package/dist/v2/core/resources/shippingRates.d.ts +18 -0
  28. package/dist/v2/core/resources/shippingRates.js +18 -0
  29. package/dist/v2/core/utils/clickIdResolver.d.ts +79 -0
  30. package/dist/v2/core/utils/clickIdResolver.js +169 -0
  31. package/dist/v2/core/utils/index.d.ts +2 -0
  32. package/dist/v2/core/utils/index.js +4 -0
  33. package/dist/v2/core/utils/metaEventId.d.ts +14 -0
  34. package/dist/v2/core/utils/metaEventId.js +16 -0
  35. package/dist/v2/core/utils/previewModeIndicator.js +101 -101
  36. package/dist/v2/index.d.ts +7 -0
  37. package/dist/v2/index.js +10 -0
  38. package/dist/v2/react/components/ApplePayButton.js +50 -0
  39. package/dist/v2/react/components/FunnelScriptInjector.js +9 -9
  40. package/dist/v2/react/components/GooglePayButton.js +39 -1
  41. package/dist/v2/react/components/StripeExpressButton.js +54 -2
  42. package/dist/v2/react/hooks/payment-actions/useNgeniusThreedsAction.js +11 -11
  43. package/dist/v2/react/hooks/useCheckoutQuery.js +41 -29
  44. package/dist/v2/react/hooks/useDiscountsQuery.js +4 -0
  45. package/dist/v2/react/hooks/useFunnel.d.ts +7 -0
  46. package/dist/v2/react/hooks/useFunnel.js +2 -1
  47. package/dist/v2/react/hooks/useOfferQuery.d.ts +11 -0
  48. package/dist/v2/react/hooks/useOfferQuery.js +11 -0
  49. package/dist/v2/react/hooks/usePixelTracking.d.ts +10 -5
  50. package/dist/v2/react/hooks/usePixelTracking.js +32 -374
  51. package/dist/v2/react/hooks/usePreviewOffer.d.ts +3 -1
  52. package/dist/v2/react/hooks/usePreviewOffer.js +4 -2
  53. package/dist/v2/react/hooks/usePromotionsQuery.js +9 -3
  54. package/dist/v2/react/hooks/useShippingRatesQuery.js +36 -21
  55. package/dist/v2/react/hooks/useStepConfig.d.ts +9 -0
  56. package/dist/v2/react/hooks/useStepConfig.js +5 -1
  57. package/dist/v2/react/index.d.ts +5 -0
  58. package/dist/v2/react/index.js +9 -0
  59. package/dist/v2/react/providers/TagadaProvider.js +18 -5
  60. package/dist/v2/standalone/apple-pay-service.d.ts +1 -1
  61. package/dist/v2/standalone/index.d.ts +3 -0
  62. package/dist/v2/standalone/index.js +23 -0
  63. package/dist/v2/standalone/payment-service.d.ts +54 -1
  64. package/dist/v2/standalone/payment-service.js +228 -61
  65. package/package.json +115 -115
@@ -221,13 +221,40 @@ export class PaymentService {
221
221
  });
222
222
  this.callbacks.onCurrentPaymentId?.(response.payment?.id || null);
223
223
  if (response.payment.requireAction !== 'none') {
224
- await this.handlePaymentAction(response.payment);
225
- return { success: true, payment: response.payment, order: response.order, redirecting: true };
224
+ // Pre-empt terminal failure surfaced through requireAction='error' (e.g.
225
+ // Stripe rejecting an APM that isn't enabled on the account) or status
226
+ // already declined/failed. Don't even invoke the action handler — for
227
+ // 'error' it would just fire onError, for declined+redirect it might
228
+ // navigate to a stale URL. Surface the failure so the button resets.
229
+ if (response.payment.requireAction === 'error'
230
+ || response.payment.status === 'declined'
231
+ || response.payment.status === 'failed') {
232
+ const msg = response.payment.requireActionData?.message
233
+ || response.payment.error?.message
234
+ || 'Payment declined';
235
+ this.callbacks.onError?.(msg);
236
+ this.callbacks.onProcessing?.(false);
237
+ return { success: false, error: msg, payment: response.payment, order: response.order };
238
+ }
239
+ // Run the action handler; propagate its outcome (covers radar declines,
240
+ // missing redirect URLs, etc. — not just the headline 'error' case).
241
+ const outcome = await this.handlePaymentAction(response.payment);
242
+ const settled = this.settleActionOutcome(outcome, response.payment, response.order);
243
+ if (settled)
244
+ return settled;
245
+ // 'pending' falls through to polling
226
246
  }
227
247
  if (response.payment.status === 'succeeded') {
228
248
  this.callbacks.onProcessing?.(false);
229
249
  return { success: true, payment: response.payment, order: response.order };
230
250
  }
251
+ if (response.payment.status === 'declined'
252
+ || response.payment.status === 'failed') {
253
+ const msg = response.payment.error?.message || 'Payment declined';
254
+ this.callbacks.onError?.(msg);
255
+ this.callbacks.onProcessing?.(false);
256
+ return { success: false, error: msg, payment: response.payment, order: response.order };
257
+ }
231
258
  return new Promise((resolve) => {
232
259
  this.startPolling(response.payment.id, {
233
260
  onSuccess: (payment) => {
@@ -239,14 +266,42 @@ export class PaymentService {
239
266
  this.callbacks.onProcessing?.(false);
240
267
  resolve({ success: false, error });
241
268
  },
242
- onRequireAction: (payment) => {
243
- void this.handlePaymentAction(payment);
269
+ onRequireAction: async (payment) => {
270
+ // Polling found a new requireAction mid-flight. Run the handler and
271
+ // resolve the outer promise with its outcome — otherwise the caller
272
+ // hangs forever waiting on a polling loop that already stopped.
273
+ const outcome = await this.handlePaymentAction(payment);
274
+ const settled = this.settleActionOutcome(outcome, payment, response.order);
275
+ if (settled) {
276
+ this.callbacks.onProcessing?.(false);
277
+ resolve(settled);
278
+ }
279
+ // 'pending' outcome means the action handler started its own polling;
280
+ // the outer promise stays open and will resolve via that path.
244
281
  },
245
282
  });
246
283
  });
247
284
  }
285
+ /**
286
+ * Translate an ActionOutcome into a PaymentResult, or null when the outcome
287
+ * is 'pending' (caller should keep polling instead of resolving).
288
+ */
289
+ settleActionOutcome(outcome, payment, order) {
290
+ switch (outcome.kind) {
291
+ case 'redirected':
292
+ return { success: true, payment, order, redirecting: true };
293
+ case 'completed':
294
+ return { success: true, payment: outcome.payment, order };
295
+ case 'failed':
296
+ return { success: false, error: outcome.error, payment: outcome.payment ?? payment, order };
297
+ case 'pending':
298
+ return null;
299
+ }
300
+ }
248
301
  /**
249
302
  * After radar / completePaymentAfterAction, handle the resumed payment.
303
+ * Fires callbacks for side effects AND returns an outcome so the caller
304
+ * (handlePaymentAction) can propagate failure into PaymentResult.
250
305
  */
251
306
  async handleResumedPayment(resumedPayment) {
252
307
  if (resumedPayment.status === 'declined' || resumedPayment.status === 'failed') {
@@ -256,18 +311,17 @@ export class PaymentService {
256
311
  this.callbacks.onError?.(errorMsg);
257
312
  this.callbacks.onProcessing?.(false);
258
313
  this.callbacks.onFailure?.(errorMsg);
259
- return;
314
+ return { kind: 'failed', error: errorMsg, payment: resumedPayment };
260
315
  }
261
316
  if (resumedPayment.status === 'succeeded') {
262
317
  this.callbacks.onProcessing?.(false);
263
318
  this.callbacks.onSuccess?.(resumedPayment);
264
- return;
319
+ return { kind: 'completed', payment: resumedPayment };
265
320
  }
266
321
  if (resumedPayment.requireAction !== 'none' && resumedPayment.requireActionData) {
267
- await this.handlePaymentAction(resumedPayment);
268
- return;
322
+ return this.handlePaymentAction(resumedPayment);
269
323
  }
270
- // Start polling for final status
324
+ // Start polling for final status — outcome will arrive via callbacks.
271
325
  this.startPolling(resumedPayment.id, {
272
326
  onSuccess: (p) => {
273
327
  this.callbacks.onProcessing?.(false);
@@ -279,18 +333,19 @@ export class PaymentService {
279
333
  },
280
334
  onRequireAction: (p) => { void this.handlePaymentAction(p); },
281
335
  });
336
+ return { kind: 'pending' };
282
337
  }
283
338
  // ==========================================================================
284
339
  // PAYMENT ACTION HANDLER (mirrors usePaymentActionHandler)
285
340
  // ==========================================================================
286
341
  async handlePaymentAction(payment) {
287
342
  if (payment.requireAction === 'none')
288
- return;
343
+ return { kind: 'pending' };
289
344
  if (payment.requireActionData?.processed)
290
- return;
345
+ return { kind: 'pending' };
291
346
  const actionData = payment.requireActionData;
292
347
  if (!actionData)
293
- return;
348
+ return { kind: 'pending' };
294
349
  try {
295
350
  await this.paymentsResource.markPaymentActionProcessed(payment.id);
296
351
  }
@@ -306,50 +361,55 @@ export class PaymentService {
306
361
  await this.callbacks.onBeforeRedirect(payment, redirectUrl);
307
362
  }
308
363
  window.location.href = redirectUrl;
364
+ return { kind: 'redirected' };
309
365
  }
310
- else if (payment.status === 'succeeded') {
366
+ if (payment.status === 'succeeded') {
311
367
  this.callbacks.onProcessing?.(false);
312
368
  this.callbacks.onSuccess?.(payment);
369
+ return { kind: 'completed', payment };
313
370
  }
314
- break;
371
+ // Action said redirect but no URL and payment not succeeded — broken
372
+ // response from backend. Surface as failure rather than hanging.
373
+ const noUrlMsg = 'Payment redirect URL missing';
374
+ this.callbacks.onError?.(noUrlMsg);
375
+ this.callbacks.onProcessing?.(false);
376
+ return { kind: 'failed', error: noUrlMsg, payment };
315
377
  }
316
378
  case 'threeds_auth': {
317
379
  const session = actionData.metadata?.threedsSession;
318
380
  if (session?.acsChallengeUrl) {
319
381
  console.log('[PaymentService] 3DS challenge redirect:', session.acsChallengeUrl);
320
382
  window.location.href = session.acsChallengeUrl;
383
+ return { kind: 'redirected' };
321
384
  }
322
- break;
385
+ const noUrlMsg = '3DS challenge URL missing';
386
+ this.callbacks.onError?.(noUrlMsg);
387
+ this.callbacks.onProcessing?.(false);
388
+ return { kind: 'failed', error: noUrlMsg, payment };
323
389
  }
324
390
  case 'error': {
325
391
  const msg = actionData.message || 'Payment action failed';
326
392
  this.callbacks.onError?.(msg);
327
393
  this.callbacks.onProcessing?.(false);
328
- break;
394
+ return { kind: 'failed', error: msg, payment };
329
395
  }
330
396
  case 'kesspay_auth':
331
- this.handleKessPayAuth(actionData);
332
- break;
397
+ return this.handleKessPayAuth(actionData);
333
398
  case 'trustflow_auth':
334
- this.handleTrustFlowAuth(actionData);
335
- break;
399
+ return this.handleTrustFlowAuth(actionData);
336
400
  case 'finix_radar':
337
- await this.handleFinixRadar(payment, actionData);
338
- break;
401
+ return this.handleFinixRadar(payment, actionData);
339
402
  case 'stripe_radar':
340
- await this.handleStripeRadar(payment, actionData);
341
- break;
403
+ return this.handleStripeRadar(payment, actionData);
342
404
  case 'radar':
343
405
  if (actionData.metadata?.provider === 'airwallex') {
344
- await this.handleAirwallexRadar(payment, actionData);
406
+ return this.handleAirwallexRadar(payment, actionData);
345
407
  }
346
- break;
408
+ return { kind: 'pending' };
347
409
  case 'mastercard_auth':
348
- await this.handleMasterCardAuth(payment, actionData);
349
- break;
410
+ return this.handleMasterCardAuth(payment, actionData);
350
411
  case 'ngenius_3ds':
351
- await this.handleNgeniusThreeds(payment, actionData);
352
- break;
412
+ return this.handleNgeniusThreeds(payment, actionData);
353
413
  default: {
354
414
  console.log('[PaymentService] Unhandled action, starting polling:', actionData.type);
355
415
  this.startPolling(payment.id, {
@@ -357,7 +417,7 @@ export class PaymentService {
357
417
  onFailure: (e) => { this.callbacks.onError?.(e); this.callbacks.onProcessing?.(false); },
358
418
  onRequireAction: (p) => { void this.handlePaymentAction(p); },
359
419
  });
360
- break;
420
+ return { kind: 'pending' };
361
421
  }
362
422
  }
363
423
  }
@@ -367,9 +427,10 @@ export class PaymentService {
367
427
  handleKessPayAuth(actionData) {
368
428
  const threeDSData = actionData?.metadata?.threeds;
369
429
  if (!threeDSData?.challengeHtml) {
370
- this.callbacks.onError?.('Missing KessPay 3DS challenge HTML');
430
+ const msg = 'Missing KessPay 3DS challenge HTML';
431
+ this.callbacks.onError?.(msg);
371
432
  this.callbacks.onProcessing?.(false);
372
- return;
433
+ return { kind: 'failed', error: msg };
373
434
  }
374
435
  try {
375
436
  this.callbacks.onProcessing?.(false);
@@ -396,10 +457,13 @@ export class PaymentService {
396
457
  document.write(threeDSData.challengeHtml);
397
458
  document.close();
398
459
  }
460
+ return { kind: 'redirected' };
399
461
  }
400
462
  catch (error) {
401
- this.callbacks.onError?.(error instanceof Error ? error.message : 'KessPay 3DS failed');
463
+ const msg = error instanceof Error ? error.message : 'KessPay 3DS failed';
464
+ this.callbacks.onError?.(msg);
402
465
  this.callbacks.onProcessing?.(false);
466
+ return { kind: 'failed', error: msg };
403
467
  }
404
468
  }
405
469
  // --------------------------------------------------------------------------
@@ -408,9 +472,10 @@ export class PaymentService {
408
472
  handleTrustFlowAuth(actionData) {
409
473
  const authData = actionData?.metadata?.trustflow;
410
474
  if (!authData?.appId || !authData?.txnId || !authData?.hash) {
411
- this.callbacks.onError?.('Missing Trust Flow 3DS data');
475
+ const msg = 'Missing Trust Flow 3DS data';
476
+ this.callbacks.onError?.(msg);
412
477
  this.callbacks.onProcessing?.(false);
413
- return;
478
+ return { kind: 'failed', error: msg };
414
479
  }
415
480
  try {
416
481
  this.callbacks.onProcessing?.(false);
@@ -427,10 +492,13 @@ export class PaymentService {
427
492
  }
428
493
  document.body.appendChild(form);
429
494
  form.submit();
495
+ return { kind: 'redirected' };
430
496
  }
431
497
  catch (error) {
432
- this.callbacks.onError?.(error instanceof Error ? error.message : 'Trust Flow 3DS failed');
498
+ const msg = error instanceof Error ? error.message : 'Trust Flow 3DS failed';
499
+ this.callbacks.onError?.(msg);
433
500
  this.callbacks.onProcessing?.(false);
501
+ return { kind: 'failed', error: msg };
434
502
  }
435
503
  }
436
504
  // --------------------------------------------------------------------------
@@ -439,9 +507,10 @@ export class PaymentService {
439
507
  async handleFinixRadar(payment, actionData) {
440
508
  const radarConfig = actionData.metadata?.radar;
441
509
  if (!radarConfig) {
442
- this.callbacks.onError?.('Finix radar config missing');
510
+ const msg = 'Finix radar config missing';
511
+ this.callbacks.onError?.(msg);
443
512
  this.callbacks.onProcessing?.(false);
444
- return;
513
+ return { kind: 'failed', error: msg, payment };
445
514
  }
446
515
  try {
447
516
  await this.loadScript('https://js.finix.com/v/1/finix.js', () => typeof window.Finix?.Auth === 'function');
@@ -469,11 +538,13 @@ export class PaymentService {
469
538
  },
470
539
  });
471
540
  const resumed = await this.paymentsResource.completePaymentAfterAction(payment.id);
472
- await this.handleResumedPayment(resumed);
541
+ return await this.handleResumedPayment(resumed);
473
542
  }
474
543
  catch (error) {
475
- this.callbacks.onError?.(error instanceof Error ? error.message : 'Finix radar failed');
544
+ const msg = error instanceof Error ? error.message : 'Finix radar failed';
545
+ this.callbacks.onError?.(msg);
476
546
  this.callbacks.onProcessing?.(false);
547
+ return { kind: 'failed', error: msg, payment };
477
548
  }
478
549
  }
479
550
  // --------------------------------------------------------------------------
@@ -482,9 +553,10 @@ export class PaymentService {
482
553
  async handleStripeRadar(payment, actionData) {
483
554
  const radarConfig = actionData.metadata?.radar;
484
555
  if (!radarConfig?.publishableKey) {
485
- this.callbacks.onError?.('Stripe radar config missing');
556
+ const msg = 'Stripe radar config missing';
557
+ this.callbacks.onError?.(msg);
486
558
  this.callbacks.onProcessing?.(false);
487
- return;
559
+ return { kind: 'failed', error: msg, payment };
488
560
  }
489
561
  try {
490
562
  await this.loadScript('https://js.stripe.com/v3/', () => typeof window.Stripe === 'function');
@@ -500,11 +572,13 @@ export class PaymentService {
500
572
  stripeRadarSessionData: result.radarSession,
501
573
  });
502
574
  const resumed = await this.paymentsResource.completePaymentAfterAction(payment.id);
503
- await this.handleResumedPayment(resumed);
575
+ return await this.handleResumedPayment(resumed);
504
576
  }
505
577
  catch (error) {
506
- this.callbacks.onError?.(error instanceof Error ? error.message : 'Stripe radar failed');
578
+ const msg = error instanceof Error ? error.message : 'Stripe radar failed';
579
+ this.callbacks.onError?.(msg);
507
580
  this.callbacks.onProcessing?.(false);
581
+ return { kind: 'failed', error: msg, payment };
508
582
  }
509
583
  }
510
584
  // --------------------------------------------------------------------------
@@ -513,9 +587,10 @@ export class PaymentService {
513
587
  async handleNgeniusThreeds(payment, actionData) {
514
588
  const sdk = actionData.metadata?.sdk;
515
589
  if (!sdk?.paymentResponse || !sdk.orderReference || !sdk.paymentReference) {
516
- this.callbacks.onError?.('N-Genius 3DS: missing SDK metadata');
590
+ const msg = 'N-Genius 3DS: missing SDK metadata';
591
+ this.callbacks.onError?.(msg);
517
592
  this.callbacks.onProcessing?.(false);
518
- return;
593
+ return { kind: 'failed', error: msg, payment };
519
594
  }
520
595
  try {
521
596
  const sdkUrl = sdk.isSandboxed
@@ -543,13 +618,14 @@ export class PaymentService {
543
618
  orderReference: sdk.orderReference,
544
619
  paymentReference: sdk.paymentReference,
545
620
  });
546
- await this.handleResumedPayment(completedPayment);
621
+ return await this.handleResumedPayment(completedPayment);
547
622
  }
548
623
  catch (error) {
549
624
  const msg = error instanceof Error ? error.message : 'N-Genius 3DS failed';
550
625
  console.error('[N-Genius 3DS] Error:', error);
551
626
  this.callbacks.onError?.(msg);
552
627
  this.callbacks.onProcessing?.(false);
628
+ return { kind: 'failed', error: msg, payment };
553
629
  }
554
630
  }
555
631
  // --------------------------------------------------------------------------
@@ -560,9 +636,10 @@ export class PaymentService {
560
636
  const orderId = payment.order?.id;
561
637
  const checkoutSessionId = payment.order?.checkoutSessionId;
562
638
  if (!orderId || !checkoutSessionId) {
563
- this.callbacks.onError?.('Missing order info for Airwallex radar');
639
+ const msg = 'Missing order info for Airwallex radar';
640
+ this.callbacks.onError?.(msg);
564
641
  this.callbacks.onProcessing?.(false);
565
- return;
642
+ return { kind: 'failed', error: msg, payment };
566
643
  }
567
644
  try {
568
645
  const sessionId = crypto.randomUUID();
@@ -591,11 +668,13 @@ export class PaymentService {
591
668
  airwallexRadarSessionId: sessionId,
592
669
  });
593
670
  const resumed = await this.paymentsResource.completePaymentAfterAction(payment.id);
594
- await this.handleResumedPayment(resumed);
671
+ return await this.handleResumedPayment(resumed);
595
672
  }
596
673
  catch (error) {
597
- this.callbacks.onError?.(error instanceof Error ? error.message : 'Airwallex radar failed');
674
+ const msg = error instanceof Error ? error.message : 'Airwallex radar failed';
675
+ this.callbacks.onError?.(msg);
598
676
  this.callbacks.onProcessing?.(false);
677
+ return { kind: 'failed', error: msg, payment };
599
678
  }
600
679
  }
601
680
  // --------------------------------------------------------------------------
@@ -604,9 +683,10 @@ export class PaymentService {
604
683
  async handleMasterCardAuth(payment, actionData) {
605
684
  const threeDSData = actionData?.metadata?.threeds;
606
685
  if (!threeDSData?.sessionId || !threeDSData?.merchantId) {
607
- this.callbacks.onError?.('Missing MasterCard 3DS data');
686
+ const msg = 'Missing MasterCard 3DS data';
687
+ this.callbacks.onError?.(msg);
608
688
  this.callbacks.onProcessing?.(false);
609
- return;
689
+ return { kind: 'failed', error: msg, payment };
610
690
  }
611
691
  try {
612
692
  this.callbacks.onProcessing?.(false);
@@ -659,21 +739,26 @@ export class PaymentService {
659
739
  document.write(challengeHtml);
660
740
  document.close();
661
741
  }
742
+ return { kind: 'redirected' };
662
743
  }
663
- else {
664
- // Frictionless — complete immediately
665
- if (threeDSData.paymentId) {
666
- const resumed = await this.paymentsResource.completePaymentAfterAction(threeDSData.paymentId);
667
- await this.handleResumedPayment(resumed);
668
- }
744
+ // Frictionless — complete immediately via resumed payment
745
+ if (threeDSData.paymentId) {
746
+ const resumed = await this.paymentsResource.completePaymentAfterAction(threeDSData.paymentId);
747
+ const cleanup = document.getElementById(containerId);
748
+ if (cleanup)
749
+ cleanup.remove();
750
+ return await this.handleResumedPayment(resumed);
669
751
  }
670
752
  const cleanup = document.getElementById(containerId);
671
753
  if (cleanup)
672
754
  cleanup.remove();
755
+ return { kind: 'pending' };
673
756
  }
674
757
  catch (error) {
675
- this.callbacks.onError?.(error instanceof Error ? error.message : 'MasterCard 3DS failed');
758
+ const msg = error instanceof Error ? error.message : 'MasterCard 3DS failed';
759
+ this.callbacks.onError?.(msg);
676
760
  this.callbacks.onProcessing?.(false);
761
+ return { kind: 'failed', error: msg, payment };
677
762
  }
678
763
  }
679
764
  // --------------------------------------------------------------------------
@@ -979,4 +1064,86 @@ export class PaymentService {
979
1064
  return { success: false, error: msg };
980
1065
  }
981
1066
  }
1067
+ /**
1068
+ * Stripe Express Checkout Element payment.
1069
+ *
1070
+ * Mirrors the inline flow from `react/components/StripeExpressButton.onConfirm`:
1071
+ * 1. processPaymentDirect with isExpress=true → returns clientSecret
1072
+ * 2. stripe.confirmPayment(elements, clientSecret) — must run while wallet sheet is open
1073
+ * 3. Poll until webhook marks payment succeeded
1074
+ *
1075
+ * Used for ECE methods Stripe surfaces in one element: apple_pay, google_pay, link, klarna.
1076
+ * The `stripe` and `elements` refs come from Stripe React hooks at the call site.
1077
+ */
1078
+ async processStripeExpressPayment(checkoutSessionId, paymentMethod, processorId, stripe, elements, options) {
1079
+ this.callbacks.onProcessing?.(true);
1080
+ this.callbacks.onError?.(null);
1081
+ try {
1082
+ const paymentFlowId = getAssignedPaymentFlowId();
1083
+ const response = await this.paymentsResource.processPaymentDirect(checkoutSessionId, '', undefined, {
1084
+ processorId,
1085
+ paymentMethod,
1086
+ isExpress: true,
1087
+ paymentFlowId,
1088
+ shippingRateId: options?.shippingRateId,
1089
+ });
1090
+ this.callbacks.onCurrentPaymentId?.(response.payment?.id || null);
1091
+ const clientSecret = response?.payment?.requireActionData?.metadata?.stripeExpressCheckout?.clientSecret;
1092
+ if (!clientSecret) {
1093
+ const msg = 'Express checkout configuration missing — no client secret returned';
1094
+ this.callbacks.onError?.(msg);
1095
+ this.callbacks.onProcessing?.(false);
1096
+ return { success: false, error: msg, payment: response.payment, order: response.order };
1097
+ }
1098
+ const { error: confirmError } = await stripe.confirmPayment({
1099
+ elements,
1100
+ clientSecret,
1101
+ confirmParams: { return_url: window.location.href },
1102
+ redirect: 'if_required',
1103
+ });
1104
+ if (confirmError) {
1105
+ const msg = confirmError.message ?? 'Payment confirmation failed';
1106
+ this.callbacks.onError?.(msg);
1107
+ this.callbacks.onProcessing?.(false);
1108
+ return { success: false, error: msg, payment: response.payment, order: response.order };
1109
+ }
1110
+ // Poll for webhook completion. Ignore stripe_express_checkout require-actions
1111
+ // (already handled by stripe.confirmPayment above) and keep polling.
1112
+ const paymentId = response.payment.id;
1113
+ return await new Promise((resolve) => {
1114
+ const tick = async () => {
1115
+ try {
1116
+ const payment = await this.paymentsResource.getPaymentStatus(paymentId);
1117
+ if (payment.status === 'succeeded' ||
1118
+ (payment.status === 'pending' && payment.subStatus === 'authorized')) {
1119
+ this.callbacks.onProcessing?.(false);
1120
+ resolve({ success: true, payment, order: response.order });
1121
+ return;
1122
+ }
1123
+ if (payment.status !== 'succeeded' && payment.status !== 'pending') {
1124
+ const msg = payment.status || 'Payment failed';
1125
+ this.callbacks.onError?.(msg);
1126
+ this.callbacks.onProcessing?.(false);
1127
+ resolve({ success: false, error: msg, payment, order: response.order });
1128
+ return;
1129
+ }
1130
+ // Pending — keep polling. Ignore stripe_express_checkout require-action
1131
+ // (it's just the original intent metadata; we already handled it).
1132
+ setTimeout(tick, 1500);
1133
+ }
1134
+ catch {
1135
+ // Network blip — try again. Bail after a few failures handled by getPaymentStatus retries upstream.
1136
+ setTimeout(tick, 1500);
1137
+ }
1138
+ };
1139
+ void tick();
1140
+ });
1141
+ }
1142
+ catch (error) {
1143
+ const msg = error instanceof Error ? error.message : String(error);
1144
+ this.callbacks.onError?.(msg);
1145
+ this.callbacks.onProcessing?.(false);
1146
+ return { success: false, error: msg };
1147
+ }
1148
+ }
982
1149
  }