@voxepay/checkout 0.1.0 → 0.2.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.
@@ -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
- <form id="voxepay-payment-form" novalidate>
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="close"]');
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
- const cardInput = (_c = this.container) === null || _c === void 0 ? void 0 : _c.querySelector('#voxepay-card-number');
663
- cardInput === null || cardInput === void 0 ? void 0 : cardInput.addEventListener('input', (e) => this.handleCardInput(e));
664
- cardInput === null || cardInput === void 0 ? void 0 : cardInput.addEventListener('blur', () => this.validateField('cardNumber'));
665
- const expiryInput = (_d = this.container) === null || _d === void 0 ? void 0 : _d.querySelector('#voxepay-expiry');
666
- expiryInput === null || expiryInput === void 0 ? void 0 : expiryInput.addEventListener('input', (e) => this.handleExpiryInput(e));
667
- expiryInput === null || expiryInput === void 0 ? void 0 : expiryInput.addEventListener('blur', () => this.validateField('expiry'));
668
- const cvvInput = (_e = this.container) === null || _e === void 0 ? void 0 : _e.querySelector('#voxepay-cvv');
669
- cvvInput === null || cvvInput === void 0 ? void 0 : cvvInput.addEventListener('input', (e) => this.handleCVVInput(e));
670
- cvvInput === null || cvvInput === void 0 ? void 0 : cvvInput.addEventListener('blur', () => this.validateField('cvv'));
671
- const form = (_f = this.container) === null || _f === void 0 ? void 0 : _f.querySelector('#voxepay-payment-form');
672
- form === null || form === void 0 ? void 0 : form.addEventListener('submit', (e) => this.handleSubmit(e));
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: 8px; padding: 14px 16px; border: none; background: transparent; color: var(--voxepay-text-muted); font-size: 0.875rem; font-weight: 500; font-family: inherit; cursor: pointer; transition: all var(--voxepay-transition-fast); border-bottom: 2px solid transparent; }
1222
+ .voxepay-method-tab svg { width: 18px; height: 18px; }
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
  }