@voxepay/checkout 0.3.11 → 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.
- package/dist/api/client.d.ts +32 -1
- package/dist/components/modal.d.ts +30 -2
- package/dist/index.cjs.js +339 -29
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.esm.js +339 -29
- package/dist/index.esm.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/voxepay-checkout.min.js +1 -1
- package/dist/voxepay-checkout.min.js.map +1 -1
- package/package.json +1 -1
- package/src/api/client.ts +84 -3
- package/src/components/modal.ts +343 -27
- package/src/index.ts +3 -0
- package/src/types.ts +2 -0
- package/src/utils/encryption.ts +2 -2
package/dist/index.esm.js
CHANGED
|
@@ -563,8 +563,8 @@ const RSA_CONFIG = {
|
|
|
563
563
|
*/
|
|
564
564
|
const generateAuthData = async ({ version, pan, pin, expiryDate, cvv, }) => {
|
|
565
565
|
try {
|
|
566
|
-
// Build the cipher text: version + "
|
|
567
|
-
const authDataCipher = `${version}
|
|
566
|
+
// Build the cipher text: version + "Z" + pan + "Z" + pin + ...
|
|
567
|
+
const authDataCipher = `${version}Z${pan}Z${pin}Z${expiryDate}Z${cvv}`;
|
|
568
568
|
const messageBytes = hexToBytes(toHex(authDataCipher));
|
|
569
569
|
// Parse RSA public key
|
|
570
570
|
const modulus = RSABigInt.fromHex(RSA_CONFIG.modulus);
|
|
@@ -630,7 +630,27 @@ class VoxePayApiClient {
|
|
|
630
630
|
if (!response.ok) {
|
|
631
631
|
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
632
|
const error = new Error(errorMessage);
|
|
633
|
-
error.code = (data === null || data === void 0 ? void 0 : data.code) || `HTTP_${response.status}`;
|
|
633
|
+
error.code = (data === null || data === void 0 ? void 0 : data.errorCode) || (data === null || data === void 0 ? void 0 : data.code) || `HTTP_${response.status}`;
|
|
634
|
+
error.status = response.status;
|
|
635
|
+
error.data = data;
|
|
636
|
+
throw error;
|
|
637
|
+
}
|
|
638
|
+
return data;
|
|
639
|
+
}
|
|
640
|
+
async get(endpoint) {
|
|
641
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
642
|
+
const response = await fetch(url, {
|
|
643
|
+
method: 'GET',
|
|
644
|
+
headers: {
|
|
645
|
+
'X-API-Key': this.apiKey,
|
|
646
|
+
'Accept': 'application/json',
|
|
647
|
+
},
|
|
648
|
+
});
|
|
649
|
+
const data = await response.json();
|
|
650
|
+
if (!response.ok) {
|
|
651
|
+
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}`;
|
|
652
|
+
const error = new Error(errorMessage);
|
|
653
|
+
error.code = (data === null || data === void 0 ? void 0 : data.errorCode) || (data === null || data === void 0 ? void 0 : data.code) || `HTTP_${response.status}`;
|
|
634
654
|
error.status = response.status;
|
|
635
655
|
error.data = data;
|
|
636
656
|
throw error;
|
|
@@ -638,10 +658,15 @@ class VoxePayApiClient {
|
|
|
638
658
|
return data;
|
|
639
659
|
}
|
|
640
660
|
/**
|
|
641
|
-
* Initiate a card
|
|
661
|
+
* Initiate a payment (card or bank transfer)
|
|
642
662
|
*/
|
|
643
663
|
async initiatePayment(data) {
|
|
644
|
-
|
|
664
|
+
const response = await this.request('/api/v1/payments/initiate', data);
|
|
665
|
+
// Handle both wrapped { success, data: {...} } and flat response formats
|
|
666
|
+
if (response.data && typeof response.data === 'object' && 'transactionRef' in response.data) {
|
|
667
|
+
return response.data;
|
|
668
|
+
}
|
|
669
|
+
return response;
|
|
645
670
|
}
|
|
646
671
|
/**
|
|
647
672
|
* Validate OTP for a payment
|
|
@@ -655,6 +680,13 @@ class VoxePayApiClient {
|
|
|
655
680
|
async resendOTP(data) {
|
|
656
681
|
await this.request('/api/v1/payments/resend-otp', data);
|
|
657
682
|
}
|
|
683
|
+
/**
|
|
684
|
+
* Get payment status by transaction reference (used for polling DVA payments)
|
|
685
|
+
*/
|
|
686
|
+
async getPaymentStatus(transactionRef) {
|
|
687
|
+
const response = await this.get(`/api/v1/payments/${encodeURIComponent(transactionRef)}`);
|
|
688
|
+
return response.data;
|
|
689
|
+
}
|
|
658
690
|
}
|
|
659
691
|
|
|
660
692
|
/**
|
|
@@ -701,6 +733,10 @@ class VoxePayModal {
|
|
|
701
733
|
this.container = null;
|
|
702
734
|
this.overlay = null;
|
|
703
735
|
this.apiClient = null;
|
|
736
|
+
/** Polling interval ID for DVA status checks */
|
|
737
|
+
this.statusPollingInterval = null;
|
|
738
|
+
/** Polling timeout (auto-stop after 30 minutes) */
|
|
739
|
+
this.statusPollingTimeout = null;
|
|
704
740
|
this.handleEscape = (e) => {
|
|
705
741
|
if (e.key === 'Escape') {
|
|
706
742
|
this.close();
|
|
@@ -755,6 +791,7 @@ class VoxePayModal {
|
|
|
755
791
|
close() {
|
|
756
792
|
var _a;
|
|
757
793
|
this.stopTransferTimer();
|
|
794
|
+
this.stopStatusPolling();
|
|
758
795
|
(_a = this.overlay) === null || _a === void 0 ? void 0 : _a.classList.remove('voxepay-visible');
|
|
759
796
|
setTimeout(() => {
|
|
760
797
|
var _a, _b, _c;
|
|
@@ -1321,26 +1358,56 @@ class VoxePayModal {
|
|
|
1321
1358
|
}
|
|
1322
1359
|
}
|
|
1323
1360
|
/**
|
|
1324
|
-
* Load bank transfer details (from callback or
|
|
1361
|
+
* Load bank transfer details (from callback or via API)
|
|
1325
1362
|
*/
|
|
1326
1363
|
async loadBankTransferDetails() {
|
|
1327
|
-
var _a;
|
|
1364
|
+
var _a, _b;
|
|
1328
1365
|
try {
|
|
1329
1366
|
let details;
|
|
1330
1367
|
if (this.options.onBankTransferRequested) {
|
|
1368
|
+
// Use merchant-provided callback
|
|
1331
1369
|
details = await this.options.onBankTransferRequested();
|
|
1332
1370
|
}
|
|
1333
|
-
else {
|
|
1334
|
-
//
|
|
1335
|
-
|
|
1371
|
+
else if (this.apiClient && this.options._sdkConfig) {
|
|
1372
|
+
// Call real DVA API
|
|
1373
|
+
const transactionRef = `VP-${Date.now().toString(36).toUpperCase()}-${Math.random().toString(36).substring(2, 8).toUpperCase()}`;
|
|
1374
|
+
const response = await this.apiClient.initiatePayment({
|
|
1375
|
+
organizationId: this.options._sdkConfig.organizationId,
|
|
1376
|
+
transactionRef,
|
|
1377
|
+
customerId: this.options.customerEmail || '',
|
|
1378
|
+
customerEmail: this.options.customerEmail,
|
|
1379
|
+
customerPhone: this.options.customerPhone,
|
|
1380
|
+
amount: this.options.amount / 100, // Convert from kobo/cents to main unit
|
|
1381
|
+
currency: this.options.currency,
|
|
1382
|
+
paymentMethod: 'BANK_TRANSFER',
|
|
1383
|
+
authData: btoa(JSON.stringify({ source: 'web', method: 'bank_transfer' })),
|
|
1384
|
+
narration: this.options.description,
|
|
1385
|
+
durationMinutes: 30,
|
|
1386
|
+
});
|
|
1387
|
+
// Store transaction ref for status polling
|
|
1388
|
+
// API may return 'id' (DVA) or 'paymentId' (card) depending on payment method
|
|
1389
|
+
this.state.transactionRef = response.transactionRef || transactionRef;
|
|
1390
|
+
this.state.paymentId = response.paymentId || response.id || null;
|
|
1391
|
+
const va = response.virtualAccount;
|
|
1392
|
+
if (!va) {
|
|
1393
|
+
throw { code: 'MISSING_VIRTUAL_ACCOUNT', message: 'No virtual account returned from server.' };
|
|
1394
|
+
}
|
|
1395
|
+
// Compute expiresIn from the ISO timestamp
|
|
1396
|
+
const expiresAtDate = new Date(va.expires_at);
|
|
1397
|
+
const nowMs = Date.now();
|
|
1398
|
+
const expiresInSeconds = Math.max(0, Math.floor((expiresAtDate.getTime() - nowMs) / 1000));
|
|
1336
1399
|
details = {
|
|
1337
|
-
accountNumber:
|
|
1338
|
-
bankName:
|
|
1339
|
-
accountName:
|
|
1340
|
-
reference:
|
|
1341
|
-
expiresIn:
|
|
1400
|
+
accountNumber: va.account_number,
|
|
1401
|
+
bankName: va.bank_name,
|
|
1402
|
+
accountName: va.account_name,
|
|
1403
|
+
reference: this.state.transactionRef,
|
|
1404
|
+
expiresIn: expiresInSeconds,
|
|
1405
|
+
expiresAt: va.expires_at,
|
|
1342
1406
|
};
|
|
1343
1407
|
}
|
|
1408
|
+
else {
|
|
1409
|
+
throw { code: 'SDK_ERROR', message: 'SDK not properly initialized. Missing API key or organization ID.' };
|
|
1410
|
+
}
|
|
1344
1411
|
this.state.bankTransferDetails = details;
|
|
1345
1412
|
// Re-render the bank transfer view
|
|
1346
1413
|
const formContainer = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#voxepay-form-container');
|
|
@@ -1352,13 +1419,57 @@ class VoxePayModal {
|
|
|
1352
1419
|
}
|
|
1353
1420
|
}
|
|
1354
1421
|
catch (error) {
|
|
1422
|
+
const errorCode = (error === null || error === void 0 ? void 0 : error.code) || 'BANK_TRANSFER_INIT_FAILED';
|
|
1423
|
+
const errorMessage = (error === null || error === void 0 ? void 0 : error.message) || 'Could not generate bank transfer details. Please try again.';
|
|
1424
|
+
// Show error in the loading area
|
|
1425
|
+
const formContainer = (_b = this.container) === null || _b === void 0 ? void 0 : _b.querySelector('#voxepay-form-container');
|
|
1426
|
+
if (formContainer) {
|
|
1427
|
+
formContainer.innerHTML = `
|
|
1428
|
+
<div class="voxepay-transfer-view">
|
|
1429
|
+
<div style="text-align: center; padding: 40px 16px;">
|
|
1430
|
+
<div style="font-size: 2.5rem; margin-bottom: 16px;">⚠️</div>
|
|
1431
|
+
<h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 8px; color: var(--voxepay-text);">${this.getDVAErrorTitle(errorCode)}</h3>
|
|
1432
|
+
<p style="font-size: 0.875rem; color: var(--voxepay-text-muted); margin-bottom: 24px;">${errorMessage}</p>
|
|
1433
|
+
<button class="voxepay-submit-btn" id="voxepay-retry-transfer"><span>Try Again</span></button>
|
|
1434
|
+
</div>
|
|
1435
|
+
</div>
|
|
1436
|
+
`;
|
|
1437
|
+
const retryBtn = formContainer.querySelector('#voxepay-retry-transfer');
|
|
1438
|
+
retryBtn === null || retryBtn === void 0 ? void 0 : retryBtn.addEventListener('click', () => {
|
|
1439
|
+
formContainer.innerHTML = `
|
|
1440
|
+
<div class="voxepay-transfer-view">
|
|
1441
|
+
<div class="voxepay-transfer-loading">
|
|
1442
|
+
<div class="voxepay-spinner"></div>
|
|
1443
|
+
<p style="margin-top: 16px; color: var(--voxepay-text-muted);">Generating account details...</p>
|
|
1444
|
+
</div>
|
|
1445
|
+
</div>
|
|
1446
|
+
`;
|
|
1447
|
+
this.loadBankTransferDetails();
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1355
1450
|
this.options.onError({
|
|
1356
|
-
code:
|
|
1357
|
-
message:
|
|
1451
|
+
code: errorCode,
|
|
1452
|
+
message: errorMessage,
|
|
1358
1453
|
recoverable: true,
|
|
1454
|
+
details: error === null || error === void 0 ? void 0 : error.data,
|
|
1359
1455
|
});
|
|
1360
1456
|
}
|
|
1361
1457
|
}
|
|
1458
|
+
/**
|
|
1459
|
+
* Get user-friendly error title for DVA error codes
|
|
1460
|
+
*/
|
|
1461
|
+
getDVAErrorTitle(code) {
|
|
1462
|
+
const titles = {
|
|
1463
|
+
'ACCOUNT_CREATION_FAILED': 'Account Generation Failed',
|
|
1464
|
+
'ACCOUNT_EXPIRED': 'Account Expired',
|
|
1465
|
+
'INVALID_AMOUNT': 'Invalid Amount',
|
|
1466
|
+
'UNAUTHORIZED': 'Authentication Failed',
|
|
1467
|
+
'ORGANIZATION_NOT_FOUND': 'Configuration Error',
|
|
1468
|
+
'MISSING_VIRTUAL_ACCOUNT': 'Account Generation Failed',
|
|
1469
|
+
'SDK_ERROR': 'Configuration Error',
|
|
1470
|
+
};
|
|
1471
|
+
return titles[code] || 'Something Went Wrong';
|
|
1472
|
+
}
|
|
1362
1473
|
/**
|
|
1363
1474
|
* Copy text to clipboard with visual feedback
|
|
1364
1475
|
*/
|
|
@@ -1389,7 +1500,7 @@ class VoxePayModal {
|
|
|
1389
1500
|
}
|
|
1390
1501
|
}
|
|
1391
1502
|
/**
|
|
1392
|
-
* Handle "I've sent the money" confirmation
|
|
1503
|
+
* Handle "I've sent the money" confirmation — starts polling for payment status
|
|
1393
1504
|
*/
|
|
1394
1505
|
async handleTransferConfirm() {
|
|
1395
1506
|
var _a, _b;
|
|
@@ -1398,12 +1509,11 @@ class VoxePayModal {
|
|
|
1398
1509
|
confirmBtn.disabled = true;
|
|
1399
1510
|
confirmBtn.innerHTML = '<div class="voxepay-spinner"></div><span>Confirming transfer...</span>';
|
|
1400
1511
|
}
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
1512
|
+
// If no API client or no transaction ref, fall back to pending result
|
|
1513
|
+
if (!this.apiClient || !this.state.transactionRef) {
|
|
1514
|
+
this.stopTransferTimer();
|
|
1405
1515
|
const result = {
|
|
1406
|
-
id: `pay_transfer_${Date.now()}`,
|
|
1516
|
+
id: this.state.paymentId || `pay_transfer_${Date.now()}`,
|
|
1407
1517
|
status: 'pending',
|
|
1408
1518
|
amount: this.options.amount,
|
|
1409
1519
|
currency: this.options.currency,
|
|
@@ -1414,16 +1524,216 @@ class VoxePayModal {
|
|
|
1414
1524
|
this.state.isSuccess = true;
|
|
1415
1525
|
this.renderSuccessView();
|
|
1416
1526
|
this.options.onSuccess(result);
|
|
1527
|
+
return;
|
|
1417
1528
|
}
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1529
|
+
// Show polling UI
|
|
1530
|
+
this.renderPollingView();
|
|
1531
|
+
this.startStatusPolling();
|
|
1532
|
+
}
|
|
1533
|
+
/**
|
|
1534
|
+
* Render the "waiting for payment confirmation" polling view
|
|
1535
|
+
*/
|
|
1536
|
+
renderPollingView() {
|
|
1537
|
+
var _a;
|
|
1538
|
+
const formContainer = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#voxepay-form-container');
|
|
1539
|
+
if (!formContainer)
|
|
1540
|
+
return;
|
|
1541
|
+
const formattedAmount = formatAmount(this.options.amount, this.options.currency);
|
|
1542
|
+
formContainer.innerHTML = `
|
|
1543
|
+
<div class="voxepay-transfer-view">
|
|
1544
|
+
<div style="text-align: center; padding: 32px 16px;">
|
|
1545
|
+
<div class="voxepay-spinner" style="width: 40px; height: 40px; margin: 0 auto 20px; border-width: 3px;"></div>
|
|
1546
|
+
<h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 8px; color: var(--voxepay-text);">Waiting for Payment</h3>
|
|
1547
|
+
<p style="font-size: 0.875rem; color: var(--voxepay-text-muted); margin-bottom: 4px;">
|
|
1548
|
+
We're checking for your transfer of <strong>${formattedAmount}</strong>
|
|
1549
|
+
</p>
|
|
1550
|
+
<p style="font-size: 0.813rem; color: var(--voxepay-text-subtle); margin-bottom: 24px;" id="voxepay-polling-status">
|
|
1551
|
+
This usually takes a few seconds...
|
|
1552
|
+
</p>
|
|
1553
|
+
<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;">
|
|
1554
|
+
<span>Cancel</span>
|
|
1555
|
+
</button>
|
|
1556
|
+
</div>
|
|
1557
|
+
</div>
|
|
1558
|
+
`;
|
|
1559
|
+
const cancelBtn = formContainer.querySelector('#voxepay-cancel-polling');
|
|
1560
|
+
cancelBtn === null || cancelBtn === void 0 ? void 0 : cancelBtn.addEventListener('click', () => {
|
|
1561
|
+
this.stopStatusPolling();
|
|
1562
|
+
// Re-render bank transfer view so user can try again
|
|
1563
|
+
if (this.state.bankTransferDetails) {
|
|
1564
|
+
const amt = formatAmount(this.options.amount, this.options.currency);
|
|
1565
|
+
formContainer.innerHTML = this.getBankTransferHTML(amt);
|
|
1566
|
+
this.attachBankTransferListeners();
|
|
1567
|
+
this.startTransferTimer();
|
|
1568
|
+
}
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1571
|
+
/**
|
|
1572
|
+
* Start polling payment status every 5 seconds
|
|
1573
|
+
*/
|
|
1574
|
+
startStatusPolling() {
|
|
1575
|
+
this.stopStatusPolling(); // clear any existing poll
|
|
1576
|
+
const poll = async () => {
|
|
1577
|
+
var _a;
|
|
1578
|
+
if (!this.apiClient || !this.state.transactionRef)
|
|
1579
|
+
return;
|
|
1580
|
+
try {
|
|
1581
|
+
const statusData = await this.apiClient.getPaymentStatus(this.state.transactionRef);
|
|
1582
|
+
this.handlePollingStatusUpdate(statusData.status, statusData);
|
|
1583
|
+
}
|
|
1584
|
+
catch (error) {
|
|
1585
|
+
console.error('[VoxePay] Status polling error:', error);
|
|
1586
|
+
// Don't stop polling on transient errors — let it retry
|
|
1587
|
+
const statusEl = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#voxepay-polling-status');
|
|
1588
|
+
if (statusEl) {
|
|
1589
|
+
statusEl.textContent = 'Still checking... Please wait.';
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
};
|
|
1593
|
+
// Initial check immediately
|
|
1594
|
+
poll();
|
|
1595
|
+
// Then poll every 5 seconds
|
|
1596
|
+
this.statusPollingInterval = window.setInterval(poll, 5000);
|
|
1597
|
+
// Auto-stop after 30 minutes
|
|
1598
|
+
this.statusPollingTimeout = window.setTimeout(() => {
|
|
1599
|
+
this.stopStatusPolling();
|
|
1600
|
+
this.handlePollingStatusUpdate('EXPIRED', null);
|
|
1601
|
+
}, 30 * 60 * 1000);
|
|
1602
|
+
}
|
|
1603
|
+
/**
|
|
1604
|
+
* Stop payment status polling
|
|
1605
|
+
*/
|
|
1606
|
+
stopStatusPolling() {
|
|
1607
|
+
if (this.statusPollingInterval) {
|
|
1608
|
+
clearInterval(this.statusPollingInterval);
|
|
1609
|
+
this.statusPollingInterval = null;
|
|
1610
|
+
}
|
|
1611
|
+
if (this.statusPollingTimeout) {
|
|
1612
|
+
clearTimeout(this.statusPollingTimeout);
|
|
1613
|
+
this.statusPollingTimeout = null;
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
/**
|
|
1617
|
+
* Handle status updates from polling
|
|
1618
|
+
*/
|
|
1619
|
+
handlePollingStatusUpdate(status, data) {
|
|
1620
|
+
var _a;
|
|
1621
|
+
const terminalStatuses = ['COMPLETED', 'FAILED', 'EXPIRED', 'CANCELLED'];
|
|
1622
|
+
if (terminalStatuses.includes(status)) {
|
|
1623
|
+
this.stopStatusPolling();
|
|
1624
|
+
this.stopTransferTimer();
|
|
1625
|
+
}
|
|
1626
|
+
switch (status) {
|
|
1627
|
+
case 'COMPLETED': {
|
|
1628
|
+
const result = {
|
|
1629
|
+
id: (data === null || data === void 0 ? void 0 : data.id) || this.state.paymentId || `pay_transfer_${Date.now()}`,
|
|
1630
|
+
status: 'success',
|
|
1631
|
+
amount: this.options.amount,
|
|
1632
|
+
currency: this.options.currency,
|
|
1633
|
+
timestamp: (data === null || data === void 0 ? void 0 : data.completedAt) || new Date().toISOString(),
|
|
1634
|
+
reference: this.state.transactionRef || undefined,
|
|
1635
|
+
paymentMethod: 'bank_transfer',
|
|
1636
|
+
data: data || undefined,
|
|
1637
|
+
};
|
|
1638
|
+
this.state.isSuccess = true;
|
|
1639
|
+
this.renderSuccessView();
|
|
1640
|
+
this.options.onSuccess(result);
|
|
1641
|
+
break;
|
|
1642
|
+
}
|
|
1643
|
+
case 'PAYMENT_RECEIVED': {
|
|
1644
|
+
// Almost done — update the polling UI message
|
|
1645
|
+
const statusEl = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#voxepay-polling-status');
|
|
1646
|
+
if (statusEl) {
|
|
1647
|
+
statusEl.textContent = 'Payment received! Processing...';
|
|
1648
|
+
}
|
|
1649
|
+
break;
|
|
1650
|
+
}
|
|
1651
|
+
case 'PARTIAL_PAYMENT': {
|
|
1652
|
+
this.stopStatusPolling();
|
|
1653
|
+
this.renderTransferErrorView('Partial Payment Received', 'The amount transferred is less than the required amount. Please transfer the remaining balance or contact support.');
|
|
1654
|
+
break;
|
|
1655
|
+
}
|
|
1656
|
+
case 'OVERPAID': {
|
|
1657
|
+
// Overpayment is still successful — show success but note the overpayment
|
|
1658
|
+
const result = {
|
|
1659
|
+
id: (data === null || data === void 0 ? void 0 : data.id) || this.state.paymentId || `pay_transfer_${Date.now()}`,
|
|
1660
|
+
status: 'success',
|
|
1661
|
+
amount: this.options.amount,
|
|
1662
|
+
currency: this.options.currency,
|
|
1663
|
+
timestamp: (data === null || data === void 0 ? void 0 : data.completedAt) || new Date().toISOString(),
|
|
1664
|
+
reference: this.state.transactionRef || undefined,
|
|
1665
|
+
paymentMethod: 'bank_transfer',
|
|
1666
|
+
data: { ...data, overpaid: true },
|
|
1667
|
+
};
|
|
1668
|
+
this.state.isSuccess = true;
|
|
1669
|
+
this.renderSuccessView();
|
|
1670
|
+
this.options.onSuccess(result);
|
|
1671
|
+
break;
|
|
1672
|
+
}
|
|
1673
|
+
case 'EXPIRED': {
|
|
1674
|
+
this.renderTransferErrorView('Payment Expired', 'The virtual account has expired. Please try again to generate a new account.');
|
|
1675
|
+
this.options.onError({
|
|
1676
|
+
code: 'ACCOUNT_EXPIRED',
|
|
1677
|
+
message: 'Virtual account expired before payment was received.',
|
|
1678
|
+
recoverable: true,
|
|
1679
|
+
});
|
|
1680
|
+
break;
|
|
1681
|
+
}
|
|
1682
|
+
case 'FAILED': {
|
|
1683
|
+
this.renderTransferErrorView('Payment Failed', (data === null || data === void 0 ? void 0 : data.message) || 'The payment could not be processed. Please try again.');
|
|
1684
|
+
this.options.onError({
|
|
1685
|
+
code: 'PAYMENT_FAILED',
|
|
1686
|
+
message: (data === null || data === void 0 ? void 0 : data.message) || 'Payment failed.',
|
|
1687
|
+
recoverable: true,
|
|
1688
|
+
});
|
|
1689
|
+
break;
|
|
1690
|
+
}
|
|
1691
|
+
case 'CANCELLED': {
|
|
1692
|
+
this.renderTransferErrorView('Payment Cancelled', 'This payment has been cancelled.');
|
|
1693
|
+
this.options.onError({
|
|
1694
|
+
code: 'PAYMENT_CANCELLED',
|
|
1695
|
+
message: 'Payment was cancelled.',
|
|
1696
|
+
recoverable: false,
|
|
1697
|
+
});
|
|
1698
|
+
break;
|
|
1422
1699
|
}
|
|
1423
|
-
const paymentError = error;
|
|
1424
|
-
this.options.onError(paymentError);
|
|
1425
1700
|
}
|
|
1426
1701
|
}
|
|
1702
|
+
/**
|
|
1703
|
+
* Render error view for transfer issues with retry option
|
|
1704
|
+
*/
|
|
1705
|
+
renderTransferErrorView(title, message) {
|
|
1706
|
+
var _a;
|
|
1707
|
+
const formContainer = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#voxepay-form-container');
|
|
1708
|
+
if (!formContainer)
|
|
1709
|
+
return;
|
|
1710
|
+
formContainer.innerHTML = `
|
|
1711
|
+
<div class="voxepay-transfer-view">
|
|
1712
|
+
<div style="text-align: center; padding: 40px 16px;">
|
|
1713
|
+
<div style="font-size: 2.5rem; margin-bottom: 16px;">❌</div>
|
|
1714
|
+
<h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 8px; color: var(--voxepay-text);">${title}</h3>
|
|
1715
|
+
<p style="font-size: 0.875rem; color: var(--voxepay-text-muted); margin-bottom: 24px;">${message}</p>
|
|
1716
|
+
<button class="voxepay-submit-btn" id="voxepay-retry-transfer"><span>Try Again</span></button>
|
|
1717
|
+
</div>
|
|
1718
|
+
</div>
|
|
1719
|
+
`;
|
|
1720
|
+
const retryBtn = formContainer.querySelector('#voxepay-retry-transfer');
|
|
1721
|
+
retryBtn === null || retryBtn === void 0 ? void 0 : retryBtn.addEventListener('click', () => {
|
|
1722
|
+
// Reset state for new attempt
|
|
1723
|
+
this.state.bankTransferDetails = null;
|
|
1724
|
+
this.state.transactionRef = null;
|
|
1725
|
+
this.state.paymentId = null;
|
|
1726
|
+
formContainer.innerHTML = `
|
|
1727
|
+
<div class="voxepay-transfer-view">
|
|
1728
|
+
<div class="voxepay-transfer-loading">
|
|
1729
|
+
<div class="voxepay-spinner"></div>
|
|
1730
|
+
<p style="margin-top: 16px; color: var(--voxepay-text-muted);">Generating account details...</p>
|
|
1731
|
+
</div>
|
|
1732
|
+
</div>
|
|
1733
|
+
`;
|
|
1734
|
+
this.loadBankTransferDetails();
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1427
1737
|
/**
|
|
1428
1738
|
* Start the transfer countdown timer
|
|
1429
1739
|
*/
|