@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.
package/dist/index.esm.js CHANGED
@@ -227,6 +227,18 @@ const ICONS = {
227
227
  error: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20" stroke="currentColor" stroke-width="2">
228
228
  <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" />
229
229
  </svg>`,
230
+ copy: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
231
+ <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" />
232
+ </svg>`,
233
+ bank: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
234
+ <path stroke-linecap="round" stroke-linejoin="round" d="M3 21h18M3 10h18M5 6l7-3 7 3M4 10v11M20 10v11M8 14v3M12 14v3M16 14v3" />
235
+ </svg>`,
236
+ card: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
237
+ <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" />
238
+ </svg>`,
239
+ clock: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
240
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
241
+ </svg>`,
230
242
  };
231
243
  // Card brand logos
232
244
  const CARD_BRAND_DISPLAY = {
@@ -247,6 +259,7 @@ class VoxePayModal {
247
259
  }
248
260
  };
249
261
  this.options = options;
262
+ const methods = options.paymentMethods || ['card', 'bank_transfer'];
250
263
  this.state = {
251
264
  cardNumber: '',
252
265
  expiry: '',
@@ -259,6 +272,10 @@ class VoxePayModal {
259
272
  otpTimer: 60,
260
273
  canResendOtp: false,
261
274
  otpTimerInterval: null,
275
+ paymentMethod: methods[0],
276
+ transferTimer: 0,
277
+ transferTimerInterval: null,
278
+ bankTransferDetails: options.bankTransferDetails || null,
262
279
  };
263
280
  }
264
281
  /**
@@ -279,6 +296,7 @@ class VoxePayModal {
279
296
  */
280
297
  close() {
281
298
  var _a;
299
+ this.stopTransferTimer();
282
300
  (_a = this.overlay) === null || _a === void 0 ? void 0 : _a.classList.remove('voxepay-visible');
283
301
  setTimeout(() => {
284
302
  var _a, _b, _c;
@@ -309,11 +327,20 @@ class VoxePayModal {
309
327
  document.body.appendChild(this.container);
310
328
  this.overlay = this.container.querySelector('.voxepay-overlay');
311
329
  }
330
+ /**
331
+ * Get available payment methods
332
+ */
333
+ getPaymentMethods() {
334
+ return this.options.paymentMethods || ['card', 'bank_transfer'];
335
+ }
312
336
  /**
313
337
  * Get modal HTML
314
338
  */
315
339
  getModalHTML() {
316
340
  const formattedAmount = formatAmount(this.options.amount, this.options.currency);
341
+ const methods = this.getPaymentMethods();
342
+ const showTabs = methods.length > 1;
343
+ const isCard = this.state.paymentMethod === 'card';
317
344
  return `
318
345
  <div class="voxepay-overlay">
319
346
  <div class="voxepay-modal" role="dialog" aria-modal="true" aria-labelledby="voxepay-title">
@@ -329,71 +356,21 @@ class VoxePayModal {
329
356
  ${ICONS.close}
330
357
  </button>
331
358
  </div>
359
+
360
+ ${showTabs ? `
361
+ <div class="voxepay-method-tabs">
362
+ ${methods.includes('card') ? `
363
+ <button class="voxepay-method-tab ${isCard ? 'active' : ''}" data-method="card">
364
+ ${ICONS.card} <span>Card</span>
365
+ </button>` : ''}
366
+ ${methods.includes('bank_transfer') ? `
367
+ <button class="voxepay-method-tab ${!isCard ? 'active' : ''}" data-method="bank_transfer">
368
+ ${ICONS.bank} <span>Bank Transfer</span>
369
+ </button>` : ''}
370
+ </div>` : ''}
332
371
 
333
372
  <div class="voxepay-body" id="voxepay-form-container">
334
- <form id="voxepay-payment-form" novalidate>
335
- <div class="voxepay-form-group">
336
- <label class="voxepay-label">
337
- <span class="voxepay-label-icon">💳</span>
338
- Card Number
339
- </label>
340
- <div class="voxepay-card-input-wrapper">
341
- <input
342
- type="text"
343
- class="voxepay-input"
344
- id="voxepay-card-number"
345
- name="cardNumber"
346
- placeholder="1234 5678 9012 3456"
347
- autocomplete="cc-number"
348
- inputmode="numeric"
349
- />
350
- <div class="voxepay-card-brand" id="voxepay-card-brand"></div>
351
- </div>
352
- <div class="voxepay-error-message" id="voxepay-card-error" style="display: none;"></div>
353
- </div>
354
-
355
- <div class="voxepay-row">
356
- <div class="voxepay-form-group">
357
- <label class="voxepay-label">
358
- <span class="voxepay-label-icon">📅</span>
359
- Expiry
360
- </label>
361
- <input
362
- type="text"
363
- class="voxepay-input"
364
- id="voxepay-expiry"
365
- name="expiry"
366
- placeholder="MM/YY"
367
- autocomplete="cc-exp"
368
- inputmode="numeric"
369
- maxlength="5"
370
- />
371
- <div class="voxepay-error-message" id="voxepay-expiry-error" style="display: none;"></div>
372
- </div>
373
-
374
- <div class="voxepay-form-group">
375
- <label class="voxepay-label">
376
- <span class="voxepay-label-icon">🔒</span>
377
- CVV
378
- </label>
379
- <input
380
- type="text"
381
- class="voxepay-input"
382
- id="voxepay-cvv"
383
- name="cvv"
384
- placeholder="•••"
385
- autocomplete="cc-csc"
386
- inputmode="numeric"
387
- maxlength="4"
388
- />
389
- <div class="voxepay-error-message" id="voxepay-cvv-error" style="display: none;"></div>
390
- </div>
391
- </div>
392
-
393
- <button type="submit" class="voxepay-submit-btn" id="voxepay-submit">
394
- <span>Pay Now ${formattedAmount}</span>
395
- </button>
396
- </form>
373
+ ${isCard ? this.getCardFormHTML(formattedAmount) : this.getBankTransferHTML(formattedAmount)}
397
374
  </div>
398
375
 
399
376
  <div class="voxepay-footer">
@@ -406,6 +383,118 @@ class VoxePayModal {
406
383
  </div>
407
384
  `;
408
385
  }
386
+ /**
387
+ * Get card form HTML
388
+ */
389
+ getCardFormHTML(formattedAmount) {
390
+ return `
391
+ <form id="voxepay-payment-form" novalidate>
392
+ <div class="voxepay-form-group">
393
+ <label class="voxepay-label">
394
+ <span class="voxepay-label-icon">💳</span>
395
+ Card Number
396
+ </label>
397
+ <div class="voxepay-card-input-wrapper">
398
+ <input type="text" class="voxepay-input" id="voxepay-card-number" name="cardNumber"
399
+ placeholder="1234 5678 9012 3456" autocomplete="cc-number" inputmode="numeric" />
400
+ <div class="voxepay-card-brand" id="voxepay-card-brand"></div>
401
+ </div>
402
+ <div class="voxepay-error-message" id="voxepay-card-error" style="display: none;"></div>
403
+ </div>
404
+
405
+ <div class="voxepay-row">
406
+ <div class="voxepay-form-group">
407
+ <label class="voxepay-label"><span class="voxepay-label-icon">📅</span> Expiry</label>
408
+ <input type="text" class="voxepay-input" id="voxepay-expiry" name="expiry"
409
+ placeholder="MM/YY" autocomplete="cc-exp" inputmode="numeric" maxlength="5" />
410
+ <div class="voxepay-error-message" id="voxepay-expiry-error" style="display: none;"></div>
411
+ </div>
412
+ <div class="voxepay-form-group">
413
+ <label class="voxepay-label"><span class="voxepay-label-icon">🔒</span> CVV</label>
414
+ <input type="text" class="voxepay-input" id="voxepay-cvv" name="cvv"
415
+ placeholder="•••" autocomplete="cc-csc" inputmode="numeric" maxlength="4" />
416
+ <div class="voxepay-error-message" id="voxepay-cvv-error" style="display: none;"></div>
417
+ </div>
418
+ </div>
419
+
420
+ <button type="submit" class="voxepay-submit-btn" id="voxepay-submit">
421
+ <span>Pay Now ${formattedAmount}</span>
422
+ </button>
423
+ </form>
424
+ `;
425
+ }
426
+ /**
427
+ * Get bank transfer HTML
428
+ */
429
+ getBankTransferHTML(formattedAmount) {
430
+ const details = this.state.bankTransferDetails;
431
+ if (!details) {
432
+ return `
433
+ <div class="voxepay-transfer-view">
434
+ <div class="voxepay-transfer-loading">
435
+ <div class="voxepay-spinner"></div>
436
+ <p style="margin-top: 16px; color: var(--voxepay-text-muted);">Generating account details...</p>
437
+ </div>
438
+ </div>
439
+ `;
440
+ }
441
+ return `
442
+ <div class="voxepay-transfer-view">
443
+ <div class="voxepay-transfer-instruction">
444
+ <p>Transfer <strong>${formattedAmount}</strong> to the account below</p>
445
+ </div>
446
+
447
+ <div class="voxepay-transfer-details">
448
+ <div class="voxepay-transfer-detail">
449
+ <span class="voxepay-transfer-label">Account Number</span>
450
+ <div class="voxepay-transfer-value-row">
451
+ <span class="voxepay-transfer-value voxepay-transfer-account" id="voxepay-account-number">${details.accountNumber}</span>
452
+ <button class="voxepay-copy-btn" id="voxepay-copy-btn" data-copy="${details.accountNumber}" title="Copy">
453
+ ${ICONS.copy}
454
+ </button>
455
+ </div>
456
+ </div>
457
+
458
+ <div class="voxepay-transfer-detail">
459
+ <span class="voxepay-transfer-label">Bank Name</span>
460
+ <span class="voxepay-transfer-value">${details.bankName}</span>
461
+ </div>
462
+
463
+ <div class="voxepay-transfer-detail">
464
+ <span class="voxepay-transfer-label">Account Name</span>
465
+ <span class="voxepay-transfer-value">${details.accountName}</span>
466
+ </div>
467
+
468
+ <div class="voxepay-transfer-detail">
469
+ <span class="voxepay-transfer-label">Amount</span>
470
+ <span class="voxepay-transfer-value voxepay-transfer-amount">${formattedAmount}</span>
471
+ </div>
472
+
473
+ <div class="voxepay-transfer-detail">
474
+ <span class="voxepay-transfer-label">Reference</span>
475
+ <span class="voxepay-transfer-value" style="font-family: monospace; letter-spacing: 1px;">${details.reference}</span>
476
+ </div>
477
+ </div>
478
+
479
+ <div class="voxepay-transfer-timer" id="voxepay-transfer-timer">
480
+ ${ICONS.clock}
481
+ <span>Account expires in <strong id="voxepay-transfer-countdown">${this.formatCountdown(details.expiresIn)}</strong></span>
482
+ </div>
483
+
484
+ <button type="button" class="voxepay-submit-btn" id="voxepay-transfer-confirm">
485
+ <span>I've sent the money</span>
486
+ </button>
487
+ </div>
488
+ `;
489
+ }
490
+ /**
491
+ * Format countdown seconds to MM:SS
492
+ */
493
+ formatCountdown(seconds) {
494
+ const m = Math.floor(seconds / 60);
495
+ const s = seconds % 60;
496
+ return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
497
+ }
409
498
  /**
410
499
  * Render success view
411
500
  */
@@ -646,8 +735,8 @@ class VoxePayModal {
646
735
  * Attach event listeners
647
736
  */
648
737
  attachEventListeners() {
649
- var _a, _b, _c, _d, _e, _f;
650
- const closeBtn = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('[data-action="close"]');
738
+ var _a, _b, _c, _d, _e, _f, _g;
739
+ const closeBtn = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('[data-action=\"close\"]');
651
740
  closeBtn === null || closeBtn === void 0 ? void 0 : closeBtn.addEventListener('click', () => this.close());
652
741
  (_b = this.overlay) === null || _b === void 0 ? void 0 : _b.addEventListener('click', (e) => {
653
742
  if (e.target === this.overlay) {
@@ -655,17 +744,221 @@ class VoxePayModal {
655
744
  }
656
745
  });
657
746
  document.addEventListener('keydown', this.handleEscape);
658
- const cardInput = (_c = this.container) === null || _c === void 0 ? void 0 : _c.querySelector('#voxepay-card-number');
659
- cardInput === null || cardInput === void 0 ? void 0 : cardInput.addEventListener('input', (e) => this.handleCardInput(e));
660
- cardInput === null || cardInput === void 0 ? void 0 : cardInput.addEventListener('blur', () => this.validateField('cardNumber'));
661
- const expiryInput = (_d = this.container) === null || _d === void 0 ? void 0 : _d.querySelector('#voxepay-expiry');
662
- expiryInput === null || expiryInput === void 0 ? void 0 : expiryInput.addEventListener('input', (e) => this.handleExpiryInput(e));
663
- expiryInput === null || expiryInput === void 0 ? void 0 : expiryInput.addEventListener('blur', () => this.validateField('expiry'));
664
- const cvvInput = (_e = this.container) === null || _e === void 0 ? void 0 : _e.querySelector('#voxepay-cvv');
665
- cvvInput === null || cvvInput === void 0 ? void 0 : cvvInput.addEventListener('input', (e) => this.handleCVVInput(e));
666
- cvvInput === null || cvvInput === void 0 ? void 0 : cvvInput.addEventListener('blur', () => this.validateField('cvv'));
667
- const form = (_f = this.container) === null || _f === void 0 ? void 0 : _f.querySelector('#voxepay-payment-form');
668
- form === null || form === void 0 ? void 0 : form.addEventListener('submit', (e) => this.handleSubmit(e));
747
+ // Payment method tabs
748
+ const tabs = (_c = this.container) === null || _c === void 0 ? void 0 : _c.querySelectorAll('.voxepay-method-tab');
749
+ tabs === null || tabs === void 0 ? void 0 : tabs.forEach(tab => {
750
+ tab.addEventListener('click', () => {
751
+ const method = tab.dataset.method;
752
+ if (method && method !== this.state.paymentMethod) {
753
+ this.switchPaymentMethod(method);
754
+ }
755
+ });
756
+ });
757
+ // Card-specific listeners
758
+ if (this.state.paymentMethod === 'card') {
759
+ const cardInput = (_d = this.container) === null || _d === void 0 ? void 0 : _d.querySelector('#voxepay-card-number');
760
+ cardInput === null || cardInput === void 0 ? void 0 : cardInput.addEventListener('input', (e) => this.handleCardInput(e));
761
+ cardInput === null || cardInput === void 0 ? void 0 : cardInput.addEventListener('blur', () => this.validateField('cardNumber'));
762
+ const expiryInput = (_e = this.container) === null || _e === void 0 ? void 0 : _e.querySelector('#voxepay-expiry');
763
+ expiryInput === null || expiryInput === void 0 ? void 0 : expiryInput.addEventListener('input', (e) => this.handleExpiryInput(e));
764
+ expiryInput === null || expiryInput === void 0 ? void 0 : expiryInput.addEventListener('blur', () => this.validateField('expiry'));
765
+ const cvvInput = (_f = this.container) === null || _f === void 0 ? void 0 : _f.querySelector('#voxepay-cvv');
766
+ cvvInput === null || cvvInput === void 0 ? void 0 : cvvInput.addEventListener('input', (e) => this.handleCVVInput(e));
767
+ cvvInput === null || cvvInput === void 0 ? void 0 : cvvInput.addEventListener('blur', () => this.validateField('cvv'));
768
+ const form = (_g = this.container) === null || _g === void 0 ? void 0 : _g.querySelector('#voxepay-payment-form');
769
+ form === null || form === void 0 ? void 0 : form.addEventListener('submit', (e) => this.handleSubmit(e));
770
+ }
771
+ // Bank transfer-specific listeners
772
+ if (this.state.paymentMethod === 'bank_transfer') {
773
+ this.attachBankTransferListeners();
774
+ // If no details yet, load them
775
+ if (!this.state.bankTransferDetails) {
776
+ this.loadBankTransferDetails();
777
+ }
778
+ else {
779
+ this.startTransferTimer();
780
+ }
781
+ }
782
+ }
783
+ /**
784
+ * Attach bank transfer specific event listeners
785
+ */
786
+ attachBankTransferListeners() {
787
+ var _a, _b;
788
+ const copyBtn = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#voxepay-copy-btn');
789
+ copyBtn === null || copyBtn === void 0 ? void 0 : copyBtn.addEventListener('click', () => {
790
+ const accountNum = copyBtn.dataset.copy || '';
791
+ this.copyToClipboard(accountNum);
792
+ });
793
+ const confirmBtn = (_b = this.container) === null || _b === void 0 ? void 0 : _b.querySelector('#voxepay-transfer-confirm');
794
+ confirmBtn === null || confirmBtn === void 0 ? void 0 : confirmBtn.addEventListener('click', () => this.handleTransferConfirm());
795
+ }
796
+ /**
797
+ * Switch between payment methods
798
+ */
799
+ switchPaymentMethod(method) {
800
+ var _a;
801
+ this.stopTransferTimer();
802
+ this.state.paymentMethod = method;
803
+ // Re-render the whole modal
804
+ if (this.container) {
805
+ this.container.innerHTML = this.getModalHTML();
806
+ this.overlay = this.container.querySelector('.voxepay-overlay');
807
+ (_a = this.overlay) === null || _a === void 0 ? void 0 : _a.classList.add('voxepay-visible');
808
+ this.attachEventListeners();
809
+ // Restore card values if switching back to card
810
+ if (method === 'card' && this.state.cardNumber) {
811
+ const cardInput = this.container.querySelector('#voxepay-card-number');
812
+ const expiryInput = this.container.querySelector('#voxepay-expiry');
813
+ const cvvInput = this.container.querySelector('#voxepay-cvv');
814
+ if (cardInput)
815
+ cardInput.value = formatCardNumber(this.state.cardNumber);
816
+ if (expiryInput)
817
+ expiryInput.value = this.state.expiry;
818
+ if (cvvInput)
819
+ cvvInput.value = this.state.cvv;
820
+ }
821
+ }
822
+ }
823
+ /**
824
+ * Load bank transfer details (from callback or generate mock)
825
+ */
826
+ async loadBankTransferDetails() {
827
+ var _a;
828
+ try {
829
+ let details;
830
+ if (this.options.onBankTransferRequested) {
831
+ details = await this.options.onBankTransferRequested();
832
+ }
833
+ else {
834
+ // Mock/default bank details for testing
835
+ await new Promise(resolve => setTimeout(resolve, 1500));
836
+ details = {
837
+ accountNumber: '0123456789',
838
+ bankName: 'VoxePay Bank',
839
+ accountName: 'VoxePay Collections',
840
+ reference: `VP-${Date.now().toString(36).toUpperCase()}`,
841
+ expiresIn: 1800, // 30 minutes
842
+ };
843
+ }
844
+ this.state.bankTransferDetails = details;
845
+ // Re-render the bank transfer view
846
+ const formContainer = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#voxepay-form-container');
847
+ if (formContainer) {
848
+ const formattedAmount = formatAmount(this.options.amount, this.options.currency);
849
+ formContainer.innerHTML = this.getBankTransferHTML(formattedAmount);
850
+ this.attachBankTransferListeners();
851
+ this.startTransferTimer();
852
+ }
853
+ }
854
+ catch (error) {
855
+ this.options.onError({
856
+ code: 'BANK_TRANSFER_INIT_FAILED',
857
+ message: 'Could not generate bank transfer details. Please try again.',
858
+ recoverable: true,
859
+ });
860
+ }
861
+ }
862
+ /**
863
+ * Copy text to clipboard with visual feedback
864
+ */
865
+ async copyToClipboard(text) {
866
+ var _a;
867
+ try {
868
+ await navigator.clipboard.writeText(text);
869
+ const copyBtn = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#voxepay-copy-btn');
870
+ if (copyBtn) {
871
+ copyBtn.innerHTML = `${ICONS.check}`;
872
+ copyBtn.classList.add('voxepay-copied');
873
+ setTimeout(() => {
874
+ copyBtn.innerHTML = `${ICONS.copy}`;
875
+ copyBtn.classList.remove('voxepay-copied');
876
+ }, 2000);
877
+ }
878
+ }
879
+ catch (_b) {
880
+ // Fallback for older browsers
881
+ const textarea = document.createElement('textarea');
882
+ textarea.value = text;
883
+ textarea.style.position = 'fixed';
884
+ textarea.style.opacity = '0';
885
+ document.body.appendChild(textarea);
886
+ textarea.select();
887
+ document.execCommand('copy');
888
+ document.body.removeChild(textarea);
889
+ }
890
+ }
891
+ /**
892
+ * Handle "I've sent the money" confirmation
893
+ */
894
+ async handleTransferConfirm() {
895
+ var _a, _b;
896
+ const confirmBtn = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#voxepay-transfer-confirm');
897
+ if (confirmBtn) {
898
+ confirmBtn.disabled = true;
899
+ confirmBtn.innerHTML = '<div class="voxepay-spinner"></div><span>Confirming transfer...</span>';
900
+ }
901
+ this.stopTransferTimer();
902
+ try {
903
+ // Simulate transfer verification
904
+ await new Promise(resolve => setTimeout(resolve, 3000));
905
+ const result = {
906
+ id: `pay_transfer_${Date.now()}`,
907
+ status: 'pending',
908
+ amount: this.options.amount,
909
+ currency: this.options.currency,
910
+ timestamp: new Date().toISOString(),
911
+ reference: (_b = this.state.bankTransferDetails) === null || _b === void 0 ? void 0 : _b.reference,
912
+ paymentMethod: 'bank_transfer',
913
+ };
914
+ this.state.isSuccess = true;
915
+ this.renderSuccessView();
916
+ this.options.onSuccess(result);
917
+ }
918
+ catch (error) {
919
+ if (confirmBtn) {
920
+ confirmBtn.disabled = false;
921
+ confirmBtn.innerHTML = "<span>I've sent the money</span>";
922
+ }
923
+ const paymentError = error;
924
+ this.options.onError(paymentError);
925
+ }
926
+ }
927
+ /**
928
+ * Start the transfer countdown timer
929
+ */
930
+ startTransferTimer() {
931
+ if (!this.state.bankTransferDetails)
932
+ return;
933
+ this.state.transferTimer = this.state.bankTransferDetails.expiresIn;
934
+ this.state.transferTimerInterval = window.setInterval(() => {
935
+ var _a, _b, _c;
936
+ this.state.transferTimer--;
937
+ const countdownEl = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#voxepay-transfer-countdown');
938
+ if (countdownEl) {
939
+ countdownEl.textContent = this.formatCountdown(this.state.transferTimer);
940
+ }
941
+ if (this.state.transferTimer <= 0) {
942
+ this.stopTransferTimer();
943
+ // Show expired state
944
+ const timerEl = (_b = this.container) === null || _b === void 0 ? void 0 : _b.querySelector('#voxepay-transfer-timer');
945
+ if (timerEl) {
946
+ timerEl.innerHTML = `${ICONS.clock} <span style="color: var(--voxepay-error);">Account expired. Please try again.</span>`;
947
+ }
948
+ const confirmBtn = (_c = this.container) === null || _c === void 0 ? void 0 : _c.querySelector('#voxepay-transfer-confirm');
949
+ if (confirmBtn)
950
+ confirmBtn.disabled = true;
951
+ }
952
+ }, 1000);
953
+ }
954
+ /**
955
+ * Stop the transfer countdown timer
956
+ */
957
+ stopTransferTimer() {
958
+ if (this.state.transferTimerInterval) {
959
+ clearInterval(this.state.transferTimerInterval);
960
+ this.state.transferTimerInterval = null;
961
+ }
669
962
  }
670
963
  handleCardInput(e) {
671
964
  var _a;
@@ -918,8 +1211,40 @@ class VoxePayModal {
918
1211
 
919
1212
  .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); }
920
1213
  .voxepay-back-btn:hover { color: var(--voxepay-primary); }
1214
+
1215
+ /* Payment Method Tabs */
1216
+ .voxepay-method-tabs { display: flex; border-bottom: 1px solid var(--voxepay-border); background: var(--voxepay-surface); }
1217
+ .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; }
1218
+ .voxepay-method-tab svg { width: 18px; height: 18px; }
1219
+ .voxepay-method-tab:hover { color: var(--voxepay-text); background: var(--voxepay-surface-hover); }
1220
+ .voxepay-method-tab.active { color: var(--voxepay-primary); border-bottom-color: var(--voxepay-primary); background: var(--voxepay-bg); }
1221
+
1222
+ /* Bank Transfer View */
1223
+ .voxepay-transfer-view { padding: 4px 0; }
1224
+ .voxepay-transfer-loading { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 48px 0; }
1225
+ .voxepay-transfer-instruction { text-align: center; margin-bottom: 20px; font-size: 0.938rem; color: var(--voxepay-text-muted); }
1226
+ .voxepay-transfer-instruction strong { color: var(--voxepay-text); font-size: 1.063rem; }
1227
+ .voxepay-transfer-details { background: var(--voxepay-surface); border: 1px solid var(--voxepay-border); border-radius: var(--voxepay-border-radius); overflow: hidden; margin-bottom: 16px; }
1228
+ .voxepay-transfer-detail { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; border-bottom: 1px solid var(--voxepay-border); }
1229
+ .voxepay-transfer-detail:last-child { border-bottom: none; }
1230
+ .voxepay-transfer-label { font-size: 0.813rem; color: var(--voxepay-text-muted); font-weight: 500; }
1231
+ .voxepay-transfer-value { font-size: 0.938rem; font-weight: 600; color: var(--voxepay-text); }
1232
+ .voxepay-transfer-value-row { display: flex; align-items: center; gap: 8px; }
1233
+ .voxepay-transfer-account { font-size: 1.125rem; font-weight: 700; color: var(--voxepay-primary); letter-spacing: 1.5px; font-family: 'DM Sans', monospace; }
1234
+ .voxepay-transfer-amount { color: var(--voxepay-primary); }
1235
+
1236
+ /* Copy Button */
1237
+ .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; }
1238
+ .voxepay-copy-btn svg { width: 16px; height: 16px; }
1239
+ .voxepay-copy-btn:hover { border-color: var(--voxepay-primary); color: var(--voxepay-primary); background: rgba(0, 97, 255, 0.1); }
1240
+ .voxepay-copy-btn.voxepay-copied { border-color: var(--voxepay-success); color: var(--voxepay-success); background: var(--voxepay-success-bg); }
1241
+
1242
+ /* Transfer Timer */
1243
+ .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); }
1244
+ .voxepay-transfer-timer svg { width: 16px; height: 16px; color: var(--voxepay-primary); flex-shrink: 0; }
1245
+ .voxepay-transfer-timer strong { color: var(--voxepay-primary); font-weight: 700; font-family: monospace; font-size: 0.938rem; }
921
1246
 
922
- @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; } }
1247
+ @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; } }
923
1248
  `;
924
1249
  }
925
1250
  }