@voxepay/checkout 0.1.0 → 0.2.1
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/components/modal.d.ts +49 -1
- package/dist/index.cjs.js +402 -77
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.esm.js +402 -77
- package/dist/index.esm.js.map +1 -1
- package/dist/types.d.ts +25 -0
- package/dist/voxepay-checkout.min.js +1 -1
- package/dist/voxepay-checkout.min.js.map +1 -1
- package/package.json +2 -2
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* VoxePay Checkout Modal Component
|
|
3
3
|
* A modern, glassmorphism payment modal
|
|
4
4
|
*/
|
|
5
|
-
import type { CheckoutOptions } from '../types';
|
|
5
|
+
import type { CheckoutOptions, BankTransferDetails, PaymentMethod } from '../types';
|
|
6
6
|
export interface ModalState {
|
|
7
7
|
cardNumber: string;
|
|
8
8
|
expiry: string;
|
|
@@ -20,6 +20,10 @@ export interface ModalState {
|
|
|
20
20
|
otpTimer: number;
|
|
21
21
|
canResendOtp: boolean;
|
|
22
22
|
otpTimerInterval: number | null;
|
|
23
|
+
paymentMethod: PaymentMethod;
|
|
24
|
+
transferTimer: number;
|
|
25
|
+
transferTimerInterval: number | null;
|
|
26
|
+
bankTransferDetails: BankTransferDetails | null;
|
|
23
27
|
}
|
|
24
28
|
export declare class VoxePayModal {
|
|
25
29
|
private options;
|
|
@@ -43,10 +47,26 @@ export declare class VoxePayModal {
|
|
|
43
47
|
* Render the modal HTML
|
|
44
48
|
*/
|
|
45
49
|
private render;
|
|
50
|
+
/**
|
|
51
|
+
* Get available payment methods
|
|
52
|
+
*/
|
|
53
|
+
private getPaymentMethods;
|
|
46
54
|
/**
|
|
47
55
|
* Get modal HTML
|
|
48
56
|
*/
|
|
49
57
|
private getModalHTML;
|
|
58
|
+
/**
|
|
59
|
+
* Get card form HTML
|
|
60
|
+
*/
|
|
61
|
+
private getCardFormHTML;
|
|
62
|
+
/**
|
|
63
|
+
* Get bank transfer HTML
|
|
64
|
+
*/
|
|
65
|
+
private getBankTransferHTML;
|
|
66
|
+
/**
|
|
67
|
+
* Format countdown seconds to MM:SS
|
|
68
|
+
*/
|
|
69
|
+
private formatCountdown;
|
|
50
70
|
/**
|
|
51
71
|
* Render success view
|
|
52
72
|
*/
|
|
@@ -73,6 +93,34 @@ export declare class VoxePayModal {
|
|
|
73
93
|
* Attach event listeners
|
|
74
94
|
*/
|
|
75
95
|
private attachEventListeners;
|
|
96
|
+
/**
|
|
97
|
+
* Attach bank transfer specific event listeners
|
|
98
|
+
*/
|
|
99
|
+
private attachBankTransferListeners;
|
|
100
|
+
/**
|
|
101
|
+
* Switch between payment methods
|
|
102
|
+
*/
|
|
103
|
+
private switchPaymentMethod;
|
|
104
|
+
/**
|
|
105
|
+
* Load bank transfer details (from callback or generate mock)
|
|
106
|
+
*/
|
|
107
|
+
private loadBankTransferDetails;
|
|
108
|
+
/**
|
|
109
|
+
* Copy text to clipboard with visual feedback
|
|
110
|
+
*/
|
|
111
|
+
private copyToClipboard;
|
|
112
|
+
/**
|
|
113
|
+
* Handle "I've sent the money" confirmation
|
|
114
|
+
*/
|
|
115
|
+
private handleTransferConfirm;
|
|
116
|
+
/**
|
|
117
|
+
* Start the transfer countdown timer
|
|
118
|
+
*/
|
|
119
|
+
private startTransferTimer;
|
|
120
|
+
/**
|
|
121
|
+
* Stop the transfer countdown timer
|
|
122
|
+
*/
|
|
123
|
+
private stopTransferTimer;
|
|
76
124
|
private handleEscape;
|
|
77
125
|
private handleCardInput;
|
|
78
126
|
private handleExpiryInput;
|
package/dist/index.cjs.js
CHANGED
|
@@ -231,6 +231,18 @@ const ICONS = {
|
|
|
231
231
|
error: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20" stroke="currentColor" stroke-width="2">
|
|
232
232
|
<path stroke-linecap="round" stroke-linejoin="round" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" />
|
|
233
233
|
</svg>`,
|
|
234
|
+
copy: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
235
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
|
236
|
+
</svg>`,
|
|
237
|
+
bank: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
238
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M3 21h18M3 10h18M5 6l7-3 7 3M4 10v11M20 10v11M8 14v3M12 14v3M16 14v3" />
|
|
239
|
+
</svg>`,
|
|
240
|
+
card: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
241
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
|
|
242
|
+
</svg>`,
|
|
243
|
+
clock: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
244
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
245
|
+
</svg>`,
|
|
234
246
|
};
|
|
235
247
|
// Card brand logos
|
|
236
248
|
const CARD_BRAND_DISPLAY = {
|
|
@@ -251,6 +263,7 @@ class VoxePayModal {
|
|
|
251
263
|
}
|
|
252
264
|
};
|
|
253
265
|
this.options = options;
|
|
266
|
+
const methods = options.paymentMethods || ['card', 'bank_transfer'];
|
|
254
267
|
this.state = {
|
|
255
268
|
cardNumber: '',
|
|
256
269
|
expiry: '',
|
|
@@ -263,6 +276,10 @@ class VoxePayModal {
|
|
|
263
276
|
otpTimer: 60,
|
|
264
277
|
canResendOtp: false,
|
|
265
278
|
otpTimerInterval: null,
|
|
279
|
+
paymentMethod: methods[0],
|
|
280
|
+
transferTimer: 0,
|
|
281
|
+
transferTimerInterval: null,
|
|
282
|
+
bankTransferDetails: options.bankTransferDetails || null,
|
|
266
283
|
};
|
|
267
284
|
}
|
|
268
285
|
/**
|
|
@@ -283,6 +300,7 @@ class VoxePayModal {
|
|
|
283
300
|
*/
|
|
284
301
|
close() {
|
|
285
302
|
var _a;
|
|
303
|
+
this.stopTransferTimer();
|
|
286
304
|
(_a = this.overlay) === null || _a === void 0 ? void 0 : _a.classList.remove('voxepay-visible');
|
|
287
305
|
setTimeout(() => {
|
|
288
306
|
var _a, _b, _c;
|
|
@@ -313,11 +331,20 @@ class VoxePayModal {
|
|
|
313
331
|
document.body.appendChild(this.container);
|
|
314
332
|
this.overlay = this.container.querySelector('.voxepay-overlay');
|
|
315
333
|
}
|
|
334
|
+
/**
|
|
335
|
+
* Get available payment methods
|
|
336
|
+
*/
|
|
337
|
+
getPaymentMethods() {
|
|
338
|
+
return this.options.paymentMethods || ['card', 'bank_transfer'];
|
|
339
|
+
}
|
|
316
340
|
/**
|
|
317
341
|
* Get modal HTML
|
|
318
342
|
*/
|
|
319
343
|
getModalHTML() {
|
|
320
344
|
const formattedAmount = formatAmount(this.options.amount, this.options.currency);
|
|
345
|
+
const methods = this.getPaymentMethods();
|
|
346
|
+
const showTabs = methods.length > 1;
|
|
347
|
+
const isCard = this.state.paymentMethod === 'card';
|
|
321
348
|
return `
|
|
322
349
|
<div class="voxepay-overlay">
|
|
323
350
|
<div class="voxepay-modal" role="dialog" aria-modal="true" aria-labelledby="voxepay-title">
|
|
@@ -333,71 +360,21 @@ class VoxePayModal {
|
|
|
333
360
|
${ICONS.close}
|
|
334
361
|
</button>
|
|
335
362
|
</div>
|
|
363
|
+
|
|
364
|
+
${showTabs ? `
|
|
365
|
+
<div class="voxepay-method-tabs">
|
|
366
|
+
${methods.includes('card') ? `
|
|
367
|
+
<button class="voxepay-method-tab ${isCard ? 'active' : ''}" data-method="card">
|
|
368
|
+
${ICONS.card} <span>Card</span>
|
|
369
|
+
</button>` : ''}
|
|
370
|
+
${methods.includes('bank_transfer') ? `
|
|
371
|
+
<button class="voxepay-method-tab ${!isCard ? 'active' : ''}" data-method="bank_transfer">
|
|
372
|
+
${ICONS.bank} <span>Bank Transfer</span>
|
|
373
|
+
</button>` : ''}
|
|
374
|
+
</div>` : ''}
|
|
336
375
|
|
|
337
376
|
<div class="voxepay-body" id="voxepay-form-container">
|
|
338
|
-
|
|
339
|
-
<div class="voxepay-form-group">
|
|
340
|
-
<label class="voxepay-label">
|
|
341
|
-
<span class="voxepay-label-icon">💳</span>
|
|
342
|
-
Card Number
|
|
343
|
-
</label>
|
|
344
|
-
<div class="voxepay-card-input-wrapper">
|
|
345
|
-
<input
|
|
346
|
-
type="text"
|
|
347
|
-
class="voxepay-input"
|
|
348
|
-
id="voxepay-card-number"
|
|
349
|
-
name="cardNumber"
|
|
350
|
-
placeholder="1234 5678 9012 3456"
|
|
351
|
-
autocomplete="cc-number"
|
|
352
|
-
inputmode="numeric"
|
|
353
|
-
/>
|
|
354
|
-
<div class="voxepay-card-brand" id="voxepay-card-brand"></div>
|
|
355
|
-
</div>
|
|
356
|
-
<div class="voxepay-error-message" id="voxepay-card-error" style="display: none;"></div>
|
|
357
|
-
</div>
|
|
358
|
-
|
|
359
|
-
<div class="voxepay-row">
|
|
360
|
-
<div class="voxepay-form-group">
|
|
361
|
-
<label class="voxepay-label">
|
|
362
|
-
<span class="voxepay-label-icon">📅</span>
|
|
363
|
-
Expiry
|
|
364
|
-
</label>
|
|
365
|
-
<input
|
|
366
|
-
type="text"
|
|
367
|
-
class="voxepay-input"
|
|
368
|
-
id="voxepay-expiry"
|
|
369
|
-
name="expiry"
|
|
370
|
-
placeholder="MM/YY"
|
|
371
|
-
autocomplete="cc-exp"
|
|
372
|
-
inputmode="numeric"
|
|
373
|
-
maxlength="5"
|
|
374
|
-
/>
|
|
375
|
-
<div class="voxepay-error-message" id="voxepay-expiry-error" style="display: none;"></div>
|
|
376
|
-
</div>
|
|
377
|
-
|
|
378
|
-
<div class="voxepay-form-group">
|
|
379
|
-
<label class="voxepay-label">
|
|
380
|
-
<span class="voxepay-label-icon">🔒</span>
|
|
381
|
-
CVV
|
|
382
|
-
</label>
|
|
383
|
-
<input
|
|
384
|
-
type="text"
|
|
385
|
-
class="voxepay-input"
|
|
386
|
-
id="voxepay-cvv"
|
|
387
|
-
name="cvv"
|
|
388
|
-
placeholder="•••"
|
|
389
|
-
autocomplete="cc-csc"
|
|
390
|
-
inputmode="numeric"
|
|
391
|
-
maxlength="4"
|
|
392
|
-
/>
|
|
393
|
-
<div class="voxepay-error-message" id="voxepay-cvv-error" style="display: none;"></div>
|
|
394
|
-
</div>
|
|
395
|
-
</div>
|
|
396
|
-
|
|
397
|
-
<button type="submit" class="voxepay-submit-btn" id="voxepay-submit">
|
|
398
|
-
<span>Pay Now ${formattedAmount}</span>
|
|
399
|
-
</button>
|
|
400
|
-
</form>
|
|
377
|
+
${isCard ? this.getCardFormHTML(formattedAmount) : this.getBankTransferHTML(formattedAmount)}
|
|
401
378
|
</div>
|
|
402
379
|
|
|
403
380
|
<div class="voxepay-footer">
|
|
@@ -410,6 +387,118 @@ class VoxePayModal {
|
|
|
410
387
|
</div>
|
|
411
388
|
`;
|
|
412
389
|
}
|
|
390
|
+
/**
|
|
391
|
+
* Get card form HTML
|
|
392
|
+
*/
|
|
393
|
+
getCardFormHTML(formattedAmount) {
|
|
394
|
+
return `
|
|
395
|
+
<form id="voxepay-payment-form" novalidate>
|
|
396
|
+
<div class="voxepay-form-group">
|
|
397
|
+
<label class="voxepay-label">
|
|
398
|
+
<span class="voxepay-label-icon">💳</span>
|
|
399
|
+
Card Number
|
|
400
|
+
</label>
|
|
401
|
+
<div class="voxepay-card-input-wrapper">
|
|
402
|
+
<input type="text" class="voxepay-input" id="voxepay-card-number" name="cardNumber"
|
|
403
|
+
placeholder="1234 5678 9012 3456" autocomplete="cc-number" inputmode="numeric" />
|
|
404
|
+
<div class="voxepay-card-brand" id="voxepay-card-brand"></div>
|
|
405
|
+
</div>
|
|
406
|
+
<div class="voxepay-error-message" id="voxepay-card-error" style="display: none;"></div>
|
|
407
|
+
</div>
|
|
408
|
+
|
|
409
|
+
<div class="voxepay-row">
|
|
410
|
+
<div class="voxepay-form-group">
|
|
411
|
+
<label class="voxepay-label"><span class="voxepay-label-icon">📅</span> Expiry</label>
|
|
412
|
+
<input type="text" class="voxepay-input" id="voxepay-expiry" name="expiry"
|
|
413
|
+
placeholder="MM/YY" autocomplete="cc-exp" inputmode="numeric" maxlength="5" />
|
|
414
|
+
<div class="voxepay-error-message" id="voxepay-expiry-error" style="display: none;"></div>
|
|
415
|
+
</div>
|
|
416
|
+
<div class="voxepay-form-group">
|
|
417
|
+
<label class="voxepay-label"><span class="voxepay-label-icon">🔒</span> CVV</label>
|
|
418
|
+
<input type="text" class="voxepay-input" id="voxepay-cvv" name="cvv"
|
|
419
|
+
placeholder="•••" autocomplete="cc-csc" inputmode="numeric" maxlength="4" />
|
|
420
|
+
<div class="voxepay-error-message" id="voxepay-cvv-error" style="display: none;"></div>
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
|
|
424
|
+
<button type="submit" class="voxepay-submit-btn" id="voxepay-submit">
|
|
425
|
+
<span>Pay Now ${formattedAmount}</span>
|
|
426
|
+
</button>
|
|
427
|
+
</form>
|
|
428
|
+
`;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Get bank transfer HTML
|
|
432
|
+
*/
|
|
433
|
+
getBankTransferHTML(formattedAmount) {
|
|
434
|
+
const details = this.state.bankTransferDetails;
|
|
435
|
+
if (!details) {
|
|
436
|
+
return `
|
|
437
|
+
<div class="voxepay-transfer-view">
|
|
438
|
+
<div class="voxepay-transfer-loading">
|
|
439
|
+
<div class="voxepay-spinner"></div>
|
|
440
|
+
<p style="margin-top: 16px; color: var(--voxepay-text-muted);">Generating account details...</p>
|
|
441
|
+
</div>
|
|
442
|
+
</div>
|
|
443
|
+
`;
|
|
444
|
+
}
|
|
445
|
+
return `
|
|
446
|
+
<div class="voxepay-transfer-view">
|
|
447
|
+
<div class="voxepay-transfer-instruction">
|
|
448
|
+
<p>Transfer <strong>${formattedAmount}</strong> to the account below</p>
|
|
449
|
+
</div>
|
|
450
|
+
|
|
451
|
+
<div class="voxepay-transfer-details">
|
|
452
|
+
<div class="voxepay-transfer-detail">
|
|
453
|
+
<span class="voxepay-transfer-label">Account Number</span>
|
|
454
|
+
<div class="voxepay-transfer-value-row">
|
|
455
|
+
<span class="voxepay-transfer-value voxepay-transfer-account" id="voxepay-account-number">${details.accountNumber}</span>
|
|
456
|
+
<button class="voxepay-copy-btn" id="voxepay-copy-btn" data-copy="${details.accountNumber}" title="Copy">
|
|
457
|
+
${ICONS.copy}
|
|
458
|
+
</button>
|
|
459
|
+
</div>
|
|
460
|
+
</div>
|
|
461
|
+
|
|
462
|
+
<div class="voxepay-transfer-detail">
|
|
463
|
+
<span class="voxepay-transfer-label">Bank Name</span>
|
|
464
|
+
<span class="voxepay-transfer-value">${details.bankName}</span>
|
|
465
|
+
</div>
|
|
466
|
+
|
|
467
|
+
<div class="voxepay-transfer-detail">
|
|
468
|
+
<span class="voxepay-transfer-label">Account Name</span>
|
|
469
|
+
<span class="voxepay-transfer-value">${details.accountName}</span>
|
|
470
|
+
</div>
|
|
471
|
+
|
|
472
|
+
<div class="voxepay-transfer-detail">
|
|
473
|
+
<span class="voxepay-transfer-label">Amount</span>
|
|
474
|
+
<span class="voxepay-transfer-value voxepay-transfer-amount">${formattedAmount}</span>
|
|
475
|
+
</div>
|
|
476
|
+
|
|
477
|
+
<div class="voxepay-transfer-detail">
|
|
478
|
+
<span class="voxepay-transfer-label">Reference</span>
|
|
479
|
+
<span class="voxepay-transfer-value" style="font-family: monospace; letter-spacing: 1px;">${details.reference}</span>
|
|
480
|
+
</div>
|
|
481
|
+
</div>
|
|
482
|
+
|
|
483
|
+
<div class="voxepay-transfer-timer" id="voxepay-transfer-timer">
|
|
484
|
+
${ICONS.clock}
|
|
485
|
+
<span>Account expires in <strong id="voxepay-transfer-countdown">${this.formatCountdown(details.expiresIn)}</strong></span>
|
|
486
|
+
</div>
|
|
487
|
+
|
|
488
|
+
<button type="button" class="voxepay-submit-btn" id="voxepay-transfer-confirm">
|
|
489
|
+
<span>I've sent the money</span>
|
|
490
|
+
</button>
|
|
491
|
+
</div>
|
|
492
|
+
`;
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Format countdown seconds to MM:SS
|
|
496
|
+
*/
|
|
497
|
+
formatCountdown(seconds) {
|
|
498
|
+
const m = Math.floor(seconds / 60);
|
|
499
|
+
const s = seconds % 60;
|
|
500
|
+
return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
|
|
501
|
+
}
|
|
413
502
|
/**
|
|
414
503
|
* Render success view
|
|
415
504
|
*/
|
|
@@ -650,8 +739,8 @@ class VoxePayModal {
|
|
|
650
739
|
* Attach event listeners
|
|
651
740
|
*/
|
|
652
741
|
attachEventListeners() {
|
|
653
|
-
var _a, _b, _c, _d, _e, _f;
|
|
654
|
-
const closeBtn = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('[data-action
|
|
742
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
743
|
+
const closeBtn = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('[data-action=\"close\"]');
|
|
655
744
|
closeBtn === null || closeBtn === void 0 ? void 0 : closeBtn.addEventListener('click', () => this.close());
|
|
656
745
|
(_b = this.overlay) === null || _b === void 0 ? void 0 : _b.addEventListener('click', (e) => {
|
|
657
746
|
if (e.target === this.overlay) {
|
|
@@ -659,17 +748,221 @@ class VoxePayModal {
|
|
|
659
748
|
}
|
|
660
749
|
});
|
|
661
750
|
document.addEventListener('keydown', this.handleEscape);
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
751
|
+
// Payment method tabs
|
|
752
|
+
const tabs = (_c = this.container) === null || _c === void 0 ? void 0 : _c.querySelectorAll('.voxepay-method-tab');
|
|
753
|
+
tabs === null || tabs === void 0 ? void 0 : tabs.forEach(tab => {
|
|
754
|
+
tab.addEventListener('click', () => {
|
|
755
|
+
const method = tab.dataset.method;
|
|
756
|
+
if (method && method !== this.state.paymentMethod) {
|
|
757
|
+
this.switchPaymentMethod(method);
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
});
|
|
761
|
+
// Card-specific listeners
|
|
762
|
+
if (this.state.paymentMethod === 'card') {
|
|
763
|
+
const cardInput = (_d = this.container) === null || _d === void 0 ? void 0 : _d.querySelector('#voxepay-card-number');
|
|
764
|
+
cardInput === null || cardInput === void 0 ? void 0 : cardInput.addEventListener('input', (e) => this.handleCardInput(e));
|
|
765
|
+
cardInput === null || cardInput === void 0 ? void 0 : cardInput.addEventListener('blur', () => this.validateField('cardNumber'));
|
|
766
|
+
const expiryInput = (_e = this.container) === null || _e === void 0 ? void 0 : _e.querySelector('#voxepay-expiry');
|
|
767
|
+
expiryInput === null || expiryInput === void 0 ? void 0 : expiryInput.addEventListener('input', (e) => this.handleExpiryInput(e));
|
|
768
|
+
expiryInput === null || expiryInput === void 0 ? void 0 : expiryInput.addEventListener('blur', () => this.validateField('expiry'));
|
|
769
|
+
const cvvInput = (_f = this.container) === null || _f === void 0 ? void 0 : _f.querySelector('#voxepay-cvv');
|
|
770
|
+
cvvInput === null || cvvInput === void 0 ? void 0 : cvvInput.addEventListener('input', (e) => this.handleCVVInput(e));
|
|
771
|
+
cvvInput === null || cvvInput === void 0 ? void 0 : cvvInput.addEventListener('blur', () => this.validateField('cvv'));
|
|
772
|
+
const form = (_g = this.container) === null || _g === void 0 ? void 0 : _g.querySelector('#voxepay-payment-form');
|
|
773
|
+
form === null || form === void 0 ? void 0 : form.addEventListener('submit', (e) => this.handleSubmit(e));
|
|
774
|
+
}
|
|
775
|
+
// Bank transfer-specific listeners
|
|
776
|
+
if (this.state.paymentMethod === 'bank_transfer') {
|
|
777
|
+
this.attachBankTransferListeners();
|
|
778
|
+
// If no details yet, load them
|
|
779
|
+
if (!this.state.bankTransferDetails) {
|
|
780
|
+
this.loadBankTransferDetails();
|
|
781
|
+
}
|
|
782
|
+
else {
|
|
783
|
+
this.startTransferTimer();
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Attach bank transfer specific event listeners
|
|
789
|
+
*/
|
|
790
|
+
attachBankTransferListeners() {
|
|
791
|
+
var _a, _b;
|
|
792
|
+
const copyBtn = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#voxepay-copy-btn');
|
|
793
|
+
copyBtn === null || copyBtn === void 0 ? void 0 : copyBtn.addEventListener('click', () => {
|
|
794
|
+
const accountNum = copyBtn.dataset.copy || '';
|
|
795
|
+
this.copyToClipboard(accountNum);
|
|
796
|
+
});
|
|
797
|
+
const confirmBtn = (_b = this.container) === null || _b === void 0 ? void 0 : _b.querySelector('#voxepay-transfer-confirm');
|
|
798
|
+
confirmBtn === null || confirmBtn === void 0 ? void 0 : confirmBtn.addEventListener('click', () => this.handleTransferConfirm());
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Switch between payment methods
|
|
802
|
+
*/
|
|
803
|
+
switchPaymentMethod(method) {
|
|
804
|
+
var _a;
|
|
805
|
+
this.stopTransferTimer();
|
|
806
|
+
this.state.paymentMethod = method;
|
|
807
|
+
// Re-render the whole modal
|
|
808
|
+
if (this.container) {
|
|
809
|
+
this.container.innerHTML = this.getModalHTML();
|
|
810
|
+
this.overlay = this.container.querySelector('.voxepay-overlay');
|
|
811
|
+
(_a = this.overlay) === null || _a === void 0 ? void 0 : _a.classList.add('voxepay-visible');
|
|
812
|
+
this.attachEventListeners();
|
|
813
|
+
// Restore card values if switching back to card
|
|
814
|
+
if (method === 'card' && this.state.cardNumber) {
|
|
815
|
+
const cardInput = this.container.querySelector('#voxepay-card-number');
|
|
816
|
+
const expiryInput = this.container.querySelector('#voxepay-expiry');
|
|
817
|
+
const cvvInput = this.container.querySelector('#voxepay-cvv');
|
|
818
|
+
if (cardInput)
|
|
819
|
+
cardInput.value = formatCardNumber(this.state.cardNumber);
|
|
820
|
+
if (expiryInput)
|
|
821
|
+
expiryInput.value = this.state.expiry;
|
|
822
|
+
if (cvvInput)
|
|
823
|
+
cvvInput.value = this.state.cvv;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Load bank transfer details (from callback or generate mock)
|
|
829
|
+
*/
|
|
830
|
+
async loadBankTransferDetails() {
|
|
831
|
+
var _a;
|
|
832
|
+
try {
|
|
833
|
+
let details;
|
|
834
|
+
if (this.options.onBankTransferRequested) {
|
|
835
|
+
details = await this.options.onBankTransferRequested();
|
|
836
|
+
}
|
|
837
|
+
else {
|
|
838
|
+
// Mock/default bank details for testing
|
|
839
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
840
|
+
details = {
|
|
841
|
+
accountNumber: '0123456789',
|
|
842
|
+
bankName: 'VoxePay Bank',
|
|
843
|
+
accountName: 'VoxePay Collections',
|
|
844
|
+
reference: `VP-${Date.now().toString(36).toUpperCase()}`,
|
|
845
|
+
expiresIn: 1800, // 30 minutes
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
this.state.bankTransferDetails = details;
|
|
849
|
+
// Re-render the bank transfer view
|
|
850
|
+
const formContainer = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#voxepay-form-container');
|
|
851
|
+
if (formContainer) {
|
|
852
|
+
const formattedAmount = formatAmount(this.options.amount, this.options.currency);
|
|
853
|
+
formContainer.innerHTML = this.getBankTransferHTML(formattedAmount);
|
|
854
|
+
this.attachBankTransferListeners();
|
|
855
|
+
this.startTransferTimer();
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
catch (error) {
|
|
859
|
+
this.options.onError({
|
|
860
|
+
code: 'BANK_TRANSFER_INIT_FAILED',
|
|
861
|
+
message: 'Could not generate bank transfer details. Please try again.',
|
|
862
|
+
recoverable: true,
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Copy text to clipboard with visual feedback
|
|
868
|
+
*/
|
|
869
|
+
async copyToClipboard(text) {
|
|
870
|
+
var _a;
|
|
871
|
+
try {
|
|
872
|
+
await navigator.clipboard.writeText(text);
|
|
873
|
+
const copyBtn = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#voxepay-copy-btn');
|
|
874
|
+
if (copyBtn) {
|
|
875
|
+
copyBtn.innerHTML = `${ICONS.check}`;
|
|
876
|
+
copyBtn.classList.add('voxepay-copied');
|
|
877
|
+
setTimeout(() => {
|
|
878
|
+
copyBtn.innerHTML = `${ICONS.copy}`;
|
|
879
|
+
copyBtn.classList.remove('voxepay-copied');
|
|
880
|
+
}, 2000);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
catch (_b) {
|
|
884
|
+
// Fallback for older browsers
|
|
885
|
+
const textarea = document.createElement('textarea');
|
|
886
|
+
textarea.value = text;
|
|
887
|
+
textarea.style.position = 'fixed';
|
|
888
|
+
textarea.style.opacity = '0';
|
|
889
|
+
document.body.appendChild(textarea);
|
|
890
|
+
textarea.select();
|
|
891
|
+
document.execCommand('copy');
|
|
892
|
+
document.body.removeChild(textarea);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Handle "I've sent the money" confirmation
|
|
897
|
+
*/
|
|
898
|
+
async handleTransferConfirm() {
|
|
899
|
+
var _a, _b;
|
|
900
|
+
const confirmBtn = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#voxepay-transfer-confirm');
|
|
901
|
+
if (confirmBtn) {
|
|
902
|
+
confirmBtn.disabled = true;
|
|
903
|
+
confirmBtn.innerHTML = '<div class="voxepay-spinner"></div><span>Confirming transfer...</span>';
|
|
904
|
+
}
|
|
905
|
+
this.stopTransferTimer();
|
|
906
|
+
try {
|
|
907
|
+
// Simulate transfer verification
|
|
908
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
909
|
+
const result = {
|
|
910
|
+
id: `pay_transfer_${Date.now()}`,
|
|
911
|
+
status: 'pending',
|
|
912
|
+
amount: this.options.amount,
|
|
913
|
+
currency: this.options.currency,
|
|
914
|
+
timestamp: new Date().toISOString(),
|
|
915
|
+
reference: (_b = this.state.bankTransferDetails) === null || _b === void 0 ? void 0 : _b.reference,
|
|
916
|
+
paymentMethod: 'bank_transfer',
|
|
917
|
+
};
|
|
918
|
+
this.state.isSuccess = true;
|
|
919
|
+
this.renderSuccessView();
|
|
920
|
+
this.options.onSuccess(result);
|
|
921
|
+
}
|
|
922
|
+
catch (error) {
|
|
923
|
+
if (confirmBtn) {
|
|
924
|
+
confirmBtn.disabled = false;
|
|
925
|
+
confirmBtn.innerHTML = "<span>I've sent the money</span>";
|
|
926
|
+
}
|
|
927
|
+
const paymentError = error;
|
|
928
|
+
this.options.onError(paymentError);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Start the transfer countdown timer
|
|
933
|
+
*/
|
|
934
|
+
startTransferTimer() {
|
|
935
|
+
if (!this.state.bankTransferDetails)
|
|
936
|
+
return;
|
|
937
|
+
this.state.transferTimer = this.state.bankTransferDetails.expiresIn;
|
|
938
|
+
this.state.transferTimerInterval = window.setInterval(() => {
|
|
939
|
+
var _a, _b, _c;
|
|
940
|
+
this.state.transferTimer--;
|
|
941
|
+
const countdownEl = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#voxepay-transfer-countdown');
|
|
942
|
+
if (countdownEl) {
|
|
943
|
+
countdownEl.textContent = this.formatCountdown(this.state.transferTimer);
|
|
944
|
+
}
|
|
945
|
+
if (this.state.transferTimer <= 0) {
|
|
946
|
+
this.stopTransferTimer();
|
|
947
|
+
// Show expired state
|
|
948
|
+
const timerEl = (_b = this.container) === null || _b === void 0 ? void 0 : _b.querySelector('#voxepay-transfer-timer');
|
|
949
|
+
if (timerEl) {
|
|
950
|
+
timerEl.innerHTML = `${ICONS.clock} <span style="color: var(--voxepay-error);">Account expired. Please try again.</span>`;
|
|
951
|
+
}
|
|
952
|
+
const confirmBtn = (_c = this.container) === null || _c === void 0 ? void 0 : _c.querySelector('#voxepay-transfer-confirm');
|
|
953
|
+
if (confirmBtn)
|
|
954
|
+
confirmBtn.disabled = true;
|
|
955
|
+
}
|
|
956
|
+
}, 1000);
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Stop the transfer countdown timer
|
|
960
|
+
*/
|
|
961
|
+
stopTransferTimer() {
|
|
962
|
+
if (this.state.transferTimerInterval) {
|
|
963
|
+
clearInterval(this.state.transferTimerInterval);
|
|
964
|
+
this.state.transferTimerInterval = null;
|
|
965
|
+
}
|
|
673
966
|
}
|
|
674
967
|
handleCardInput(e) {
|
|
675
968
|
var _a;
|
|
@@ -922,8 +1215,40 @@ class VoxePayModal {
|
|
|
922
1215
|
|
|
923
1216
|
.voxepay-back-btn { display: block; width: 100%; padding: 12px; background: transparent; border: none; color: var(--voxepay-text-muted); font-size: 0.875rem; cursor: pointer; margin-top: 12px; transition: color var(--voxepay-transition-fast); }
|
|
924
1217
|
.voxepay-back-btn:hover { color: var(--voxepay-primary); }
|
|
1218
|
+
|
|
1219
|
+
/* Payment Method Tabs */
|
|
1220
|
+
.voxepay-method-tabs { display: flex; border-bottom: 1px solid var(--voxepay-border); background: var(--voxepay-surface); }
|
|
1221
|
+
.voxepay-method-tab { flex: 1; display: flex; align-items: center; justify-content: center; gap: 6px; padding: 12px 12px; border: none; background: transparent; color: var(--voxepay-text-muted); font-size: 0.813rem; font-weight: 500; font-family: inherit; cursor: pointer; transition: all var(--voxepay-transition-fast); border-bottom: 2px solid transparent; white-space: nowrap; }
|
|
1222
|
+
.voxepay-method-tab svg { width: 16px; height: 16px; flex-shrink: 0; }
|
|
1223
|
+
.voxepay-method-tab:hover { color: var(--voxepay-text); background: var(--voxepay-surface-hover); }
|
|
1224
|
+
.voxepay-method-tab.active { color: var(--voxepay-primary); border-bottom-color: var(--voxepay-primary); background: var(--voxepay-bg); }
|
|
1225
|
+
|
|
1226
|
+
/* Bank Transfer View */
|
|
1227
|
+
.voxepay-transfer-view { padding: 4px 0; }
|
|
1228
|
+
.voxepay-transfer-loading { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 48px 0; }
|
|
1229
|
+
.voxepay-transfer-instruction { text-align: center; margin-bottom: 20px; font-size: 0.938rem; color: var(--voxepay-text-muted); }
|
|
1230
|
+
.voxepay-transfer-instruction strong { color: var(--voxepay-text); font-size: 1.063rem; }
|
|
1231
|
+
.voxepay-transfer-details { background: var(--voxepay-surface); border: 1px solid var(--voxepay-border); border-radius: var(--voxepay-border-radius); overflow: hidden; margin-bottom: 16px; }
|
|
1232
|
+
.voxepay-transfer-detail { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; border-bottom: 1px solid var(--voxepay-border); }
|
|
1233
|
+
.voxepay-transfer-detail:last-child { border-bottom: none; }
|
|
1234
|
+
.voxepay-transfer-label { font-size: 0.813rem; color: var(--voxepay-text-muted); font-weight: 500; }
|
|
1235
|
+
.voxepay-transfer-value { font-size: 0.938rem; font-weight: 600; color: var(--voxepay-text); }
|
|
1236
|
+
.voxepay-transfer-value-row { display: flex; align-items: center; gap: 8px; }
|
|
1237
|
+
.voxepay-transfer-account { font-size: 1.125rem; font-weight: 700; color: var(--voxepay-primary); letter-spacing: 1.5px; font-family: 'DM Sans', monospace; }
|
|
1238
|
+
.voxepay-transfer-amount { color: var(--voxepay-primary); }
|
|
1239
|
+
|
|
1240
|
+
/* Copy Button */
|
|
1241
|
+
.voxepay-copy-btn { display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; border: 1px solid var(--voxepay-border); border-radius: 8px; background: var(--voxepay-surface-hover); color: var(--voxepay-text-muted); cursor: pointer; transition: all var(--voxepay-transition-fast); flex-shrink: 0; }
|
|
1242
|
+
.voxepay-copy-btn svg { width: 16px; height: 16px; }
|
|
1243
|
+
.voxepay-copy-btn:hover { border-color: var(--voxepay-primary); color: var(--voxepay-primary); background: rgba(0, 97, 255, 0.1); }
|
|
1244
|
+
.voxepay-copy-btn.voxepay-copied { border-color: var(--voxepay-success); color: var(--voxepay-success); background: var(--voxepay-success-bg); }
|
|
1245
|
+
|
|
1246
|
+
/* Transfer Timer */
|
|
1247
|
+
.voxepay-transfer-timer { display: flex; align-items: center; justify-content: center; gap: 8px; padding: 10px 16px; background: var(--voxepay-surface); border: 1px solid var(--voxepay-border); border-radius: var(--voxepay-border-radius); margin-bottom: 16px; font-size: 0.875rem; color: var(--voxepay-text-muted); }
|
|
1248
|
+
.voxepay-transfer-timer svg { width: 16px; height: 16px; color: var(--voxepay-primary); flex-shrink: 0; }
|
|
1249
|
+
.voxepay-transfer-timer strong { color: var(--voxepay-primary); font-weight: 700; font-family: monospace; font-size: 0.938rem; }
|
|
925
1250
|
|
|
926
|
-
@media (max-width: 480px) { .voxepay-modal { max-width: 100%; max-height: 100%; border-radius: 0; height: 100%; } .voxepay-body { padding: 20px; } .voxepay-otp-digit { width: 42px; height: 50px; font-size: 1.25rem; } }
|
|
1251
|
+
@media (max-width: 480px) { .voxepay-modal { max-width: 100%; max-height: 100%; border-radius: 0; height: 100%; } .voxepay-body { padding: 20px; } .voxepay-otp-digit { width: 42px; height: 50px; font-size: 1.25rem; } .voxepay-transfer-detail { flex-direction: column; align-items: flex-start; gap: 4px; } }
|
|
927
1252
|
`;
|
|
928
1253
|
}
|
|
929
1254
|
}
|