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