@voxepay/checkout 0.3.12 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.esm.js CHANGED
@@ -611,10 +611,17 @@ const cleanPan = (pan) => {
611
611
  */
612
612
  const DEFAULT_BASE_URL = 'https://devpay.voxepay.app';
613
613
  class VoxePayApiClient {
614
- constructor(apiKey, baseUrl) {
614
+ constructor(apiKey, baseUrl, bearerToken) {
615
615
  this.apiKey = apiKey;
616
+ this.bearerToken = bearerToken;
616
617
  this.baseUrl = baseUrl || DEFAULT_BASE_URL;
617
618
  }
619
+ /**
620
+ * Set Bearer token for authenticated endpoints (like getPaymentStatus)
621
+ */
622
+ setBearerToken(token) {
623
+ this.bearerToken = token;
624
+ }
618
625
  async request(endpoint, body) {
619
626
  const url = `${this.baseUrl}${endpoint}`;
620
627
  const response = await fetch(url, {
@@ -630,18 +637,50 @@ class VoxePayApiClient {
630
637
  if (!response.ok) {
631
638
  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}`;
632
639
  const error = new Error(errorMessage);
633
- error.code = (data === null || data === void 0 ? void 0 : data.code) || `HTTP_${response.status}`;
640
+ error.code = (data === null || data === void 0 ? void 0 : data.errorCode) || (data === null || data === void 0 ? void 0 : data.code) || `HTTP_${response.status}`;
641
+ error.status = response.status;
642
+ error.data = data;
643
+ throw error;
644
+ }
645
+ return data;
646
+ }
647
+ async get(endpoint, useBearerAuth = false) {
648
+ const url = `${this.baseUrl}${endpoint}`;
649
+ const headers = {
650
+ 'Accept': 'application/json',
651
+ };
652
+ if (useBearerAuth && this.bearerToken) {
653
+ headers['Authorization'] = `Bearer ${this.bearerToken}`;
654
+ }
655
+ else {
656
+ headers['X-API-Key'] = this.apiKey;
657
+ }
658
+ const response = await fetch(url, {
659
+ method: 'GET',
660
+ headers,
661
+ });
662
+ const data = await response.json();
663
+ if (!response.ok) {
664
+ 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}`;
665
+ const error = new Error(errorMessage);
666
+ error.code = (data === null || data === void 0 ? void 0 : data.errorCode) || (data === null || data === void 0 ? void 0 : data.code) || `HTTP_${response.status}`;
634
667
  error.status = response.status;
635
668
  error.data = data;
669
+ error.success = data === null || data === void 0 ? void 0 : data.success;
636
670
  throw error;
637
671
  }
638
672
  return data;
639
673
  }
640
674
  /**
641
- * Initiate a card payment
675
+ * Initiate a payment (card or bank transfer)
642
676
  */
643
677
  async initiatePayment(data) {
644
- return this.request('/api/v1/payments/initiate', data);
678
+ const response = await this.request('/api/v1/payments/initiate', data);
679
+ // Handle both wrapped { success, data: {...} } and flat response formats
680
+ if (response.data && typeof response.data === 'object' && 'transactionRef' in response.data) {
681
+ return response.data;
682
+ }
683
+ return response;
645
684
  }
646
685
  /**
647
686
  * Validate OTP for a payment
@@ -655,6 +694,35 @@ class VoxePayApiClient {
655
694
  async resendOTP(data) {
656
695
  await this.request('/api/v1/payments/resend-otp', data);
657
696
  }
697
+ /**
698
+ * Get payment status by transaction reference (used for polling DVA payments)
699
+ * Uses public endpoint - no authentication required
700
+ * @param transactionRef - The transaction reference to check
701
+ * @param options - Optional query parameters (simulateWebhook, bypassKey)
702
+ */
703
+ async getPaymentStatus(transactionRef, options) {
704
+ let endpoint = `/api/v1/payments/public/${encodeURIComponent(transactionRef)}`;
705
+ // Add query parameters if provided
706
+ const queryParams = [];
707
+ if ((options === null || options === void 0 ? void 0 : options.simulateWebhook) !== undefined) {
708
+ queryParams.push(`simulateWebhook=${options.simulateWebhook}`);
709
+ }
710
+ if (options === null || options === void 0 ? void 0 : options.bypassKey) {
711
+ queryParams.push(`bypassKey=${encodeURIComponent(options.bypassKey)}`);
712
+ }
713
+ if (queryParams.length > 0) {
714
+ endpoint += `?${queryParams.join('&')}`;
715
+ }
716
+ const response = await this.get(endpoint, false // Use X-API-Key (public endpoint doesn't need Bearer token)
717
+ );
718
+ if (!response.success) {
719
+ const error = new Error(response.message || 'Payment not found');
720
+ error.code = response.errorCode || 'PAYMENT_NOT_FOUND';
721
+ error.success = false;
722
+ throw error;
723
+ }
724
+ return response.data;
725
+ }
658
726
  }
659
727
 
660
728
  /**
@@ -701,6 +769,10 @@ class VoxePayModal {
701
769
  this.container = null;
702
770
  this.overlay = null;
703
771
  this.apiClient = null;
772
+ /** Polling interval ID for DVA status checks */
773
+ this.statusPollingInterval = null;
774
+ /** Polling timeout (auto-stop after 30 minutes) */
775
+ this.statusPollingTimeout = null;
704
776
  this.handleEscape = (e) => {
705
777
  if (e.key === 'Escape') {
706
778
  this.close();
@@ -755,6 +827,7 @@ class VoxePayModal {
755
827
  close() {
756
828
  var _a;
757
829
  this.stopTransferTimer();
830
+ this.stopStatusPolling();
758
831
  (_a = this.overlay) === null || _a === void 0 ? void 0 : _a.classList.remove('voxepay-visible');
759
832
  setTimeout(() => {
760
833
  var _a, _b, _c;
@@ -1321,26 +1394,56 @@ class VoxePayModal {
1321
1394
  }
1322
1395
  }
1323
1396
  /**
1324
- * Load bank transfer details (from callback or generate mock)
1397
+ * Load bank transfer details (from callback or via API)
1325
1398
  */
1326
1399
  async loadBankTransferDetails() {
1327
- var _a;
1400
+ var _a, _b;
1328
1401
  try {
1329
1402
  let details;
1330
1403
  if (this.options.onBankTransferRequested) {
1404
+ // Use merchant-provided callback
1331
1405
  details = await this.options.onBankTransferRequested();
1332
1406
  }
1333
- else {
1334
- // Mock/default bank details for testing
1335
- await new Promise(resolve => setTimeout(resolve, 1500));
1407
+ else if (this.apiClient && this.options._sdkConfig) {
1408
+ // Call real DVA API
1409
+ const transactionRef = `VP-${Date.now().toString(36).toUpperCase()}-${Math.random().toString(36).substring(2, 8).toUpperCase()}`;
1410
+ const response = await this.apiClient.initiatePayment({
1411
+ organizationId: this.options._sdkConfig.organizationId,
1412
+ transactionRef,
1413
+ customerId: this.options.customerEmail || '',
1414
+ customerEmail: this.options.customerEmail,
1415
+ customerPhone: this.options.customerPhone,
1416
+ amount: this.options.amount / 100, // Convert from kobo/cents to main unit
1417
+ currency: this.options.currency,
1418
+ paymentMethod: 'BANK_TRANSFER',
1419
+ authData: btoa(JSON.stringify({ source: 'web', method: 'bank_transfer' })),
1420
+ narration: this.options.description,
1421
+ durationMinutes: 30,
1422
+ });
1423
+ // Store transaction ref for status polling
1424
+ // API may return 'id' (DVA) or 'paymentId' (card) depending on payment method
1425
+ this.state.transactionRef = response.transactionRef || transactionRef;
1426
+ this.state.paymentId = response.paymentId || response.id || null;
1427
+ const va = response.virtualAccount;
1428
+ if (!va) {
1429
+ throw { code: 'MISSING_VIRTUAL_ACCOUNT', message: 'No virtual account returned from server.' };
1430
+ }
1431
+ // Compute expiresIn from the ISO timestamp
1432
+ const expiresAtDate = new Date(va.expires_at);
1433
+ const nowMs = Date.now();
1434
+ const expiresInSeconds = Math.max(0, Math.floor((expiresAtDate.getTime() - nowMs) / 1000));
1336
1435
  details = {
1337
- accountNumber: '0123456789',
1338
- bankName: 'VoxePay Bank',
1339
- accountName: 'VoxePay Collections',
1340
- reference: `VP-${Date.now().toString(36).toUpperCase()}`,
1341
- expiresIn: 1800, // 30 minutes
1436
+ accountNumber: va.account_number,
1437
+ bankName: va.bank_name,
1438
+ accountName: va.account_name,
1439
+ reference: this.state.transactionRef,
1440
+ expiresIn: expiresInSeconds,
1441
+ expiresAt: va.expires_at,
1342
1442
  };
1343
1443
  }
1444
+ else {
1445
+ throw { code: 'SDK_ERROR', message: 'SDK not properly initialized. Missing API key or organization ID.' };
1446
+ }
1344
1447
  this.state.bankTransferDetails = details;
1345
1448
  // Re-render the bank transfer view
1346
1449
  const formContainer = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#voxepay-form-container');
@@ -1352,13 +1455,57 @@ class VoxePayModal {
1352
1455
  }
1353
1456
  }
1354
1457
  catch (error) {
1458
+ const errorCode = (error === null || error === void 0 ? void 0 : error.code) || 'BANK_TRANSFER_INIT_FAILED';
1459
+ const errorMessage = (error === null || error === void 0 ? void 0 : error.message) || 'Could not generate bank transfer details. Please try again.';
1460
+ // Show error in the loading area
1461
+ const formContainer = (_b = this.container) === null || _b === void 0 ? void 0 : _b.querySelector('#voxepay-form-container');
1462
+ if (formContainer) {
1463
+ formContainer.innerHTML = `
1464
+ <div class="voxepay-transfer-view">
1465
+ <div style="text-align: center; padding: 40px 16px;">
1466
+ <div style="font-size: 2.5rem; margin-bottom: 16px;">⚠️</div>
1467
+ <h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 8px; color: var(--voxepay-text);">${this.getDVAErrorTitle(errorCode)}</h3>
1468
+ <p style="font-size: 0.875rem; color: var(--voxepay-text-muted); margin-bottom: 24px;">${errorMessage}</p>
1469
+ <button class="voxepay-submit-btn" id="voxepay-retry-transfer"><span>Try Again</span></button>
1470
+ </div>
1471
+ </div>
1472
+ `;
1473
+ const retryBtn = formContainer.querySelector('#voxepay-retry-transfer');
1474
+ retryBtn === null || retryBtn === void 0 ? void 0 : retryBtn.addEventListener('click', () => {
1475
+ formContainer.innerHTML = `
1476
+ <div class="voxepay-transfer-view">
1477
+ <div class="voxepay-transfer-loading">
1478
+ <div class="voxepay-spinner"></div>
1479
+ <p style="margin-top: 16px; color: var(--voxepay-text-muted);">Generating account details...</p>
1480
+ </div>
1481
+ </div>
1482
+ `;
1483
+ this.loadBankTransferDetails();
1484
+ });
1485
+ }
1355
1486
  this.options.onError({
1356
- code: 'BANK_TRANSFER_INIT_FAILED',
1357
- message: 'Could not generate bank transfer details. Please try again.',
1487
+ code: errorCode,
1488
+ message: errorMessage,
1358
1489
  recoverable: true,
1490
+ details: error === null || error === void 0 ? void 0 : error.data,
1359
1491
  });
1360
1492
  }
1361
1493
  }
1494
+ /**
1495
+ * Get user-friendly error title for DVA error codes
1496
+ */
1497
+ getDVAErrorTitle(code) {
1498
+ const titles = {
1499
+ 'ACCOUNT_CREATION_FAILED': 'Account Generation Failed',
1500
+ 'ACCOUNT_EXPIRED': 'Account Expired',
1501
+ 'INVALID_AMOUNT': 'Invalid Amount',
1502
+ 'UNAUTHORIZED': 'Authentication Failed',
1503
+ 'ORGANIZATION_NOT_FOUND': 'Configuration Error',
1504
+ 'MISSING_VIRTUAL_ACCOUNT': 'Account Generation Failed',
1505
+ 'SDK_ERROR': 'Configuration Error',
1506
+ };
1507
+ return titles[code] || 'Something Went Wrong';
1508
+ }
1362
1509
  /**
1363
1510
  * Copy text to clipboard with visual feedback
1364
1511
  */
@@ -1389,7 +1536,7 @@ class VoxePayModal {
1389
1536
  }
1390
1537
  }
1391
1538
  /**
1392
- * Handle "I've sent the money" confirmation
1539
+ * Handle "I've sent the money" confirmation — starts polling for payment status
1393
1540
  */
1394
1541
  async handleTransferConfirm() {
1395
1542
  var _a, _b;
@@ -1398,12 +1545,11 @@ class VoxePayModal {
1398
1545
  confirmBtn.disabled = true;
1399
1546
  confirmBtn.innerHTML = '<div class="voxepay-spinner"></div><span>Confirming transfer...</span>';
1400
1547
  }
1401
- this.stopTransferTimer();
1402
- try {
1403
- // Simulate transfer verification
1404
- await new Promise(resolve => setTimeout(resolve, 3000));
1548
+ // If no API client or no transaction ref, fall back to pending result
1549
+ if (!this.apiClient || !this.state.transactionRef) {
1550
+ this.stopTransferTimer();
1405
1551
  const result = {
1406
- id: `pay_transfer_${Date.now()}`,
1552
+ id: this.state.paymentId || `pay_transfer_${Date.now()}`,
1407
1553
  status: 'pending',
1408
1554
  amount: this.options.amount,
1409
1555
  currency: this.options.currency,
@@ -1414,16 +1560,216 @@ class VoxePayModal {
1414
1560
  this.state.isSuccess = true;
1415
1561
  this.renderSuccessView();
1416
1562
  this.options.onSuccess(result);
1563
+ return;
1417
1564
  }
1418
- catch (error) {
1419
- if (confirmBtn) {
1420
- confirmBtn.disabled = false;
1421
- confirmBtn.innerHTML = "<span>I've sent the money</span>";
1565
+ // Show polling UI
1566
+ this.renderPollingView();
1567
+ this.startStatusPolling();
1568
+ }
1569
+ /**
1570
+ * Render the "waiting for payment confirmation" polling view
1571
+ */
1572
+ renderPollingView() {
1573
+ var _a;
1574
+ const formContainer = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#voxepay-form-container');
1575
+ if (!formContainer)
1576
+ return;
1577
+ const formattedAmount = formatAmount(this.options.amount, this.options.currency);
1578
+ formContainer.innerHTML = `
1579
+ <div class="voxepay-transfer-view">
1580
+ <div style="text-align: center; padding: 32px 16px;">
1581
+ <div class="voxepay-spinner" style="width: 40px; height: 40px; margin: 0 auto 20px; border-width: 3px;"></div>
1582
+ <h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 8px; color: var(--voxepay-text);">Waiting for Payment</h3>
1583
+ <p style="font-size: 0.875rem; color: var(--voxepay-text-muted); margin-bottom: 4px;">
1584
+ We're checking for your transfer of <strong>${formattedAmount}</strong>
1585
+ </p>
1586
+ <p style="font-size: 0.813rem; color: var(--voxepay-text-subtle); margin-bottom: 24px;" id="voxepay-polling-status">
1587
+ This usually takes a few seconds...
1588
+ </p>
1589
+ <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;">
1590
+ <span>Cancel</span>
1591
+ </button>
1592
+ </div>
1593
+ </div>
1594
+ `;
1595
+ const cancelBtn = formContainer.querySelector('#voxepay-cancel-polling');
1596
+ cancelBtn === null || cancelBtn === void 0 ? void 0 : cancelBtn.addEventListener('click', () => {
1597
+ this.stopStatusPolling();
1598
+ // Re-render bank transfer view so user can try again
1599
+ if (this.state.bankTransferDetails) {
1600
+ const amt = formatAmount(this.options.amount, this.options.currency);
1601
+ formContainer.innerHTML = this.getBankTransferHTML(amt);
1602
+ this.attachBankTransferListeners();
1603
+ this.startTransferTimer();
1422
1604
  }
1423
- const paymentError = error;
1424
- this.options.onError(paymentError);
1605
+ });
1606
+ }
1607
+ /**
1608
+ * Start polling payment status every 5 seconds
1609
+ */
1610
+ startStatusPolling() {
1611
+ this.stopStatusPolling(); // clear any existing poll
1612
+ const poll = async () => {
1613
+ var _a;
1614
+ if (!this.apiClient || !this.state.transactionRef)
1615
+ return;
1616
+ try {
1617
+ const statusData = await this.apiClient.getPaymentStatus(this.state.transactionRef);
1618
+ this.handlePollingStatusUpdate(statusData.status, statusData);
1619
+ }
1620
+ catch (error) {
1621
+ console.error('[VoxePay] Status polling error:', error);
1622
+ // Don't stop polling on transient errors — let it retry
1623
+ const statusEl = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#voxepay-polling-status');
1624
+ if (statusEl) {
1625
+ statusEl.textContent = 'Still checking... Please wait.';
1626
+ }
1627
+ }
1628
+ };
1629
+ // Initial check immediately
1630
+ poll();
1631
+ // Then poll every 5 seconds
1632
+ this.statusPollingInterval = window.setInterval(poll, 5000);
1633
+ // Auto-stop after 30 minutes
1634
+ this.statusPollingTimeout = window.setTimeout(() => {
1635
+ this.stopStatusPolling();
1636
+ this.handlePollingStatusUpdate('EXPIRED', null);
1637
+ }, 30 * 60 * 1000);
1638
+ }
1639
+ /**
1640
+ * Stop payment status polling
1641
+ */
1642
+ stopStatusPolling() {
1643
+ if (this.statusPollingInterval) {
1644
+ clearInterval(this.statusPollingInterval);
1645
+ this.statusPollingInterval = null;
1646
+ }
1647
+ if (this.statusPollingTimeout) {
1648
+ clearTimeout(this.statusPollingTimeout);
1649
+ this.statusPollingTimeout = null;
1425
1650
  }
1426
1651
  }
1652
+ /**
1653
+ * Handle status updates from polling
1654
+ */
1655
+ handlePollingStatusUpdate(status, data) {
1656
+ var _a;
1657
+ const terminalStatuses = ['COMPLETED', 'FAILED', 'EXPIRED', 'CANCELLED'];
1658
+ if (terminalStatuses.includes(status)) {
1659
+ this.stopStatusPolling();
1660
+ this.stopTransferTimer();
1661
+ }
1662
+ switch (status) {
1663
+ case 'COMPLETED': {
1664
+ const result = {
1665
+ id: (data === null || data === void 0 ? void 0 : data.id) || this.state.paymentId || `pay_transfer_${Date.now()}`,
1666
+ status: 'success',
1667
+ amount: this.options.amount,
1668
+ currency: this.options.currency,
1669
+ timestamp: (data === null || data === void 0 ? void 0 : data.completedAt) || new Date().toISOString(),
1670
+ reference: this.state.transactionRef || undefined,
1671
+ paymentMethod: 'bank_transfer',
1672
+ data: data || undefined,
1673
+ };
1674
+ this.state.isSuccess = true;
1675
+ this.renderSuccessView();
1676
+ this.options.onSuccess(result);
1677
+ break;
1678
+ }
1679
+ case 'PAYMENT_RECEIVED': {
1680
+ // Almost done — update the polling UI message
1681
+ const statusEl = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#voxepay-polling-status');
1682
+ if (statusEl) {
1683
+ statusEl.textContent = 'Payment received! Processing...';
1684
+ }
1685
+ break;
1686
+ }
1687
+ case 'PARTIAL_PAYMENT': {
1688
+ this.stopStatusPolling();
1689
+ this.renderTransferErrorView('Partial Payment Received', 'The amount transferred is less than the required amount. Please transfer the remaining balance or contact support.');
1690
+ break;
1691
+ }
1692
+ case 'OVERPAID': {
1693
+ // Overpayment is still successful — show success but note the overpayment
1694
+ const result = {
1695
+ id: (data === null || data === void 0 ? void 0 : data.id) || this.state.paymentId || `pay_transfer_${Date.now()}`,
1696
+ status: 'success',
1697
+ amount: this.options.amount,
1698
+ currency: this.options.currency,
1699
+ timestamp: (data === null || data === void 0 ? void 0 : data.completedAt) || new Date().toISOString(),
1700
+ reference: this.state.transactionRef || undefined,
1701
+ paymentMethod: 'bank_transfer',
1702
+ data: { ...data, overpaid: true },
1703
+ };
1704
+ this.state.isSuccess = true;
1705
+ this.renderSuccessView();
1706
+ this.options.onSuccess(result);
1707
+ break;
1708
+ }
1709
+ case 'EXPIRED': {
1710
+ this.renderTransferErrorView('Payment Expired', 'The virtual account has expired. Please try again to generate a new account.');
1711
+ this.options.onError({
1712
+ code: 'ACCOUNT_EXPIRED',
1713
+ message: 'Virtual account expired before payment was received.',
1714
+ recoverable: true,
1715
+ });
1716
+ break;
1717
+ }
1718
+ case 'FAILED': {
1719
+ this.renderTransferErrorView('Payment Failed', (data === null || data === void 0 ? void 0 : data.message) || 'The payment could not be processed. Please try again.');
1720
+ this.options.onError({
1721
+ code: 'PAYMENT_FAILED',
1722
+ message: (data === null || data === void 0 ? void 0 : data.message) || 'Payment failed.',
1723
+ recoverable: true,
1724
+ });
1725
+ break;
1726
+ }
1727
+ case 'CANCELLED': {
1728
+ this.renderTransferErrorView('Payment Cancelled', 'This payment has been cancelled.');
1729
+ this.options.onError({
1730
+ code: 'PAYMENT_CANCELLED',
1731
+ message: 'Payment was cancelled.',
1732
+ recoverable: false,
1733
+ });
1734
+ break;
1735
+ }
1736
+ }
1737
+ }
1738
+ /**
1739
+ * Render error view for transfer issues with retry option
1740
+ */
1741
+ renderTransferErrorView(title, message) {
1742
+ var _a;
1743
+ const formContainer = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#voxepay-form-container');
1744
+ if (!formContainer)
1745
+ return;
1746
+ formContainer.innerHTML = `
1747
+ <div class="voxepay-transfer-view">
1748
+ <div style="text-align: center; padding: 40px 16px;">
1749
+ <div style="font-size: 2.5rem; margin-bottom: 16px;">❌</div>
1750
+ <h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 8px; color: var(--voxepay-text);">${title}</h3>
1751
+ <p style="font-size: 0.875rem; color: var(--voxepay-text-muted); margin-bottom: 24px;">${message}</p>
1752
+ <button class="voxepay-submit-btn" id="voxepay-retry-transfer"><span>Try Again</span></button>
1753
+ </div>
1754
+ </div>
1755
+ `;
1756
+ const retryBtn = formContainer.querySelector('#voxepay-retry-transfer');
1757
+ retryBtn === null || retryBtn === void 0 ? void 0 : retryBtn.addEventListener('click', () => {
1758
+ // Reset state for new attempt
1759
+ this.state.bankTransferDetails = null;
1760
+ this.state.transactionRef = null;
1761
+ this.state.paymentId = null;
1762
+ formContainer.innerHTML = `
1763
+ <div class="voxepay-transfer-view">
1764
+ <div class="voxepay-transfer-loading">
1765
+ <div class="voxepay-spinner"></div>
1766
+ <p style="margin-top: 16px; color: var(--voxepay-text-muted);">Generating account details...</p>
1767
+ </div>
1768
+ </div>
1769
+ `;
1770
+ this.loadBankTransferDetails();
1771
+ });
1772
+ }
1427
1773
  /**
1428
1774
  * Start the transfer countdown timer
1429
1775
  */