@zahlen/checkout 0.1.2 → 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/index.cjs.js CHANGED
@@ -494,6 +494,247 @@ class ZahlenModal {
494
494
  const closeBtn = formContainer.querySelector('[data-action="close"]');
495
495
  closeBtn === null || closeBtn === void 0 ? void 0 : closeBtn.addEventListener('click', ()=>this.close());
496
496
  }
497
+ /**
498
+ * Render OTP verification view
499
+ */ renderOTPView() {
500
+ var _a;
501
+ const formContainer = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#zahlen-form-container');
502
+ if (!formContainer) return;
503
+ this.state.isOtpStep = true;
504
+ this.state.otpTimer = 60;
505
+ this.state.canResendOtp = false;
506
+ const maskedPhone = this.options.customerEmail ? `****${this.options.customerEmail.slice(-4)}` : '****1234';
507
+ formContainer.innerHTML = `
508
+ <div class="zahlen-otp-view">
509
+ <div class="zahlen-otp-header">
510
+ <div class="zahlen-otp-icon">📱</div>
511
+ <h3 class="zahlen-otp-title">Verify Your Payment</h3>
512
+ <p class="zahlen-otp-subtitle">We've sent a 6-digit code to ${maskedPhone}</p>
513
+ </div>
514
+
515
+ <div class="zahlen-otp-inputs-container">
516
+ <div class="zahlen-otp-inputs" id="zahlen-otp-inputs">
517
+ <input type="text" maxlength="1" class="zahlen-otp-digit" data-index="0" inputmode="numeric" autocomplete="one-time-code" />
518
+ <input type="text" maxlength="1" class="zahlen-otp-digit" data-index="1" inputmode="numeric" />
519
+ <input type="text" maxlength="1" class="zahlen-otp-digit" data-index="2" inputmode="numeric" />
520
+ <input type="text" maxlength="1" class="zahlen-otp-digit" data-index="3" inputmode="numeric" />
521
+ <input type="text" maxlength="1" class="zahlen-otp-digit" data-index="4" inputmode="numeric" />
522
+ <input type="text" maxlength="1" class="zahlen-otp-digit" data-index="5" inputmode="numeric" />
523
+ </div>
524
+ <div class="zahlen-error-message" id="zahlen-otp-error" style="display: none;"></div>
525
+ </div>
526
+
527
+ <div class="zahlen-otp-timer" id="zahlen-otp-timer">
528
+ Resend code in <span id="zahlen-timer-count">60</span>s
529
+ </div>
530
+
531
+ <button class="zahlen-resend-btn" id="zahlen-resend-otp" disabled>
532
+ Resend OTP
533
+ </button>
534
+
535
+ <button type="button" class="zahlen-submit-btn" id="zahlen-verify-otp">
536
+ <span>🔐 Verify & Pay</span>
537
+ </button>
538
+
539
+ <button class="zahlen-back-btn" id="zahlen-back-to-card">
540
+ ← Back to card details
541
+ </button>
542
+ </div>
543
+ `;
544
+ this.attachOTPEventListeners();
545
+ this.startOTPTimer();
546
+ }
547
+ /**
548
+ * Attach OTP-specific event listeners
549
+ */ attachOTPEventListeners() {
550
+ var _a, _b, _c, _d, _e;
551
+ const otpInputs = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelectorAll('.zahlen-otp-digit');
552
+ otpInputs === null || otpInputs === void 0 ? void 0 : otpInputs.forEach((input, index)=>{
553
+ input.addEventListener('input', (e)=>this.handleOTPInput(e, index));
554
+ input.addEventListener('keydown', (e)=>this.handleOTPKeydown(e, index));
555
+ input.addEventListener('paste', (e)=>this.handleOTPPaste(e));
556
+ });
557
+ // Verify button
558
+ const verifyBtn = (_b = this.container) === null || _b === void 0 ? void 0 : _b.querySelector('#zahlen-verify-otp');
559
+ verifyBtn === null || verifyBtn === void 0 ? void 0 : verifyBtn.addEventListener('click', ()=>this.handleOTPSubmit());
560
+ // Resend button
561
+ const resendBtn = (_c = this.container) === null || _c === void 0 ? void 0 : _c.querySelector('#zahlen-resend-otp');
562
+ resendBtn === null || resendBtn === void 0 ? void 0 : resendBtn.addEventListener('click', ()=>this.handleResendOTP());
563
+ // Back button
564
+ const backBtn = (_d = this.container) === null || _d === void 0 ? void 0 : _d.querySelector('#zahlen-back-to-card');
565
+ backBtn === null || backBtn === void 0 ? void 0 : backBtn.addEventListener('click', ()=>this.handleBackToCard());
566
+ // Focus first input
567
+ (_e = otpInputs === null || otpInputs === void 0 ? void 0 : otpInputs[0]) === null || _e === void 0 ? void 0 : _e.focus();
568
+ }
569
+ /**
570
+ * Handle OTP digit input
571
+ */ handleOTPInput(e, index) {
572
+ var _a;
573
+ const input = e.target;
574
+ const value = input.value.replace(/\D/g, '');
575
+ input.value = value;
576
+ if (value && index < 5) {
577
+ const nextInput = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector(`[data-index="${index + 1}"]`);
578
+ nextInput === null || nextInput === void 0 ? void 0 : nextInput.focus();
579
+ }
580
+ this.updateOTPState();
581
+ this.clearError('otp');
582
+ }
583
+ /**
584
+ * Handle OTP keydown for backspace navigation
585
+ */ handleOTPKeydown(e, index) {
586
+ var _a;
587
+ const input = e.target;
588
+ if (e.key === 'Backspace' && !input.value && index > 0) {
589
+ const prevInput = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector(`[data-index="${index - 1}"]`);
590
+ prevInput === null || prevInput === void 0 ? void 0 : prevInput.focus();
591
+ }
592
+ }
593
+ /**
594
+ * Handle OTP paste
595
+ */ handleOTPPaste(e) {
596
+ var _a, _b;
597
+ e.preventDefault();
598
+ const pastedData = (_a = e.clipboardData) === null || _a === void 0 ? void 0 : _a.getData('text').replace(/\D/g, '').slice(0, 6);
599
+ if (pastedData) {
600
+ const inputs = (_b = this.container) === null || _b === void 0 ? void 0 : _b.querySelectorAll('.zahlen-otp-digit');
601
+ pastedData.split('').forEach((digit, i)=>{
602
+ if (inputs[i]) {
603
+ inputs[i].value = digit;
604
+ }
605
+ });
606
+ this.updateOTPState();
607
+ }
608
+ }
609
+ /**
610
+ * Update OTP state from inputs
611
+ */ updateOTPState() {
612
+ var _a;
613
+ const inputs = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelectorAll('.zahlen-otp-digit');
614
+ let otp = '';
615
+ inputs === null || inputs === void 0 ? void 0 : inputs.forEach((input)=>otp += input.value);
616
+ this.state.otp = otp;
617
+ }
618
+ /**
619
+ * Start OTP resend timer
620
+ */ startOTPTimer() {
621
+ var _a, _b, _c;
622
+ const timerEl = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#zahlen-timer-count');
623
+ const timerContainer = (_b = this.container) === null || _b === void 0 ? void 0 : _b.querySelector('#zahlen-otp-timer');
624
+ const resendBtn = (_c = this.container) === null || _c === void 0 ? void 0 : _c.querySelector('#zahlen-resend-otp');
625
+ this.state.otpTimerInterval = window.setInterval(()=>{
626
+ this.state.otpTimer--;
627
+ if (timerEl) {
628
+ timerEl.textContent = String(this.state.otpTimer);
629
+ }
630
+ if (this.state.otpTimer <= 0) {
631
+ if (this.state.otpTimerInterval) {
632
+ clearInterval(this.state.otpTimerInterval);
633
+ }
634
+ this.state.canResendOtp = true;
635
+ if (timerContainer) timerContainer.style.display = 'none';
636
+ if (resendBtn) resendBtn.disabled = false;
637
+ }
638
+ }, 1000);
639
+ }
640
+ /**
641
+ * Handle OTP submission
642
+ */ handleOTPSubmit() {
643
+ return __awaiter(this, void 0, void 0, function*() {
644
+ if (this.state.otp.length !== 6) {
645
+ this.showError('otp', 'Please enter the complete 6-digit code');
646
+ return;
647
+ }
648
+ this.setOTPProcessing(true);
649
+ try {
650
+ // Simulate OTP verification
651
+ yield this.verifyOTP();
652
+ // Clear timer
653
+ if (this.state.otpTimerInterval) {
654
+ clearInterval(this.state.otpTimerInterval);
655
+ }
656
+ // Process final payment
657
+ const result = yield this.processPayment();
658
+ this.state.isSuccess = true;
659
+ this.renderSuccessView();
660
+ this.options.onSuccess(result);
661
+ } catch (error) {
662
+ this.setOTPProcessing(false);
663
+ const paymentError = error;
664
+ this.showError('otp', paymentError.message || 'Invalid OTP. Please try again.');
665
+ }
666
+ });
667
+ }
668
+ /**
669
+ * Set OTP processing state
670
+ */ setOTPProcessing(isProcessing) {
671
+ var _a;
672
+ const verifyBtn = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#zahlen-verify-otp');
673
+ if (verifyBtn) {
674
+ verifyBtn.disabled = isProcessing;
675
+ verifyBtn.innerHTML = isProcessing ? '<div class="zahlen-spinner"></div><span>Verifying...</span>' : '<span>🔐 Verify & Pay</span>';
676
+ }
677
+ }
678
+ /**
679
+ * Verify OTP (placeholder - integrate with backend)
680
+ */ verifyOTP() {
681
+ return __awaiter(this, void 0, void 0, function*() {
682
+ yield new Promise((resolve)=>setTimeout(resolve, 1500));
683
+ // Simulated OTP validation - accept any 6 digits for demo
684
+ // In production, send to your API for verification
685
+ if (this.state.otp.length !== 6) {
686
+ throw {
687
+ code: 'INVALID_OTP',
688
+ message: 'Invalid OTP',
689
+ recoverable: true
690
+ };
691
+ }
692
+ });
693
+ }
694
+ /**
695
+ * Handle resend OTP
696
+ */ handleResendOTP() {
697
+ var _a, _b, _c, _d;
698
+ const timerContainer = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#zahlen-otp-timer');
699
+ const resendBtn = (_b = this.container) === null || _b === void 0 ? void 0 : _b.querySelector('#zahlen-resend-otp');
700
+ // Reset timer
701
+ this.state.otpTimer = 60;
702
+ this.state.canResendOtp = false;
703
+ if (timerContainer) timerContainer.style.display = 'block';
704
+ if (resendBtn) resendBtn.disabled = true;
705
+ // Clear old inputs
706
+ const inputs = (_c = this.container) === null || _c === void 0 ? void 0 : _c.querySelectorAll('.zahlen-otp-digit');
707
+ inputs === null || inputs === void 0 ? void 0 : inputs.forEach((input)=>input.value = '');
708
+ this.state.otp = '';
709
+ // Restart timer
710
+ this.startOTPTimer();
711
+ // Focus first input
712
+ (_d = inputs === null || inputs === void 0 ? void 0 : inputs[0]) === null || _d === void 0 ? void 0 : _d.focus();
713
+ }
714
+ /**
715
+ * Handle back to card details
716
+ */ handleBackToCard() {
717
+ var _a;
718
+ if (this.state.otpTimerInterval) {
719
+ clearInterval(this.state.otpTimerInterval);
720
+ }
721
+ this.state.isOtpStep = false;
722
+ this.state.otp = '';
723
+ // Re-render full modal
724
+ if (this.container) {
725
+ this.container.innerHTML = this.getModalHTML();
726
+ this.overlay = this.container.querySelector('.zahlen-overlay');
727
+ (_a = this.overlay) === null || _a === void 0 ? void 0 : _a.classList.add('zahlen-visible');
728
+ this.attachEventListeners();
729
+ // Restore previous card values
730
+ const cardInput = this.container.querySelector('#zahlen-card-number');
731
+ const expiryInput = this.container.querySelector('#zahlen-expiry');
732
+ const cvvInput = this.container.querySelector('#zahlen-cvv');
733
+ if (cardInput) cardInput.value = formatCardNumber(this.state.cardNumber);
734
+ if (expiryInput) expiryInput.value = this.state.expiry;
735
+ if (cvvInput) cvvInput.value = this.state.cvv;
736
+ }
737
+ }
497
738
  /**
498
739
  * Attach event listeners
499
740
  */ attachEventListeners() {
@@ -623,13 +864,12 @@ class ZahlenModal {
623
864
  // Show loading state
624
865
  this.setProcessing(true);
625
866
  try {
626
- // Simulate API call (replace with actual API integration)
627
- const result = yield this.processPayment();
628
- // Show success
629
- this.state.isSuccess = true;
630
- this.renderSuccessView();
631
- // Callback
632
- this.options.onSuccess(result);
867
+ // Simulate sending OTP (in production, send to your API)
868
+ yield new Promise((resolve)=>setTimeout(resolve, 1000));
869
+ // Hide processing state
870
+ this.setProcessing(false);
871
+ // Show OTP verification view
872
+ this.renderOTPView();
633
873
  } catch (error) {
634
874
  this.setProcessing(false);
635
875
  const paymentError = error;
@@ -796,7 +1036,30 @@ class ZahlenModal {
796
1036
  .zahlen-success-btn { padding: 12px 32px; background: var(--zahlen-surface); border: 1.5px solid var(--zahlen-border); border-radius: var(--zahlen-border-radius); color: var(--zahlen-text); font-size: 1rem; font-weight: 500; cursor: pointer; transition: all var(--zahlen-transition-fast); }
797
1037
  .zahlen-success-btn:hover { background: var(--zahlen-surface-hover); border-color: var(--zahlen-primary); color: var(--zahlen-primary); }
798
1038
 
799
- @media (max-width: 480px) { .zahlen-modal { max-width: 100%; max-height: 100%; border-radius: 0; height: 100%; } .zahlen-body { padding: 20px; } }
1039
+ /* OTP View Styles */
1040
+ .zahlen-otp-view { text-align: center; padding: 24px 16px; }
1041
+ .zahlen-otp-header { margin-bottom: 24px; }
1042
+ .zahlen-otp-icon { font-size: 3rem; margin-bottom: 12px; }
1043
+ .zahlen-otp-title { font-size: 1.25rem; font-weight: 700; color: var(--zahlen-text); margin-bottom: 8px; }
1044
+ .zahlen-otp-subtitle { font-size: 0.875rem; color: var(--zahlen-text-muted); }
1045
+
1046
+ .zahlen-otp-inputs-container { margin-bottom: 16px; }
1047
+ .zahlen-otp-inputs { display: flex; justify-content: center; gap: 8px; margin-bottom: 8px; }
1048
+ .zahlen-otp-digit { width: 48px; height: 56px; text-align: center; font-size: 1.5rem; font-weight: 700; border: 2px solid var(--zahlen-border); border-radius: var(--zahlen-border-radius); background: var(--zahlen-input-bg); color: var(--zahlen-text); outline: none; transition: all var(--zahlen-transition-fast); }
1049
+ .zahlen-otp-digit:focus { border-color: var(--zahlen-primary); box-shadow: 0 0 0 4px var(--zahlen-glow); }
1050
+ .zahlen-otp-digit.filled { border-color: var(--zahlen-primary); background: var(--zahlen-surface); }
1051
+
1052
+ .zahlen-otp-timer { font-size: 0.875rem; color: var(--zahlen-text-muted); margin-bottom: 12px; }
1053
+ .zahlen-otp-timer span { font-weight: 700; color: var(--zahlen-primary); }
1054
+
1055
+ .zahlen-resend-btn { padding: 8px 16px; background: transparent; border: 1px solid var(--zahlen-border); border-radius: var(--zahlen-border-radius); color: var(--zahlen-text-muted); font-size: 0.875rem; cursor: pointer; margin-bottom: 16px; transition: all var(--zahlen-transition-fast); }
1056
+ .zahlen-resend-btn:hover:not(:disabled) { border-color: var(--zahlen-primary); color: var(--zahlen-primary); }
1057
+ .zahlen-resend-btn:disabled { opacity: 0.5; cursor: not-allowed; }
1058
+
1059
+ .zahlen-back-btn { display: block; width: 100%; padding: 12px; background: transparent; border: none; color: var(--zahlen-text-muted); font-size: 0.875rem; cursor: pointer; margin-top: 12px; transition: color var(--zahlen-transition-fast); }
1060
+ .zahlen-back-btn:hover { color: var(--zahlen-primary); }
1061
+
1062
+ @media (max-width: 480px) { .zahlen-modal { max-width: 100%; max-height: 100%; border-radius: 0; height: 100%; } .zahlen-body { padding: 20px; } .zahlen-otp-digit { width: 42px; height: 50px; font-size: 1.25rem; } }
800
1063
  `;
801
1064
  }
802
1065
  constructor(options){
@@ -813,9 +1076,14 @@ class ZahlenModal {
813
1076
  cardNumber: '',
814
1077
  expiry: '',
815
1078
  cvv: '',
1079
+ otp: '',
816
1080
  errors: {},
817
1081
  isProcessing: false,
818
- isSuccess: false
1082
+ isSuccess: false,
1083
+ isOtpStep: false,
1084
+ otpTimer: 60,
1085
+ canResendOtp: false,
1086
+ otpTimerInterval: null
819
1087
  };
820
1088
  }
821
1089
  }
package/index.esm.js CHANGED
@@ -490,6 +490,247 @@ class ZahlenModal {
490
490
  const closeBtn = formContainer.querySelector('[data-action="close"]');
491
491
  closeBtn === null || closeBtn === void 0 ? void 0 : closeBtn.addEventListener('click', ()=>this.close());
492
492
  }
493
+ /**
494
+ * Render OTP verification view
495
+ */ renderOTPView() {
496
+ var _a;
497
+ const formContainer = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#zahlen-form-container');
498
+ if (!formContainer) return;
499
+ this.state.isOtpStep = true;
500
+ this.state.otpTimer = 60;
501
+ this.state.canResendOtp = false;
502
+ const maskedPhone = this.options.customerEmail ? `****${this.options.customerEmail.slice(-4)}` : '****1234';
503
+ formContainer.innerHTML = `
504
+ <div class="zahlen-otp-view">
505
+ <div class="zahlen-otp-header">
506
+ <div class="zahlen-otp-icon">📱</div>
507
+ <h3 class="zahlen-otp-title">Verify Your Payment</h3>
508
+ <p class="zahlen-otp-subtitle">We've sent a 6-digit code to ${maskedPhone}</p>
509
+ </div>
510
+
511
+ <div class="zahlen-otp-inputs-container">
512
+ <div class="zahlen-otp-inputs" id="zahlen-otp-inputs">
513
+ <input type="text" maxlength="1" class="zahlen-otp-digit" data-index="0" inputmode="numeric" autocomplete="one-time-code" />
514
+ <input type="text" maxlength="1" class="zahlen-otp-digit" data-index="1" inputmode="numeric" />
515
+ <input type="text" maxlength="1" class="zahlen-otp-digit" data-index="2" inputmode="numeric" />
516
+ <input type="text" maxlength="1" class="zahlen-otp-digit" data-index="3" inputmode="numeric" />
517
+ <input type="text" maxlength="1" class="zahlen-otp-digit" data-index="4" inputmode="numeric" />
518
+ <input type="text" maxlength="1" class="zahlen-otp-digit" data-index="5" inputmode="numeric" />
519
+ </div>
520
+ <div class="zahlen-error-message" id="zahlen-otp-error" style="display: none;"></div>
521
+ </div>
522
+
523
+ <div class="zahlen-otp-timer" id="zahlen-otp-timer">
524
+ Resend code in <span id="zahlen-timer-count">60</span>s
525
+ </div>
526
+
527
+ <button class="zahlen-resend-btn" id="zahlen-resend-otp" disabled>
528
+ Resend OTP
529
+ </button>
530
+
531
+ <button type="button" class="zahlen-submit-btn" id="zahlen-verify-otp">
532
+ <span>🔐 Verify & Pay</span>
533
+ </button>
534
+
535
+ <button class="zahlen-back-btn" id="zahlen-back-to-card">
536
+ ← Back to card details
537
+ </button>
538
+ </div>
539
+ `;
540
+ this.attachOTPEventListeners();
541
+ this.startOTPTimer();
542
+ }
543
+ /**
544
+ * Attach OTP-specific event listeners
545
+ */ attachOTPEventListeners() {
546
+ var _a, _b, _c, _d, _e;
547
+ const otpInputs = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelectorAll('.zahlen-otp-digit');
548
+ otpInputs === null || otpInputs === void 0 ? void 0 : otpInputs.forEach((input, index)=>{
549
+ input.addEventListener('input', (e)=>this.handleOTPInput(e, index));
550
+ input.addEventListener('keydown', (e)=>this.handleOTPKeydown(e, index));
551
+ input.addEventListener('paste', (e)=>this.handleOTPPaste(e));
552
+ });
553
+ // Verify button
554
+ const verifyBtn = (_b = this.container) === null || _b === void 0 ? void 0 : _b.querySelector('#zahlen-verify-otp');
555
+ verifyBtn === null || verifyBtn === void 0 ? void 0 : verifyBtn.addEventListener('click', ()=>this.handleOTPSubmit());
556
+ // Resend button
557
+ const resendBtn = (_c = this.container) === null || _c === void 0 ? void 0 : _c.querySelector('#zahlen-resend-otp');
558
+ resendBtn === null || resendBtn === void 0 ? void 0 : resendBtn.addEventListener('click', ()=>this.handleResendOTP());
559
+ // Back button
560
+ const backBtn = (_d = this.container) === null || _d === void 0 ? void 0 : _d.querySelector('#zahlen-back-to-card');
561
+ backBtn === null || backBtn === void 0 ? void 0 : backBtn.addEventListener('click', ()=>this.handleBackToCard());
562
+ // Focus first input
563
+ (_e = otpInputs === null || otpInputs === void 0 ? void 0 : otpInputs[0]) === null || _e === void 0 ? void 0 : _e.focus();
564
+ }
565
+ /**
566
+ * Handle OTP digit input
567
+ */ handleOTPInput(e, index) {
568
+ var _a;
569
+ const input = e.target;
570
+ const value = input.value.replace(/\D/g, '');
571
+ input.value = value;
572
+ if (value && index < 5) {
573
+ const nextInput = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector(`[data-index="${index + 1}"]`);
574
+ nextInput === null || nextInput === void 0 ? void 0 : nextInput.focus();
575
+ }
576
+ this.updateOTPState();
577
+ this.clearError('otp');
578
+ }
579
+ /**
580
+ * Handle OTP keydown for backspace navigation
581
+ */ handleOTPKeydown(e, index) {
582
+ var _a;
583
+ const input = e.target;
584
+ if (e.key === 'Backspace' && !input.value && index > 0) {
585
+ const prevInput = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector(`[data-index="${index - 1}"]`);
586
+ prevInput === null || prevInput === void 0 ? void 0 : prevInput.focus();
587
+ }
588
+ }
589
+ /**
590
+ * Handle OTP paste
591
+ */ handleOTPPaste(e) {
592
+ var _a, _b;
593
+ e.preventDefault();
594
+ const pastedData = (_a = e.clipboardData) === null || _a === void 0 ? void 0 : _a.getData('text').replace(/\D/g, '').slice(0, 6);
595
+ if (pastedData) {
596
+ const inputs = (_b = this.container) === null || _b === void 0 ? void 0 : _b.querySelectorAll('.zahlen-otp-digit');
597
+ pastedData.split('').forEach((digit, i)=>{
598
+ if (inputs[i]) {
599
+ inputs[i].value = digit;
600
+ }
601
+ });
602
+ this.updateOTPState();
603
+ }
604
+ }
605
+ /**
606
+ * Update OTP state from inputs
607
+ */ updateOTPState() {
608
+ var _a;
609
+ const inputs = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelectorAll('.zahlen-otp-digit');
610
+ let otp = '';
611
+ inputs === null || inputs === void 0 ? void 0 : inputs.forEach((input)=>otp += input.value);
612
+ this.state.otp = otp;
613
+ }
614
+ /**
615
+ * Start OTP resend timer
616
+ */ startOTPTimer() {
617
+ var _a, _b, _c;
618
+ const timerEl = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#zahlen-timer-count');
619
+ const timerContainer = (_b = this.container) === null || _b === void 0 ? void 0 : _b.querySelector('#zahlen-otp-timer');
620
+ const resendBtn = (_c = this.container) === null || _c === void 0 ? void 0 : _c.querySelector('#zahlen-resend-otp');
621
+ this.state.otpTimerInterval = window.setInterval(()=>{
622
+ this.state.otpTimer--;
623
+ if (timerEl) {
624
+ timerEl.textContent = String(this.state.otpTimer);
625
+ }
626
+ if (this.state.otpTimer <= 0) {
627
+ if (this.state.otpTimerInterval) {
628
+ clearInterval(this.state.otpTimerInterval);
629
+ }
630
+ this.state.canResendOtp = true;
631
+ if (timerContainer) timerContainer.style.display = 'none';
632
+ if (resendBtn) resendBtn.disabled = false;
633
+ }
634
+ }, 1000);
635
+ }
636
+ /**
637
+ * Handle OTP submission
638
+ */ handleOTPSubmit() {
639
+ return __awaiter(this, void 0, void 0, function*() {
640
+ if (this.state.otp.length !== 6) {
641
+ this.showError('otp', 'Please enter the complete 6-digit code');
642
+ return;
643
+ }
644
+ this.setOTPProcessing(true);
645
+ try {
646
+ // Simulate OTP verification
647
+ yield this.verifyOTP();
648
+ // Clear timer
649
+ if (this.state.otpTimerInterval) {
650
+ clearInterval(this.state.otpTimerInterval);
651
+ }
652
+ // Process final payment
653
+ const result = yield this.processPayment();
654
+ this.state.isSuccess = true;
655
+ this.renderSuccessView();
656
+ this.options.onSuccess(result);
657
+ } catch (error) {
658
+ this.setOTPProcessing(false);
659
+ const paymentError = error;
660
+ this.showError('otp', paymentError.message || 'Invalid OTP. Please try again.');
661
+ }
662
+ });
663
+ }
664
+ /**
665
+ * Set OTP processing state
666
+ */ setOTPProcessing(isProcessing) {
667
+ var _a;
668
+ const verifyBtn = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#zahlen-verify-otp');
669
+ if (verifyBtn) {
670
+ verifyBtn.disabled = isProcessing;
671
+ verifyBtn.innerHTML = isProcessing ? '<div class="zahlen-spinner"></div><span>Verifying...</span>' : '<span>🔐 Verify & Pay</span>';
672
+ }
673
+ }
674
+ /**
675
+ * Verify OTP (placeholder - integrate with backend)
676
+ */ verifyOTP() {
677
+ return __awaiter(this, void 0, void 0, function*() {
678
+ yield new Promise((resolve)=>setTimeout(resolve, 1500));
679
+ // Simulated OTP validation - accept any 6 digits for demo
680
+ // In production, send to your API for verification
681
+ if (this.state.otp.length !== 6) {
682
+ throw {
683
+ code: 'INVALID_OTP',
684
+ message: 'Invalid OTP',
685
+ recoverable: true
686
+ };
687
+ }
688
+ });
689
+ }
690
+ /**
691
+ * Handle resend OTP
692
+ */ handleResendOTP() {
693
+ var _a, _b, _c, _d;
694
+ const timerContainer = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#zahlen-otp-timer');
695
+ const resendBtn = (_b = this.container) === null || _b === void 0 ? void 0 : _b.querySelector('#zahlen-resend-otp');
696
+ // Reset timer
697
+ this.state.otpTimer = 60;
698
+ this.state.canResendOtp = false;
699
+ if (timerContainer) timerContainer.style.display = 'block';
700
+ if (resendBtn) resendBtn.disabled = true;
701
+ // Clear old inputs
702
+ const inputs = (_c = this.container) === null || _c === void 0 ? void 0 : _c.querySelectorAll('.zahlen-otp-digit');
703
+ inputs === null || inputs === void 0 ? void 0 : inputs.forEach((input)=>input.value = '');
704
+ this.state.otp = '';
705
+ // Restart timer
706
+ this.startOTPTimer();
707
+ // Focus first input
708
+ (_d = inputs === null || inputs === void 0 ? void 0 : inputs[0]) === null || _d === void 0 ? void 0 : _d.focus();
709
+ }
710
+ /**
711
+ * Handle back to card details
712
+ */ handleBackToCard() {
713
+ var _a;
714
+ if (this.state.otpTimerInterval) {
715
+ clearInterval(this.state.otpTimerInterval);
716
+ }
717
+ this.state.isOtpStep = false;
718
+ this.state.otp = '';
719
+ // Re-render full modal
720
+ if (this.container) {
721
+ this.container.innerHTML = this.getModalHTML();
722
+ this.overlay = this.container.querySelector('.zahlen-overlay');
723
+ (_a = this.overlay) === null || _a === void 0 ? void 0 : _a.classList.add('zahlen-visible');
724
+ this.attachEventListeners();
725
+ // Restore previous card values
726
+ const cardInput = this.container.querySelector('#zahlen-card-number');
727
+ const expiryInput = this.container.querySelector('#zahlen-expiry');
728
+ const cvvInput = this.container.querySelector('#zahlen-cvv');
729
+ if (cardInput) cardInput.value = formatCardNumber(this.state.cardNumber);
730
+ if (expiryInput) expiryInput.value = this.state.expiry;
731
+ if (cvvInput) cvvInput.value = this.state.cvv;
732
+ }
733
+ }
493
734
  /**
494
735
  * Attach event listeners
495
736
  */ attachEventListeners() {
@@ -619,13 +860,12 @@ class ZahlenModal {
619
860
  // Show loading state
620
861
  this.setProcessing(true);
621
862
  try {
622
- // Simulate API call (replace with actual API integration)
623
- const result = yield this.processPayment();
624
- // Show success
625
- this.state.isSuccess = true;
626
- this.renderSuccessView();
627
- // Callback
628
- this.options.onSuccess(result);
863
+ // Simulate sending OTP (in production, send to your API)
864
+ yield new Promise((resolve)=>setTimeout(resolve, 1000));
865
+ // Hide processing state
866
+ this.setProcessing(false);
867
+ // Show OTP verification view
868
+ this.renderOTPView();
629
869
  } catch (error) {
630
870
  this.setProcessing(false);
631
871
  const paymentError = error;
@@ -792,7 +1032,30 @@ class ZahlenModal {
792
1032
  .zahlen-success-btn { padding: 12px 32px; background: var(--zahlen-surface); border: 1.5px solid var(--zahlen-border); border-radius: var(--zahlen-border-radius); color: var(--zahlen-text); font-size: 1rem; font-weight: 500; cursor: pointer; transition: all var(--zahlen-transition-fast); }
793
1033
  .zahlen-success-btn:hover { background: var(--zahlen-surface-hover); border-color: var(--zahlen-primary); color: var(--zahlen-primary); }
794
1034
 
795
- @media (max-width: 480px) { .zahlen-modal { max-width: 100%; max-height: 100%; border-radius: 0; height: 100%; } .zahlen-body { padding: 20px; } }
1035
+ /* OTP View Styles */
1036
+ .zahlen-otp-view { text-align: center; padding: 24px 16px; }
1037
+ .zahlen-otp-header { margin-bottom: 24px; }
1038
+ .zahlen-otp-icon { font-size: 3rem; margin-bottom: 12px; }
1039
+ .zahlen-otp-title { font-size: 1.25rem; font-weight: 700; color: var(--zahlen-text); margin-bottom: 8px; }
1040
+ .zahlen-otp-subtitle { font-size: 0.875rem; color: var(--zahlen-text-muted); }
1041
+
1042
+ .zahlen-otp-inputs-container { margin-bottom: 16px; }
1043
+ .zahlen-otp-inputs { display: flex; justify-content: center; gap: 8px; margin-bottom: 8px; }
1044
+ .zahlen-otp-digit { width: 48px; height: 56px; text-align: center; font-size: 1.5rem; font-weight: 700; border: 2px solid var(--zahlen-border); border-radius: var(--zahlen-border-radius); background: var(--zahlen-input-bg); color: var(--zahlen-text); outline: none; transition: all var(--zahlen-transition-fast); }
1045
+ .zahlen-otp-digit:focus { border-color: var(--zahlen-primary); box-shadow: 0 0 0 4px var(--zahlen-glow); }
1046
+ .zahlen-otp-digit.filled { border-color: var(--zahlen-primary); background: var(--zahlen-surface); }
1047
+
1048
+ .zahlen-otp-timer { font-size: 0.875rem; color: var(--zahlen-text-muted); margin-bottom: 12px; }
1049
+ .zahlen-otp-timer span { font-weight: 700; color: var(--zahlen-primary); }
1050
+
1051
+ .zahlen-resend-btn { padding: 8px 16px; background: transparent; border: 1px solid var(--zahlen-border); border-radius: var(--zahlen-border-radius); color: var(--zahlen-text-muted); font-size: 0.875rem; cursor: pointer; margin-bottom: 16px; transition: all var(--zahlen-transition-fast); }
1052
+ .zahlen-resend-btn:hover:not(:disabled) { border-color: var(--zahlen-primary); color: var(--zahlen-primary); }
1053
+ .zahlen-resend-btn:disabled { opacity: 0.5; cursor: not-allowed; }
1054
+
1055
+ .zahlen-back-btn { display: block; width: 100%; padding: 12px; background: transparent; border: none; color: var(--zahlen-text-muted); font-size: 0.875rem; cursor: pointer; margin-top: 12px; transition: color var(--zahlen-transition-fast); }
1056
+ .zahlen-back-btn:hover { color: var(--zahlen-primary); }
1057
+
1058
+ @media (max-width: 480px) { .zahlen-modal { max-width: 100%; max-height: 100%; border-radius: 0; height: 100%; } .zahlen-body { padding: 20px; } .zahlen-otp-digit { width: 42px; height: 50px; font-size: 1.25rem; } }
796
1059
  `;
797
1060
  }
798
1061
  constructor(options){
@@ -809,9 +1072,14 @@ class ZahlenModal {
809
1072
  cardNumber: '',
810
1073
  expiry: '',
811
1074
  cvv: '',
1075
+ otp: '',
812
1076
  errors: {},
813
1077
  isProcessing: false,
814
- isSuccess: false
1078
+ isSuccess: false,
1079
+ isOtpStep: false,
1080
+ otpTimer: 60,
1081
+ canResendOtp: false,
1082
+ otpTimerInterval: null
815
1083
  };
816
1084
  }
817
1085
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zahlen/checkout",
3
- "version": "0.1.2",
3
+ "version": "0.2.1",
4
4
  "description": "Modern, Gen Z-friendly payment checkout modal for the web",
5
5
  "author": "Zahlen",
6
6
  "license": "MIT",
@@ -7,13 +7,19 @@ export interface ModalState {
7
7
  cardNumber: string;
8
8
  expiry: string;
9
9
  cvv: string;
10
+ otp: string;
10
11
  errors: {
11
12
  cardNumber?: string;
12
13
  expiry?: string;
13
14
  cvv?: string;
15
+ otp?: string;
14
16
  };
15
17
  isProcessing: boolean;
16
18
  isSuccess: boolean;
19
+ isOtpStep: boolean;
20
+ otpTimer: number;
21
+ canResendOtp: boolean;
22
+ otpTimerInterval: number | null;
17
23
  }
18
24
  export declare class ZahlenModal {
19
25
  private options;
@@ -45,6 +51,54 @@ export declare class ZahlenModal {
45
51
  * Render success view
46
52
  */
47
53
  private renderSuccessView;
54
+ /**
55
+ * Render OTP verification view
56
+ */
57
+ private renderOTPView;
58
+ /**
59
+ * Attach OTP-specific event listeners
60
+ */
61
+ private attachOTPEventListeners;
62
+ /**
63
+ * Handle OTP digit input
64
+ */
65
+ private handleOTPInput;
66
+ /**
67
+ * Handle OTP keydown for backspace navigation
68
+ */
69
+ private handleOTPKeydown;
70
+ /**
71
+ * Handle OTP paste
72
+ */
73
+ private handleOTPPaste;
74
+ /**
75
+ * Update OTP state from inputs
76
+ */
77
+ private updateOTPState;
78
+ /**
79
+ * Start OTP resend timer
80
+ */
81
+ private startOTPTimer;
82
+ /**
83
+ * Handle OTP submission
84
+ */
85
+ private handleOTPSubmit;
86
+ /**
87
+ * Set OTP processing state
88
+ */
89
+ private setOTPProcessing;
90
+ /**
91
+ * Verify OTP (placeholder - integrate with backend)
92
+ */
93
+ private verifyOTP;
94
+ /**
95
+ * Handle resend OTP
96
+ */
97
+ private handleResendOTP;
98
+ /**
99
+ * Handle back to card details
100
+ */
101
+ private handleBackToCard;
48
102
  /**
49
103
  * Attach event listeners
50
104
  */
package/README.md DELETED
@@ -1,257 +0,0 @@
1
- # @zahlen/checkout
2
-
3
- <p align="center">
4
- <img src="https://img.shields.io/npm/v/@zahlen/checkout?style=flat-square" alt="npm version" />
5
- <img src="https://img.shields.io/npm/l/@zahlen/checkout?style=flat-square" alt="license" />
6
- <img src="https://img.shields.io/bundlephobia/minzip/@zahlen/checkout?style=flat-square" alt="bundle size" />
7
- </p>
8
-
9
- <p align="center">
10
- <strong>A modern, Gen Z-friendly payment checkout modal for the web.</strong>
11
- <br />
12
- Beautiful • Fast • Easy to integrate
13
- </p>
14
-
15
- ---
16
-
17
- ## ✨ Features
18
-
19
- - 🎨 **Modern UI** - Glassmorphism design with dark mode by default
20
- - ⚡ **Lightweight** - Under 10KB gzipped
21
- - 🔒 **Secure** - PCI-compliant, no card data touches your servers
22
- - 📱 **Responsive** - Works beautifully on all devices
23
- - 🌍 **Multi-currency** - Supports NGN, USD, EUR, GBP, and more
24
- - 🎭 **Customizable** - Theming via CSS variables
25
- - 💳 **Smart Detection** - Auto-detects card brands (Visa, Mastercard, Verve, etc.)
26
-
27
- ---
28
-
29
- ## 🚀 Quick Start
30
-
31
- ### CDN (Recommended for quick setup)
32
-
33
- ```html
34
- <!-- Add the script -->
35
- <script src="https://unpkg.com/@zahlen/checkout"></script>
36
-
37
- <!-- Add a pay button -->
38
- <button id="pay-btn">Pay ₦4,999</button>
39
-
40
- <script>
41
- // Initialize
42
- Zahlen.init({ apiKey: 'pk_live_your_api_key' });
43
-
44
- // Handle click
45
- document.getElementById('pay-btn').onclick = () => {
46
- Zahlen.checkout({
47
- amount: 499900, // Amount in kobo (₦4,999)
48
- currency: 'NGN',
49
- description: 'Premium Plan',
50
- onSuccess: (result) => {
51
- console.log('Payment successful!', result);
52
- // Redirect to success page
53
- },
54
- onError: (error) => {
55
- console.error('Payment failed:', error.message);
56
- }
57
- });
58
- };
59
- </script>
60
- ```
61
-
62
- ### npm / yarn
63
-
64
- ```bash
65
- npm install @zahlen/checkout
66
- # or
67
- yarn add @zahlen/checkout
68
- ```
69
-
70
- ```typescript
71
- import { Zahlen } from '@zahlen/checkout';
72
-
73
- Zahlen.init({ apiKey: 'pk_live_your_api_key' });
74
-
75
- function handlePayment() {
76
- Zahlen.checkout({
77
- amount: 499900,
78
- currency: 'NGN',
79
- description: 'Premium Plan',
80
- customerEmail: 'user@example.com',
81
- onSuccess: (result) => {
82
- console.log('Paid!', result);
83
- },
84
- onError: (error) => {
85
- console.error('Failed:', error);
86
- },
87
- onClose: () => {
88
- console.log('Modal closed');
89
- }
90
- });
91
- }
92
- ```
93
-
94
- ---
95
-
96
- ## 📖 API Reference
97
-
98
- ### `Zahlen.init(config)`
99
-
100
- Initialize the SDK. Call this once when your app loads.
101
-
102
- ```typescript
103
- interface ZahlenConfig {
104
- apiKey: string; // Required: Your API key
105
- theme?: 'dark' | 'light' | 'auto'; // Default: 'dark'
106
- locale?: string; // Default: 'en-US'
107
- customStyles?: Partial<ZahlenTheme>;
108
- }
109
- ```
110
-
111
- ### `Zahlen.checkout(options)`
112
-
113
- Open the checkout modal.
114
-
115
- ```typescript
116
- interface CheckoutOptions {
117
- amount: number; // Required: Amount in smallest unit (kobo/cents)
118
- currency: string; // Required: ISO currency code
119
- description?: string; // Optional: Payment description
120
- customerEmail?: string; // Optional: For receipts
121
- metadata?: object; // Optional: Custom data
122
- onSuccess: (result: PaymentResult) => void;
123
- onError: (error: PaymentError) => void;
124
- onClose?: () => void;
125
- }
126
- ```
127
-
128
- ### `Zahlen.setTheme(theme)`
129
-
130
- Change the theme dynamically.
131
-
132
- ```typescript
133
- Zahlen.setTheme('light'); // or 'dark' or 'auto'
134
- ```
135
-
136
- ### `Zahlen.closeModal()`
137
-
138
- Programmatically close the modal.
139
-
140
- ---
141
-
142
- ## 🎨 Customization
143
-
144
- ### Custom Colors
145
-
146
- ```typescript
147
- Zahlen.init({
148
- apiKey: 'pk_live_xxx',
149
- customStyles: {
150
- '--zahlen-primary': '#FF6B6B',
151
- '--zahlen-secondary': '#4ECDC4',
152
- '--zahlen-bg': '#1A1A2E',
153
- }
154
- });
155
- ```
156
-
157
- ### Available CSS Variables
158
-
159
- | Variable | Default | Description |
160
- |----------|---------|-------------|
161
- | `--zahlen-primary` | `#7C3AED` | Primary brand color |
162
- | `--zahlen-secondary` | `#4F46E5` | Secondary/gradient color |
163
- | `--zahlen-bg` | `#0F0F23` | Modal background |
164
- | `--zahlen-surface` | `rgba(255,255,255,0.05)` | Input backgrounds |
165
- | `--zahlen-border` | `rgba(255,255,255,0.1)` | Border color |
166
- | `--zahlen-text` | `#FFFFFF` | Primary text |
167
- | `--zahlen-text-muted` | `#A0A0B0` | Muted text |
168
- | `--zahlen-border-radius` | `12px` | Border radius |
169
-
170
- ---
171
-
172
- ## 📱 Framework Integrations
173
-
174
- ### React
175
-
176
- ```bash
177
- npm install @zahlen/checkout-react
178
- ```
179
-
180
- ```tsx
181
- import { ZahlenButton, ZahlenProvider } from '@zahlen/checkout-react';
182
-
183
- function App() {
184
- return (
185
- <ZahlenProvider apiKey="pk_live_xxx">
186
- <ZahlenButton
187
- amount={499900}
188
- currency="NGN"
189
- onSuccess={(result) => console.log('Paid!', result)}
190
- onError={(error) => console.error(error)}
191
- >
192
- Pay ₦4,999
193
- </ZahlenButton>
194
- </ZahlenProvider>
195
- );
196
- }
197
- ```
198
-
199
- ### Angular
200
-
201
- ```bash
202
- npm install @zahlen/checkout-angular
203
- ```
204
-
205
- ```typescript
206
- // app.module.ts
207
- import { ZahlenModule } from '@zahlen/checkout-angular';
208
-
209
- @NgModule({
210
- imports: [ZahlenModule.forRoot({ apiKey: 'pk_live_xxx' })]
211
- })
212
- export class AppModule {}
213
- ```
214
-
215
- ```html
216
- <!-- component.html -->
217
- <button zahlenCheckout
218
- [amount]="499900"
219
- [currency]="'NGN'"
220
- (success)="onSuccess($event)"
221
- (error)="onError($event)">
222
- Pay ₦4,999
223
- </button>
224
- ```
225
-
226
- ---
227
-
228
- ## 💳 Supported Cards
229
-
230
- | Card Brand | Supported |
231
- |------------|-----------|
232
- | Visa | ✅ |
233
- | Mastercard | ✅ |
234
- | Verve | ✅ |
235
- | American Express | ✅ |
236
- | Discover | ✅ |
237
-
238
- ---
239
-
240
- ## 🔒 Security
241
-
242
- - All card data is encrypted and processed securely
243
- - PCI DSS compliant
244
- - No sensitive data stored on your servers
245
- - Tokenized payments
246
-
247
- ---
248
-
249
- ## 📄 License
250
-
251
- MIT © Zahlen
252
-
253
- ---
254
-
255
- <p align="center">
256
- Made with 💜 by the Zahlen team
257
- </p>