@voxepay/checkout 0.3.12 → 0.4.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.
@@ -13,6 +13,16 @@ export interface InitiatePaymentRequest {
13
13
  paymentMethod: 'CARD' | 'BANK_TRANSFER';
14
14
  authData: string;
15
15
  narration?: string;
16
+ /** Virtual account validity in minutes (5-1440). Only for BANK_TRANSFER. Default: 30 */
17
+ durationMinutes?: number;
18
+ }
19
+ export interface VirtualAccountResponse {
20
+ account_number: string;
21
+ account_name: string;
22
+ bank_name: string;
23
+ bank_code: string;
24
+ expires_at: string;
25
+ status: string;
16
26
  }
17
27
  export interface InitiatePaymentResponse {
18
28
  paymentId: string;
@@ -21,6 +31,22 @@ export interface InitiatePaymentResponse {
21
31
  status: string;
22
32
  message: string;
23
33
  eciFlag?: string;
34
+ otpRequired?: boolean;
35
+ virtualAccount?: VirtualAccountResponse;
36
+ [key: string]: unknown;
37
+ }
38
+ export type DVAPaymentStatus = 'INITIATED' | 'PENDING_PAYMENT' | 'PAYMENT_RECEIVED' | 'COMPLETED' | 'PARTIAL_PAYMENT' | 'OVERPAID' | 'EXPIRED' | 'FAILED' | 'CANCELLED';
39
+ export interface PaymentStatusResponse {
40
+ id: string;
41
+ transactionRef: string;
42
+ status: DVAPaymentStatus;
43
+ amount: number;
44
+ paymentMethod: string;
45
+ completedAt?: string;
46
+ virtualAccount?: {
47
+ account_number: string;
48
+ status: string;
49
+ };
24
50
  [key: string]: unknown;
25
51
  }
26
52
  export interface ValidateOTPRequest {
@@ -48,8 +74,9 @@ export declare class VoxePayApiClient {
48
74
  private baseUrl;
49
75
  constructor(apiKey: string, baseUrl?: string);
50
76
  private request;
77
+ private get;
51
78
  /**
52
- * Initiate a card payment
79
+ * Initiate a payment (card or bank transfer)
53
80
  */
54
81
  initiatePayment(data: InitiatePaymentRequest): Promise<InitiatePaymentResponse>;
55
82
  /**
@@ -60,4 +87,8 @@ export declare class VoxePayApiClient {
60
87
  * Resend OTP for a payment
61
88
  */
62
89
  resendOTP(data: ResendOTPRequest): Promise<void>;
90
+ /**
91
+ * Get payment status by transaction reference (used for polling DVA payments)
92
+ */
93
+ getPaymentStatus(transactionRef: string): Promise<PaymentStatusResponse>;
63
94
  }
@@ -109,17 +109,45 @@ export declare class VoxePayModal {
109
109
  */
110
110
  private switchPaymentMethod;
111
111
  /**
112
- * Load bank transfer details (from callback or generate mock)
112
+ * Load bank transfer details (from callback or via API)
113
113
  */
114
114
  private loadBankTransferDetails;
115
+ /**
116
+ * Get user-friendly error title for DVA error codes
117
+ */
118
+ private getDVAErrorTitle;
115
119
  /**
116
120
  * Copy text to clipboard with visual feedback
117
121
  */
118
122
  private copyToClipboard;
123
+ /** Polling interval ID for DVA status checks */
124
+ private statusPollingInterval;
125
+ /** Polling timeout (auto-stop after 30 minutes) */
126
+ private statusPollingTimeout;
119
127
  /**
120
- * Handle "I've sent the money" confirmation
128
+ * Handle "I've sent the money" confirmation — starts polling for payment status
121
129
  */
122
130
  private handleTransferConfirm;
131
+ /**
132
+ * Render the "waiting for payment confirmation" polling view
133
+ */
134
+ private renderPollingView;
135
+ /**
136
+ * Start polling payment status every 5 seconds
137
+ */
138
+ private startStatusPolling;
139
+ /**
140
+ * Stop payment status polling
141
+ */
142
+ private stopStatusPolling;
143
+ /**
144
+ * Handle status updates from polling
145
+ */
146
+ private handlePollingStatusUpdate;
147
+ /**
148
+ * Render error view for transfer issues with retry option
149
+ */
150
+ private renderTransferErrorView;
123
151
  /**
124
152
  * Start the transfer countdown timer
125
153
  */
package/dist/index.cjs.js CHANGED
@@ -634,7 +634,27 @@ class VoxePayApiClient {
634
634
  if (!response.ok) {
635
635
  const errorMessage = (data === null || data === void 0 ? void 0 : data.message) || (data === null || data === void 0 ? void 0 : data.error) || `Request failed with status ${response.status}`;
636
636
  const error = new Error(errorMessage);
637
- error.code = (data === null || data === void 0 ? void 0 : data.code) || `HTTP_${response.status}`;
637
+ error.code = (data === null || data === void 0 ? void 0 : data.errorCode) || (data === null || data === void 0 ? void 0 : data.code) || `HTTP_${response.status}`;
638
+ error.status = response.status;
639
+ error.data = data;
640
+ throw error;
641
+ }
642
+ return data;
643
+ }
644
+ async get(endpoint) {
645
+ const url = `${this.baseUrl}${endpoint}`;
646
+ const response = await fetch(url, {
647
+ method: 'GET',
648
+ headers: {
649
+ 'X-API-Key': this.apiKey,
650
+ 'Accept': 'application/json',
651
+ },
652
+ });
653
+ const data = await response.json();
654
+ if (!response.ok) {
655
+ const errorMessage = (data === null || data === void 0 ? void 0 : data.message) || (data === null || data === void 0 ? void 0 : data.error) || `Request failed with status ${response.status}`;
656
+ const error = new Error(errorMessage);
657
+ error.code = (data === null || data === void 0 ? void 0 : data.errorCode) || (data === null || data === void 0 ? void 0 : data.code) || `HTTP_${response.status}`;
638
658
  error.status = response.status;
639
659
  error.data = data;
640
660
  throw error;
@@ -642,10 +662,15 @@ class VoxePayApiClient {
642
662
  return data;
643
663
  }
644
664
  /**
645
- * Initiate a card payment
665
+ * Initiate a payment (card or bank transfer)
646
666
  */
647
667
  async initiatePayment(data) {
648
- return this.request('/api/v1/payments/initiate', data);
668
+ const response = await this.request('/api/v1/payments/initiate', data);
669
+ // Handle both wrapped { success, data: {...} } and flat response formats
670
+ if (response.data && typeof response.data === 'object' && 'transactionRef' in response.data) {
671
+ return response.data;
672
+ }
673
+ return response;
649
674
  }
650
675
  /**
651
676
  * Validate OTP for a payment
@@ -659,6 +684,13 @@ class VoxePayApiClient {
659
684
  async resendOTP(data) {
660
685
  await this.request('/api/v1/payments/resend-otp', data);
661
686
  }
687
+ /**
688
+ * Get payment status by transaction reference (used for polling DVA payments)
689
+ */
690
+ async getPaymentStatus(transactionRef) {
691
+ const response = await this.get(`/api/v1/payments/${encodeURIComponent(transactionRef)}`);
692
+ return response.data;
693
+ }
662
694
  }
663
695
 
664
696
  /**
@@ -705,6 +737,10 @@ class VoxePayModal {
705
737
  this.container = null;
706
738
  this.overlay = null;
707
739
  this.apiClient = null;
740
+ /** Polling interval ID for DVA status checks */
741
+ this.statusPollingInterval = null;
742
+ /** Polling timeout (auto-stop after 30 minutes) */
743
+ this.statusPollingTimeout = null;
708
744
  this.handleEscape = (e) => {
709
745
  if (e.key === 'Escape') {
710
746
  this.close();
@@ -759,6 +795,7 @@ class VoxePayModal {
759
795
  close() {
760
796
  var _a;
761
797
  this.stopTransferTimer();
798
+ this.stopStatusPolling();
762
799
  (_a = this.overlay) === null || _a === void 0 ? void 0 : _a.classList.remove('voxepay-visible');
763
800
  setTimeout(() => {
764
801
  var _a, _b, _c;
@@ -1325,26 +1362,56 @@ class VoxePayModal {
1325
1362
  }
1326
1363
  }
1327
1364
  /**
1328
- * Load bank transfer details (from callback or generate mock)
1365
+ * Load bank transfer details (from callback or via API)
1329
1366
  */
1330
1367
  async loadBankTransferDetails() {
1331
- var _a;
1368
+ var _a, _b;
1332
1369
  try {
1333
1370
  let details;
1334
1371
  if (this.options.onBankTransferRequested) {
1372
+ // Use merchant-provided callback
1335
1373
  details = await this.options.onBankTransferRequested();
1336
1374
  }
1337
- else {
1338
- // Mock/default bank details for testing
1339
- await new Promise(resolve => setTimeout(resolve, 1500));
1375
+ else if (this.apiClient && this.options._sdkConfig) {
1376
+ // Call real DVA API
1377
+ const transactionRef = `VP-${Date.now().toString(36).toUpperCase()}-${Math.random().toString(36).substring(2, 8).toUpperCase()}`;
1378
+ const response = await this.apiClient.initiatePayment({
1379
+ organizationId: this.options._sdkConfig.organizationId,
1380
+ transactionRef,
1381
+ customerId: this.options.customerEmail || '',
1382
+ customerEmail: this.options.customerEmail,
1383
+ customerPhone: this.options.customerPhone,
1384
+ amount: this.options.amount / 100, // Convert from kobo/cents to main unit
1385
+ currency: this.options.currency,
1386
+ paymentMethod: 'BANK_TRANSFER',
1387
+ authData: btoa(JSON.stringify({ source: 'web', method: 'bank_transfer' })),
1388
+ narration: this.options.description,
1389
+ durationMinutes: 30,
1390
+ });
1391
+ // Store transaction ref for status polling
1392
+ // API may return 'id' (DVA) or 'paymentId' (card) depending on payment method
1393
+ this.state.transactionRef = response.transactionRef || transactionRef;
1394
+ this.state.paymentId = response.paymentId || response.id || null;
1395
+ const va = response.virtualAccount;
1396
+ if (!va) {
1397
+ throw { code: 'MISSING_VIRTUAL_ACCOUNT', message: 'No virtual account returned from server.' };
1398
+ }
1399
+ // Compute expiresIn from the ISO timestamp
1400
+ const expiresAtDate = new Date(va.expires_at);
1401
+ const nowMs = Date.now();
1402
+ const expiresInSeconds = Math.max(0, Math.floor((expiresAtDate.getTime() - nowMs) / 1000));
1340
1403
  details = {
1341
- accountNumber: '0123456789',
1342
- bankName: 'VoxePay Bank',
1343
- accountName: 'VoxePay Collections',
1344
- reference: `VP-${Date.now().toString(36).toUpperCase()}`,
1345
- expiresIn: 1800, // 30 minutes
1404
+ accountNumber: va.account_number,
1405
+ bankName: va.bank_name,
1406
+ accountName: va.account_name,
1407
+ reference: this.state.transactionRef,
1408
+ expiresIn: expiresInSeconds,
1409
+ expiresAt: va.expires_at,
1346
1410
  };
1347
1411
  }
1412
+ else {
1413
+ throw { code: 'SDK_ERROR', message: 'SDK not properly initialized. Missing API key or organization ID.' };
1414
+ }
1348
1415
  this.state.bankTransferDetails = details;
1349
1416
  // Re-render the bank transfer view
1350
1417
  const formContainer = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#voxepay-form-container');
@@ -1356,13 +1423,57 @@ class VoxePayModal {
1356
1423
  }
1357
1424
  }
1358
1425
  catch (error) {
1426
+ const errorCode = (error === null || error === void 0 ? void 0 : error.code) || 'BANK_TRANSFER_INIT_FAILED';
1427
+ const errorMessage = (error === null || error === void 0 ? void 0 : error.message) || 'Could not generate bank transfer details. Please try again.';
1428
+ // Show error in the loading area
1429
+ const formContainer = (_b = this.container) === null || _b === void 0 ? void 0 : _b.querySelector('#voxepay-form-container');
1430
+ if (formContainer) {
1431
+ formContainer.innerHTML = `
1432
+ <div class="voxepay-transfer-view">
1433
+ <div style="text-align: center; padding: 40px 16px;">
1434
+ <div style="font-size: 2.5rem; margin-bottom: 16px;">⚠️</div>
1435
+ <h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 8px; color: var(--voxepay-text);">${this.getDVAErrorTitle(errorCode)}</h3>
1436
+ <p style="font-size: 0.875rem; color: var(--voxepay-text-muted); margin-bottom: 24px;">${errorMessage}</p>
1437
+ <button class="voxepay-submit-btn" id="voxepay-retry-transfer"><span>Try Again</span></button>
1438
+ </div>
1439
+ </div>
1440
+ `;
1441
+ const retryBtn = formContainer.querySelector('#voxepay-retry-transfer');
1442
+ retryBtn === null || retryBtn === void 0 ? void 0 : retryBtn.addEventListener('click', () => {
1443
+ formContainer.innerHTML = `
1444
+ <div class="voxepay-transfer-view">
1445
+ <div class="voxepay-transfer-loading">
1446
+ <div class="voxepay-spinner"></div>
1447
+ <p style="margin-top: 16px; color: var(--voxepay-text-muted);">Generating account details...</p>
1448
+ </div>
1449
+ </div>
1450
+ `;
1451
+ this.loadBankTransferDetails();
1452
+ });
1453
+ }
1359
1454
  this.options.onError({
1360
- code: 'BANK_TRANSFER_INIT_FAILED',
1361
- message: 'Could not generate bank transfer details. Please try again.',
1455
+ code: errorCode,
1456
+ message: errorMessage,
1362
1457
  recoverable: true,
1458
+ details: error === null || error === void 0 ? void 0 : error.data,
1363
1459
  });
1364
1460
  }
1365
1461
  }
1462
+ /**
1463
+ * Get user-friendly error title for DVA error codes
1464
+ */
1465
+ getDVAErrorTitle(code) {
1466
+ const titles = {
1467
+ 'ACCOUNT_CREATION_FAILED': 'Account Generation Failed',
1468
+ 'ACCOUNT_EXPIRED': 'Account Expired',
1469
+ 'INVALID_AMOUNT': 'Invalid Amount',
1470
+ 'UNAUTHORIZED': 'Authentication Failed',
1471
+ 'ORGANIZATION_NOT_FOUND': 'Configuration Error',
1472
+ 'MISSING_VIRTUAL_ACCOUNT': 'Account Generation Failed',
1473
+ 'SDK_ERROR': 'Configuration Error',
1474
+ };
1475
+ return titles[code] || 'Something Went Wrong';
1476
+ }
1366
1477
  /**
1367
1478
  * Copy text to clipboard with visual feedback
1368
1479
  */
@@ -1393,7 +1504,7 @@ class VoxePayModal {
1393
1504
  }
1394
1505
  }
1395
1506
  /**
1396
- * Handle "I've sent the money" confirmation
1507
+ * Handle "I've sent the money" confirmation — starts polling for payment status
1397
1508
  */
1398
1509
  async handleTransferConfirm() {
1399
1510
  var _a, _b;
@@ -1402,12 +1513,11 @@ class VoxePayModal {
1402
1513
  confirmBtn.disabled = true;
1403
1514
  confirmBtn.innerHTML = '<div class="voxepay-spinner"></div><span>Confirming transfer...</span>';
1404
1515
  }
1405
- this.stopTransferTimer();
1406
- try {
1407
- // Simulate transfer verification
1408
- await new Promise(resolve => setTimeout(resolve, 3000));
1516
+ // If no API client or no transaction ref, fall back to pending result
1517
+ if (!this.apiClient || !this.state.transactionRef) {
1518
+ this.stopTransferTimer();
1409
1519
  const result = {
1410
- id: `pay_transfer_${Date.now()}`,
1520
+ id: this.state.paymentId || `pay_transfer_${Date.now()}`,
1411
1521
  status: 'pending',
1412
1522
  amount: this.options.amount,
1413
1523
  currency: this.options.currency,
@@ -1418,16 +1528,216 @@ class VoxePayModal {
1418
1528
  this.state.isSuccess = true;
1419
1529
  this.renderSuccessView();
1420
1530
  this.options.onSuccess(result);
1531
+ return;
1421
1532
  }
1422
- catch (error) {
1423
- if (confirmBtn) {
1424
- confirmBtn.disabled = false;
1425
- confirmBtn.innerHTML = "<span>I've sent the money</span>";
1533
+ // Show polling UI
1534
+ this.renderPollingView();
1535
+ this.startStatusPolling();
1536
+ }
1537
+ /**
1538
+ * Render the "waiting for payment confirmation" polling view
1539
+ */
1540
+ renderPollingView() {
1541
+ var _a;
1542
+ const formContainer = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#voxepay-form-container');
1543
+ if (!formContainer)
1544
+ return;
1545
+ const formattedAmount = formatAmount(this.options.amount, this.options.currency);
1546
+ formContainer.innerHTML = `
1547
+ <div class="voxepay-transfer-view">
1548
+ <div style="text-align: center; padding: 32px 16px;">
1549
+ <div class="voxepay-spinner" style="width: 40px; height: 40px; margin: 0 auto 20px; border-width: 3px;"></div>
1550
+ <h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 8px; color: var(--voxepay-text);">Waiting for Payment</h3>
1551
+ <p style="font-size: 0.875rem; color: var(--voxepay-text-muted); margin-bottom: 4px;">
1552
+ We're checking for your transfer of <strong>${formattedAmount}</strong>
1553
+ </p>
1554
+ <p style="font-size: 0.813rem; color: var(--voxepay-text-subtle); margin-bottom: 24px;" id="voxepay-polling-status">
1555
+ This usually takes a few seconds...
1556
+ </p>
1557
+ <button class="voxepay-submit-btn" id="voxepay-cancel-polling" style="background: var(--voxepay-surface); border: 1px solid var(--voxepay-border); color: var(--voxepay-text-muted); box-shadow: none;">
1558
+ <span>Cancel</span>
1559
+ </button>
1560
+ </div>
1561
+ </div>
1562
+ `;
1563
+ const cancelBtn = formContainer.querySelector('#voxepay-cancel-polling');
1564
+ cancelBtn === null || cancelBtn === void 0 ? void 0 : cancelBtn.addEventListener('click', () => {
1565
+ this.stopStatusPolling();
1566
+ // Re-render bank transfer view so user can try again
1567
+ if (this.state.bankTransferDetails) {
1568
+ const amt = formatAmount(this.options.amount, this.options.currency);
1569
+ formContainer.innerHTML = this.getBankTransferHTML(amt);
1570
+ this.attachBankTransferListeners();
1571
+ this.startTransferTimer();
1572
+ }
1573
+ });
1574
+ }
1575
+ /**
1576
+ * Start polling payment status every 5 seconds
1577
+ */
1578
+ startStatusPolling() {
1579
+ this.stopStatusPolling(); // clear any existing poll
1580
+ const poll = async () => {
1581
+ var _a;
1582
+ if (!this.apiClient || !this.state.transactionRef)
1583
+ return;
1584
+ try {
1585
+ const statusData = await this.apiClient.getPaymentStatus(this.state.transactionRef);
1586
+ this.handlePollingStatusUpdate(statusData.status, statusData);
1587
+ }
1588
+ catch (error) {
1589
+ console.error('[VoxePay] Status polling error:', error);
1590
+ // Don't stop polling on transient errors — let it retry
1591
+ const statusEl = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#voxepay-polling-status');
1592
+ if (statusEl) {
1593
+ statusEl.textContent = 'Still checking... Please wait.';
1594
+ }
1595
+ }
1596
+ };
1597
+ // Initial check immediately
1598
+ poll();
1599
+ // Then poll every 5 seconds
1600
+ this.statusPollingInterval = window.setInterval(poll, 5000);
1601
+ // Auto-stop after 30 minutes
1602
+ this.statusPollingTimeout = window.setTimeout(() => {
1603
+ this.stopStatusPolling();
1604
+ this.handlePollingStatusUpdate('EXPIRED', null);
1605
+ }, 30 * 60 * 1000);
1606
+ }
1607
+ /**
1608
+ * Stop payment status polling
1609
+ */
1610
+ stopStatusPolling() {
1611
+ if (this.statusPollingInterval) {
1612
+ clearInterval(this.statusPollingInterval);
1613
+ this.statusPollingInterval = null;
1614
+ }
1615
+ if (this.statusPollingTimeout) {
1616
+ clearTimeout(this.statusPollingTimeout);
1617
+ this.statusPollingTimeout = null;
1618
+ }
1619
+ }
1620
+ /**
1621
+ * Handle status updates from polling
1622
+ */
1623
+ handlePollingStatusUpdate(status, data) {
1624
+ var _a;
1625
+ const terminalStatuses = ['COMPLETED', 'FAILED', 'EXPIRED', 'CANCELLED'];
1626
+ if (terminalStatuses.includes(status)) {
1627
+ this.stopStatusPolling();
1628
+ this.stopTransferTimer();
1629
+ }
1630
+ switch (status) {
1631
+ case 'COMPLETED': {
1632
+ const result = {
1633
+ id: (data === null || data === void 0 ? void 0 : data.id) || this.state.paymentId || `pay_transfer_${Date.now()}`,
1634
+ status: 'success',
1635
+ amount: this.options.amount,
1636
+ currency: this.options.currency,
1637
+ timestamp: (data === null || data === void 0 ? void 0 : data.completedAt) || new Date().toISOString(),
1638
+ reference: this.state.transactionRef || undefined,
1639
+ paymentMethod: 'bank_transfer',
1640
+ data: data || undefined,
1641
+ };
1642
+ this.state.isSuccess = true;
1643
+ this.renderSuccessView();
1644
+ this.options.onSuccess(result);
1645
+ break;
1646
+ }
1647
+ case 'PAYMENT_RECEIVED': {
1648
+ // Almost done — update the polling UI message
1649
+ const statusEl = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#voxepay-polling-status');
1650
+ if (statusEl) {
1651
+ statusEl.textContent = 'Payment received! Processing...';
1652
+ }
1653
+ break;
1654
+ }
1655
+ case 'PARTIAL_PAYMENT': {
1656
+ this.stopStatusPolling();
1657
+ this.renderTransferErrorView('Partial Payment Received', 'The amount transferred is less than the required amount. Please transfer the remaining balance or contact support.');
1658
+ break;
1659
+ }
1660
+ case 'OVERPAID': {
1661
+ // Overpayment is still successful — show success but note the overpayment
1662
+ const result = {
1663
+ id: (data === null || data === void 0 ? void 0 : data.id) || this.state.paymentId || `pay_transfer_${Date.now()}`,
1664
+ status: 'success',
1665
+ amount: this.options.amount,
1666
+ currency: this.options.currency,
1667
+ timestamp: (data === null || data === void 0 ? void 0 : data.completedAt) || new Date().toISOString(),
1668
+ reference: this.state.transactionRef || undefined,
1669
+ paymentMethod: 'bank_transfer',
1670
+ data: { ...data, overpaid: true },
1671
+ };
1672
+ this.state.isSuccess = true;
1673
+ this.renderSuccessView();
1674
+ this.options.onSuccess(result);
1675
+ break;
1676
+ }
1677
+ case 'EXPIRED': {
1678
+ this.renderTransferErrorView('Payment Expired', 'The virtual account has expired. Please try again to generate a new account.');
1679
+ this.options.onError({
1680
+ code: 'ACCOUNT_EXPIRED',
1681
+ message: 'Virtual account expired before payment was received.',
1682
+ recoverable: true,
1683
+ });
1684
+ break;
1685
+ }
1686
+ case 'FAILED': {
1687
+ this.renderTransferErrorView('Payment Failed', (data === null || data === void 0 ? void 0 : data.message) || 'The payment could not be processed. Please try again.');
1688
+ this.options.onError({
1689
+ code: 'PAYMENT_FAILED',
1690
+ message: (data === null || data === void 0 ? void 0 : data.message) || 'Payment failed.',
1691
+ recoverable: true,
1692
+ });
1693
+ break;
1694
+ }
1695
+ case 'CANCELLED': {
1696
+ this.renderTransferErrorView('Payment Cancelled', 'This payment has been cancelled.');
1697
+ this.options.onError({
1698
+ code: 'PAYMENT_CANCELLED',
1699
+ message: 'Payment was cancelled.',
1700
+ recoverable: false,
1701
+ });
1702
+ break;
1426
1703
  }
1427
- const paymentError = error;
1428
- this.options.onError(paymentError);
1429
1704
  }
1430
1705
  }
1706
+ /**
1707
+ * Render error view for transfer issues with retry option
1708
+ */
1709
+ renderTransferErrorView(title, message) {
1710
+ var _a;
1711
+ const formContainer = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#voxepay-form-container');
1712
+ if (!formContainer)
1713
+ return;
1714
+ formContainer.innerHTML = `
1715
+ <div class="voxepay-transfer-view">
1716
+ <div style="text-align: center; padding: 40px 16px;">
1717
+ <div style="font-size: 2.5rem; margin-bottom: 16px;">❌</div>
1718
+ <h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 8px; color: var(--voxepay-text);">${title}</h3>
1719
+ <p style="font-size: 0.875rem; color: var(--voxepay-text-muted); margin-bottom: 24px;">${message}</p>
1720
+ <button class="voxepay-submit-btn" id="voxepay-retry-transfer"><span>Try Again</span></button>
1721
+ </div>
1722
+ </div>
1723
+ `;
1724
+ const retryBtn = formContainer.querySelector('#voxepay-retry-transfer');
1725
+ retryBtn === null || retryBtn === void 0 ? void 0 : retryBtn.addEventListener('click', () => {
1726
+ // Reset state for new attempt
1727
+ this.state.bankTransferDetails = null;
1728
+ this.state.transactionRef = null;
1729
+ this.state.paymentId = null;
1730
+ formContainer.innerHTML = `
1731
+ <div class="voxepay-transfer-view">
1732
+ <div class="voxepay-transfer-loading">
1733
+ <div class="voxepay-spinner"></div>
1734
+ <p style="margin-top: 16px; color: var(--voxepay-text-muted);">Generating account details...</p>
1735
+ </div>
1736
+ </div>
1737
+ `;
1738
+ this.loadBankTransferDetails();
1739
+ });
1740
+ }
1431
1741
  /**
1432
1742
  * Start the transfer countdown timer
1433
1743
  */