@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 +277 -9
- package/index.esm.js +277 -9
- package/package.json +1 -1
- package/src/components/modal.d.ts +54 -0
- package/README.md +0 -257
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
|
|
627
|
-
|
|
628
|
-
//
|
|
629
|
-
this.
|
|
630
|
-
|
|
631
|
-
|
|
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
|
-
|
|
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
|
|
623
|
-
|
|
624
|
-
//
|
|
625
|
-
this.
|
|
626
|
-
|
|
627
|
-
|
|
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
|
-
|
|
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
|
@@ -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>
|