@voxepay/checkout 0.1.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.
@@ -0,0 +1,1098 @@
1
+ /**
2
+ * Card validation utilities for VoxePay Checkout
3
+ */
4
+ const CARD_BRANDS = [
5
+ {
6
+ name: 'Visa',
7
+ code: 'visa',
8
+ pattern: /^4/,
9
+ lengths: [13, 16, 19],
10
+ cvvLength: 3,
11
+ },
12
+ {
13
+ name: 'Mastercard',
14
+ code: 'mastercard',
15
+ pattern: /^(5[1-5]|2[2-7])/,
16
+ lengths: [16],
17
+ cvvLength: 3,
18
+ },
19
+ {
20
+ name: 'Verve',
21
+ code: 'verve',
22
+ pattern: /^(506[0-9]|507[0-9]|6500)/,
23
+ lengths: [16, 18, 19],
24
+ cvvLength: 3,
25
+ },
26
+ {
27
+ name: 'American Express',
28
+ code: 'amex',
29
+ pattern: /^3[47]/,
30
+ lengths: [15],
31
+ cvvLength: 4,
32
+ },
33
+ {
34
+ name: 'Discover',
35
+ code: 'discover',
36
+ pattern: /^(6011|65|64[4-9])/,
37
+ lengths: [16, 19],
38
+ cvvLength: 3,
39
+ },
40
+ ];
41
+ /**
42
+ * Detect card brand from card number
43
+ */
44
+ function detectCardBrand(cardNumber) {
45
+ const cleaned = cardNumber.replace(/\s/g, '');
46
+ for (const brand of CARD_BRANDS) {
47
+ if (brand.pattern.test(cleaned)) {
48
+ return brand;
49
+ }
50
+ }
51
+ return null;
52
+ }
53
+ /**
54
+ * Luhn algorithm for card validation
55
+ */
56
+ function luhnCheck(cardNumber) {
57
+ const cleaned = cardNumber.replace(/\s/g, '');
58
+ if (!/^\d+$/.test(cleaned))
59
+ return false;
60
+ let sum = 0;
61
+ let isEven = false;
62
+ for (let i = cleaned.length - 1; i >= 0; i--) {
63
+ let digit = parseInt(cleaned[i], 10);
64
+ if (isEven) {
65
+ digit *= 2;
66
+ if (digit > 9) {
67
+ digit -= 9;
68
+ }
69
+ }
70
+ sum += digit;
71
+ isEven = !isEven;
72
+ }
73
+ return sum % 10 === 0;
74
+ }
75
+ /**
76
+ * Validate card number
77
+ */
78
+ function validateCardNumber(cardNumber) {
79
+ const cleaned = cardNumber.replace(/\s/g, '');
80
+ if (!cleaned) {
81
+ return { valid: false, error: 'Card number is required' };
82
+ }
83
+ if (!/^\d+$/.test(cleaned)) {
84
+ return { valid: false, error: 'Invalid card number' };
85
+ }
86
+ const brand = detectCardBrand(cleaned);
87
+ if (!brand) {
88
+ return { valid: false, error: 'Unsupported card type' };
89
+ }
90
+ if (!brand.lengths.includes(cleaned.length)) {
91
+ return { valid: false, error: 'Invalid card number length' };
92
+ }
93
+ if (!luhnCheck(cleaned)) {
94
+ return { valid: false, error: 'Invalid card number' };
95
+ }
96
+ return { valid: true };
97
+ }
98
+ /**
99
+ * Validate expiry date
100
+ */
101
+ function validateExpiry(expiry) {
102
+ const cleaned = expiry.replace(/\s/g, '');
103
+ if (!cleaned) {
104
+ return { valid: false, error: 'Expiry date is required' };
105
+ }
106
+ const match = cleaned.match(/^(\d{2})\/(\d{2})$/);
107
+ if (!match) {
108
+ return { valid: false, error: 'Invalid format (MM/YY)' };
109
+ }
110
+ const month = parseInt(match[1], 10);
111
+ const year = parseInt(match[2], 10) + 2000;
112
+ if (month < 1 || month > 12) {
113
+ return { valid: false, error: 'Invalid month' };
114
+ }
115
+ const now = new Date();
116
+ const currentYear = now.getFullYear();
117
+ const currentMonth = now.getMonth() + 1;
118
+ if (year < currentYear || (year === currentYear && month < currentMonth)) {
119
+ return { valid: false, error: 'Card has expired' };
120
+ }
121
+ if (year > currentYear + 20) {
122
+ return { valid: false, error: 'Invalid expiry year' };
123
+ }
124
+ return { valid: true };
125
+ }
126
+ /**
127
+ * Validate CVV
128
+ */
129
+ function validateCVV(cvv, cardNumber) {
130
+ const cleaned = cvv.replace(/\s/g, '');
131
+ if (!cleaned) {
132
+ return { valid: false, error: 'CVV is required' };
133
+ }
134
+ if (!/^\d+$/.test(cleaned)) {
135
+ return { valid: false, error: 'Invalid CVV' };
136
+ }
137
+ const brand = cardNumber ? detectCardBrand(cardNumber) : null;
138
+ const expectedLength = (brand === null || brand === void 0 ? void 0 : brand.cvvLength) || 3;
139
+ if (cleaned.length !== expectedLength && cleaned.length !== 3 && cleaned.length !== 4) {
140
+ return { valid: false, error: `CVV must be ${expectedLength} digits` };
141
+ }
142
+ return { valid: true };
143
+ }
144
+
145
+ /**
146
+ * Input formatting utilities for VoxePay Checkout
147
+ */
148
+ /**
149
+ * Format card number with spaces every 4 digits
150
+ */
151
+ function formatCardNumber(value) {
152
+ const cleaned = value.replace(/\D/g, '');
153
+ const groups = cleaned.match(/.{1,4}/g) || [];
154
+ return groups.join(' ').slice(0, 23); // Max: 19 digits + 4 spaces
155
+ }
156
+ /**
157
+ * Format expiry date as MM/YY
158
+ */
159
+ function formatExpiry(value) {
160
+ const cleaned = value.replace(/\D/g, '');
161
+ if (cleaned.length === 0)
162
+ return '';
163
+ if (cleaned.length === 1) {
164
+ return parseInt(cleaned) > 1 ? `0${cleaned}` : cleaned;
165
+ }
166
+ if (cleaned.length === 2) {
167
+ const month = parseInt(cleaned);
168
+ if (month > 12)
169
+ return '12';
170
+ if (month === 0)
171
+ return '01';
172
+ return cleaned;
173
+ }
174
+ const month = cleaned.slice(0, 2);
175
+ const year = cleaned.slice(2, 4);
176
+ return `${month}/${year}`;
177
+ }
178
+ /**
179
+ * Format CVV (numbers only, max 4 digits)
180
+ */
181
+ function formatCVV(value) {
182
+ return value.replace(/\D/g, '').slice(0, 4);
183
+ }
184
+ /**
185
+ * Format currency amount
186
+ */
187
+ function formatAmount(amount, currency) {
188
+ const formatter = new Intl.NumberFormat('en-US', {
189
+ style: 'currency',
190
+ currency: currency,
191
+ minimumFractionDigits: 2,
192
+ });
193
+ // Convert from smallest unit (cents/kobo) to main unit
194
+ return formatter.format(amount / 100);
195
+ }
196
+ /**
197
+ * Get currency symbol
198
+ */
199
+ function getCurrencySymbol(currency) {
200
+ const symbols = {
201
+ NGN: '₦',
202
+ USD: '$',
203
+ EUR: '€',
204
+ GBP: '£',
205
+ GHS: '₵',
206
+ KES: 'KSh',
207
+ ZAR: 'R',
208
+ };
209
+ return symbols[currency.toUpperCase()] || currency;
210
+ }
211
+
212
+ /**
213
+ * VoxePay Checkout Modal Component
214
+ * A modern, glassmorphism payment modal
215
+ */
216
+ // SVG Icons
217
+ const ICONS = {
218
+ close: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
219
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
220
+ </svg>`,
221
+ lock: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
222
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
223
+ </svg>`,
224
+ check: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3">
225
+ <path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
226
+ </svg>`,
227
+ error: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20" stroke="currentColor" stroke-width="2">
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
+ </svg>`,
230
+ };
231
+ // Card brand logos
232
+ const CARD_BRAND_DISPLAY = {
233
+ visa: 'VISA',
234
+ mastercard: 'MC',
235
+ amex: 'AMEX',
236
+ verve: 'VERVE',
237
+ discover: 'DISC',
238
+ };
239
+ class VoxePayModal {
240
+ constructor(options) {
241
+ this.container = null;
242
+ this.overlay = null;
243
+ this.handleEscape = (e) => {
244
+ if (e.key === 'Escape') {
245
+ this.close();
246
+ document.removeEventListener('keydown', this.handleEscape);
247
+ }
248
+ };
249
+ this.options = options;
250
+ this.state = {
251
+ cardNumber: '',
252
+ expiry: '',
253
+ cvv: '',
254
+ otp: '',
255
+ errors: {},
256
+ isProcessing: false,
257
+ isSuccess: false,
258
+ isOtpStep: false,
259
+ otpTimer: 60,
260
+ canResendOtp: false,
261
+ otpTimerInterval: null,
262
+ };
263
+ }
264
+ /**
265
+ * Open the checkout modal
266
+ */
267
+ open() {
268
+ this.injectStyles();
269
+ this.render();
270
+ this.attachEventListeners();
271
+ // Trigger open animation
272
+ requestAnimationFrame(() => {
273
+ var _a;
274
+ (_a = this.overlay) === null || _a === void 0 ? void 0 : _a.classList.add('voxepay-visible');
275
+ });
276
+ }
277
+ /**
278
+ * Close the checkout modal
279
+ */
280
+ close() {
281
+ var _a;
282
+ (_a = this.overlay) === null || _a === void 0 ? void 0 : _a.classList.remove('voxepay-visible');
283
+ setTimeout(() => {
284
+ var _a, _b, _c;
285
+ (_a = this.container) === null || _a === void 0 ? void 0 : _a.remove();
286
+ this.container = null;
287
+ this.overlay = null;
288
+ (_c = (_b = this.options).onClose) === null || _c === void 0 ? void 0 : _c.call(_b);
289
+ }, 300);
290
+ }
291
+ /**
292
+ * Inject styles if not already present
293
+ */
294
+ injectStyles() {
295
+ if (document.getElementById('voxepay-checkout-styles'))
296
+ return;
297
+ const style = document.createElement('style');
298
+ style.id = 'voxepay-checkout-styles';
299
+ style.textContent = this.getStyles();
300
+ document.head.appendChild(style);
301
+ }
302
+ /**
303
+ * Render the modal HTML
304
+ */
305
+ render() {
306
+ this.container = document.createElement('div');
307
+ this.container.className = 'voxepay-checkout';
308
+ this.container.innerHTML = this.getModalHTML();
309
+ document.body.appendChild(this.container);
310
+ this.overlay = this.container.querySelector('.voxepay-overlay');
311
+ }
312
+ /**
313
+ * Get modal HTML
314
+ */
315
+ getModalHTML() {
316
+ const formattedAmount = formatAmount(this.options.amount, this.options.currency);
317
+ return `
318
+ <div class="voxepay-overlay">
319
+ <div class="voxepay-modal" role="dialog" aria-modal="true" aria-labelledby="voxepay-title">
320
+ <div class="voxepay-header">
321
+ <div class="voxepay-header-left">
322
+ <div class="voxepay-logo">V</div>
323
+ <div>
324
+ <div class="voxepay-amount" id="voxepay-title">Pay ${formattedAmount}</div>
325
+ ${this.options.description ? `<div style="font-size: 0.875rem; color: var(--voxepay-text-muted);">${this.options.description}</div>` : ''}
326
+ </div>
327
+ </div>
328
+ <button class="voxepay-close" aria-label="Close" data-action="close">
329
+ ${ICONS.close}
330
+ </button>
331
+ </div>
332
+
333
+ <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>
397
+ </div>
398
+
399
+ <div class="voxepay-footer">
400
+ <div class="voxepay-powered-by">
401
+ ${ICONS.lock}
402
+ <span>Secured by <strong>VoxePay</strong></span>
403
+ </div>
404
+ </div>
405
+ </div>
406
+ </div>
407
+ `;
408
+ }
409
+ /**
410
+ * Render success view
411
+ */
412
+ renderSuccessView() {
413
+ var _a;
414
+ const formContainer = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#voxepay-form-container');
415
+ if (!formContainer)
416
+ return;
417
+ const formattedAmount = formatAmount(this.options.amount, this.options.currency);
418
+ formContainer.innerHTML = `
419
+ <div class="voxepay-success-view">
420
+ <div class="voxepay-success-icon">
421
+ ${ICONS.check}
422
+ </div>
423
+ <h2 class="voxepay-success-title">Payment Successful!</h2>
424
+ <p class="voxepay-success-message">Your payment of ${formattedAmount} has been processed.</p>
425
+ <button class="voxepay-success-btn" data-action="close">Done</button>
426
+ </div>
427
+ `;
428
+ // Re-attach close listener
429
+ const closeBtn = formContainer.querySelector('[data-action="close"]');
430
+ closeBtn === null || closeBtn === void 0 ? void 0 : closeBtn.addEventListener('click', () => this.close());
431
+ }
432
+ /**
433
+ * Render OTP verification view
434
+ */
435
+ renderOTPView() {
436
+ var _a;
437
+ const formContainer = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#voxepay-form-container');
438
+ if (!formContainer)
439
+ return;
440
+ this.state.isOtpStep = true;
441
+ this.state.otpTimer = 60;
442
+ this.state.canResendOtp = false;
443
+ const maskedPhone = this.options.customerEmail
444
+ ? `****${this.options.customerEmail.slice(-4)}`
445
+ : '****1234';
446
+ formContainer.innerHTML = `
447
+ <div class="voxepay-otp-view">
448
+ <div class="voxepay-otp-header">
449
+ <div class="voxepay-otp-icon">📱</div>
450
+ <h3 class="voxepay-otp-title">Verify Your Payment</h3>
451
+ <p class="voxepay-otp-subtitle">We've sent a 6-digit code to ${maskedPhone}</p>
452
+ </div>
453
+
454
+ <div class="voxepay-otp-inputs-container">
455
+ <div class="voxepay-otp-inputs" id="voxepay-otp-inputs">
456
+ <input type="text" maxlength="1" class="voxepay-otp-digit" data-index="0" inputmode="numeric" autocomplete="one-time-code" />
457
+ <input type="text" maxlength="1" class="voxepay-otp-digit" data-index="1" inputmode="numeric" />
458
+ <input type="text" maxlength="1" class="voxepay-otp-digit" data-index="2" inputmode="numeric" />
459
+ <input type="text" maxlength="1" class="voxepay-otp-digit" data-index="3" inputmode="numeric" />
460
+ <input type="text" maxlength="1" class="voxepay-otp-digit" data-index="4" inputmode="numeric" />
461
+ <input type="text" maxlength="1" class="voxepay-otp-digit" data-index="5" inputmode="numeric" />
462
+ </div>
463
+ <div class="voxepay-error-message" id="voxepay-otp-error" style="display: none;"></div>
464
+ </div>
465
+
466
+ <div class="voxepay-otp-timer" id="voxepay-otp-timer">
467
+ Resend code in <span id="voxepay-timer-count">60</span>s
468
+ </div>
469
+
470
+ <button class="voxepay-resend-btn" id="voxepay-resend-otp" disabled>
471
+ Resend OTP
472
+ </button>
473
+
474
+ <button type="button" class="voxepay-submit-btn" id="voxepay-verify-otp">
475
+ <span>🔐 Verify & Pay</span>
476
+ </button>
477
+
478
+ <button class="voxepay-back-btn" id="voxepay-back-to-card">
479
+ ← Back to card details
480
+ </button>
481
+ </div>
482
+ `;
483
+ this.attachOTPEventListeners();
484
+ this.startOTPTimer();
485
+ }
486
+ /**
487
+ * Attach OTP-specific event listeners
488
+ */
489
+ attachOTPEventListeners() {
490
+ var _a, _b, _c, _d, _e;
491
+ const otpInputs = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelectorAll('.voxepay-otp-digit');
492
+ otpInputs === null || otpInputs === void 0 ? void 0 : otpInputs.forEach((input, index) => {
493
+ input.addEventListener('input', (e) => this.handleOTPInput(e, index));
494
+ input.addEventListener('keydown', (e) => this.handleOTPKeydown(e, index));
495
+ input.addEventListener('paste', (e) => this.handleOTPPaste(e));
496
+ });
497
+ const verifyBtn = (_b = this.container) === null || _b === void 0 ? void 0 : _b.querySelector('#voxepay-verify-otp');
498
+ verifyBtn === null || verifyBtn === void 0 ? void 0 : verifyBtn.addEventListener('click', () => this.handleOTPSubmit());
499
+ const resendBtn = (_c = this.container) === null || _c === void 0 ? void 0 : _c.querySelector('#voxepay-resend-otp');
500
+ resendBtn === null || resendBtn === void 0 ? void 0 : resendBtn.addEventListener('click', () => this.handleResendOTP());
501
+ const backBtn = (_d = this.container) === null || _d === void 0 ? void 0 : _d.querySelector('#voxepay-back-to-card');
502
+ backBtn === null || backBtn === void 0 ? void 0 : backBtn.addEventListener('click', () => this.handleBackToCard());
503
+ (_e = otpInputs === null || otpInputs === void 0 ? void 0 : otpInputs[0]) === null || _e === void 0 ? void 0 : _e.focus();
504
+ }
505
+ handleOTPInput(e, index) {
506
+ var _a;
507
+ const input = e.target;
508
+ const value = input.value.replace(/\D/g, '');
509
+ input.value = value;
510
+ if (value && index < 5) {
511
+ const nextInput = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector(`[data-index="${index + 1}"]`);
512
+ nextInput === null || nextInput === void 0 ? void 0 : nextInput.focus();
513
+ }
514
+ this.updateOTPState();
515
+ this.clearError('otp');
516
+ }
517
+ handleOTPKeydown(e, index) {
518
+ var _a;
519
+ const input = e.target;
520
+ if (e.key === 'Backspace' && !input.value && index > 0) {
521
+ const prevInput = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector(`[data-index="${index - 1}"]`);
522
+ prevInput === null || prevInput === void 0 ? void 0 : prevInput.focus();
523
+ }
524
+ }
525
+ handleOTPPaste(e) {
526
+ var _a, _b;
527
+ e.preventDefault();
528
+ const pastedData = (_a = e.clipboardData) === null || _a === void 0 ? void 0 : _a.getData('text').replace(/\D/g, '').slice(0, 6);
529
+ if (pastedData) {
530
+ const inputs = (_b = this.container) === null || _b === void 0 ? void 0 : _b.querySelectorAll('.voxepay-otp-digit');
531
+ pastedData.split('').forEach((digit, i) => {
532
+ if (inputs[i]) {
533
+ inputs[i].value = digit;
534
+ }
535
+ });
536
+ this.updateOTPState();
537
+ }
538
+ }
539
+ updateOTPState() {
540
+ var _a;
541
+ const inputs = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelectorAll('.voxepay-otp-digit');
542
+ let otp = '';
543
+ inputs === null || inputs === void 0 ? void 0 : inputs.forEach(input => otp += input.value);
544
+ this.state.otp = otp;
545
+ }
546
+ startOTPTimer() {
547
+ var _a, _b, _c;
548
+ const timerEl = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#voxepay-timer-count');
549
+ const timerContainer = (_b = this.container) === null || _b === void 0 ? void 0 : _b.querySelector('#voxepay-otp-timer');
550
+ const resendBtn = (_c = this.container) === null || _c === void 0 ? void 0 : _c.querySelector('#voxepay-resend-otp');
551
+ this.state.otpTimerInterval = window.setInterval(() => {
552
+ this.state.otpTimer--;
553
+ if (timerEl) {
554
+ timerEl.textContent = String(this.state.otpTimer);
555
+ }
556
+ if (this.state.otpTimer <= 0) {
557
+ if (this.state.otpTimerInterval) {
558
+ clearInterval(this.state.otpTimerInterval);
559
+ }
560
+ this.state.canResendOtp = true;
561
+ if (timerContainer)
562
+ timerContainer.style.display = 'none';
563
+ if (resendBtn)
564
+ resendBtn.disabled = false;
565
+ }
566
+ }, 1000);
567
+ }
568
+ async handleOTPSubmit() {
569
+ if (this.state.otp.length !== 6) {
570
+ this.showError('otp', 'Please enter the complete 6-digit code');
571
+ return;
572
+ }
573
+ this.setOTPProcessing(true);
574
+ try {
575
+ await this.verifyOTP();
576
+ if (this.state.otpTimerInterval) {
577
+ clearInterval(this.state.otpTimerInterval);
578
+ }
579
+ const result = await this.processPayment();
580
+ this.state.isSuccess = true;
581
+ this.renderSuccessView();
582
+ this.options.onSuccess(result);
583
+ }
584
+ catch (error) {
585
+ this.setOTPProcessing(false);
586
+ const paymentError = error;
587
+ this.showError('otp', paymentError.message || 'Invalid OTP. Please try again.');
588
+ }
589
+ }
590
+ setOTPProcessing(isProcessing) {
591
+ var _a;
592
+ const verifyBtn = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#voxepay-verify-otp');
593
+ if (verifyBtn) {
594
+ verifyBtn.disabled = isProcessing;
595
+ verifyBtn.innerHTML = isProcessing
596
+ ? '<div class="voxepay-spinner"></div><span>Verifying...</span>'
597
+ : '<span>🔐 Verify & Pay</span>';
598
+ }
599
+ }
600
+ async verifyOTP() {
601
+ await new Promise(resolve => setTimeout(resolve, 1500));
602
+ if (this.state.otp.length !== 6) {
603
+ throw { code: 'INVALID_OTP', message: 'Invalid OTP', recoverable: true };
604
+ }
605
+ }
606
+ handleResendOTP() {
607
+ var _a, _b, _c, _d;
608
+ const timerContainer = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#voxepay-otp-timer');
609
+ const resendBtn = (_b = this.container) === null || _b === void 0 ? void 0 : _b.querySelector('#voxepay-resend-otp');
610
+ this.state.otpTimer = 60;
611
+ this.state.canResendOtp = false;
612
+ if (timerContainer)
613
+ timerContainer.style.display = 'block';
614
+ if (resendBtn)
615
+ resendBtn.disabled = true;
616
+ const inputs = (_c = this.container) === null || _c === void 0 ? void 0 : _c.querySelectorAll('.voxepay-otp-digit');
617
+ inputs === null || inputs === void 0 ? void 0 : inputs.forEach(input => input.value = '');
618
+ this.state.otp = '';
619
+ this.startOTPTimer();
620
+ (_d = inputs === null || inputs === void 0 ? void 0 : inputs[0]) === null || _d === void 0 ? void 0 : _d.focus();
621
+ }
622
+ handleBackToCard() {
623
+ var _a;
624
+ if (this.state.otpTimerInterval) {
625
+ clearInterval(this.state.otpTimerInterval);
626
+ }
627
+ this.state.isOtpStep = false;
628
+ this.state.otp = '';
629
+ if (this.container) {
630
+ this.container.innerHTML = this.getModalHTML();
631
+ this.overlay = this.container.querySelector('.voxepay-overlay');
632
+ (_a = this.overlay) === null || _a === void 0 ? void 0 : _a.classList.add('voxepay-visible');
633
+ this.attachEventListeners();
634
+ const cardInput = this.container.querySelector('#voxepay-card-number');
635
+ const expiryInput = this.container.querySelector('#voxepay-expiry');
636
+ const cvvInput = this.container.querySelector('#voxepay-cvv');
637
+ if (cardInput)
638
+ cardInput.value = formatCardNumber(this.state.cardNumber);
639
+ if (expiryInput)
640
+ expiryInput.value = this.state.expiry;
641
+ if (cvvInput)
642
+ cvvInput.value = this.state.cvv;
643
+ }
644
+ }
645
+ /**
646
+ * Attach event listeners
647
+ */
648
+ 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"]');
651
+ closeBtn === null || closeBtn === void 0 ? void 0 : closeBtn.addEventListener('click', () => this.close());
652
+ (_b = this.overlay) === null || _b === void 0 ? void 0 : _b.addEventListener('click', (e) => {
653
+ if (e.target === this.overlay) {
654
+ this.close();
655
+ }
656
+ });
657
+ 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));
669
+ }
670
+ handleCardInput(e) {
671
+ var _a;
672
+ const input = e.target;
673
+ const formatted = formatCardNumber(input.value);
674
+ input.value = formatted;
675
+ this.state.cardNumber = formatted.replace(/\s/g, '');
676
+ const brandEl = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#voxepay-card-brand');
677
+ const brand = detectCardBrand(this.state.cardNumber);
678
+ if (brandEl) {
679
+ brandEl.textContent = brand ? CARD_BRAND_DISPLAY[brand.code] || '' : '';
680
+ brandEl.style.opacity = brand ? '1' : '0';
681
+ }
682
+ this.clearError('cardNumber');
683
+ }
684
+ handleExpiryInput(e) {
685
+ const input = e.target;
686
+ const formatted = formatExpiry(input.value);
687
+ input.value = formatted;
688
+ this.state.expiry = formatted;
689
+ this.clearError('expiry');
690
+ }
691
+ handleCVVInput(e) {
692
+ const input = e.target;
693
+ const formatted = formatCVV(input.value);
694
+ input.value = formatted;
695
+ this.state.cvv = formatted;
696
+ this.clearError('cvv');
697
+ }
698
+ validateField(field) {
699
+ let result;
700
+ switch (field) {
701
+ case 'cardNumber':
702
+ result = validateCardNumber(this.state.cardNumber);
703
+ break;
704
+ case 'expiry':
705
+ result = validateExpiry(this.state.expiry);
706
+ break;
707
+ case 'cvv':
708
+ result = validateCVV(this.state.cvv, this.state.cardNumber);
709
+ break;
710
+ default:
711
+ return true;
712
+ }
713
+ if (!result.valid) {
714
+ this.showError(field, result.error || 'Invalid');
715
+ return false;
716
+ }
717
+ this.clearError(field);
718
+ return true;
719
+ }
720
+ showError(field, message) {
721
+ var _a, _b;
722
+ const errorEl = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector(`#voxepay-${field === 'cardNumber' ? 'card' : field}-error`);
723
+ const inputEl = (_b = this.container) === null || _b === void 0 ? void 0 : _b.querySelector(`#voxepay-${field === 'cardNumber' ? 'card-number' : field}`);
724
+ if (errorEl) {
725
+ errorEl.innerHTML = `${ICONS.error} ${message}`;
726
+ errorEl.style.display = 'flex';
727
+ }
728
+ inputEl === null || inputEl === void 0 ? void 0 : inputEl.classList.add('voxepay-error');
729
+ }
730
+ clearError(field) {
731
+ var _a, _b;
732
+ const errorEl = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector(`#voxepay-${field === 'cardNumber' ? 'card' : field}-error`);
733
+ const inputEl = (_b = this.container) === null || _b === void 0 ? void 0 : _b.querySelector(`#voxepay-${field === 'cardNumber' ? 'card-number' : field}`);
734
+ if (errorEl) {
735
+ errorEl.style.display = 'none';
736
+ }
737
+ inputEl === null || inputEl === void 0 ? void 0 : inputEl.classList.remove('voxepay-error');
738
+ }
739
+ async handleSubmit(e) {
740
+ e.preventDefault();
741
+ const isCardValid = this.validateField('cardNumber');
742
+ const isExpiryValid = this.validateField('expiry');
743
+ const isCVVValid = this.validateField('cvv');
744
+ if (!isCardValid || !isExpiryValid || !isCVVValid) {
745
+ return;
746
+ }
747
+ this.setProcessing(true);
748
+ try {
749
+ await new Promise(resolve => setTimeout(resolve, 1000));
750
+ this.setProcessing(false);
751
+ this.renderOTPView();
752
+ }
753
+ catch (error) {
754
+ this.setProcessing(false);
755
+ const paymentError = error;
756
+ this.options.onError(paymentError);
757
+ this.showError('cardNumber', paymentError.message || 'Payment failed. Please try again.');
758
+ }
759
+ }
760
+ setProcessing(isProcessing) {
761
+ var _a;
762
+ this.state.isProcessing = isProcessing;
763
+ const submitBtn = (_a = this.container) === null || _a === void 0 ? void 0 : _a.querySelector('#voxepay-submit');
764
+ if (submitBtn) {
765
+ submitBtn.disabled = isProcessing;
766
+ submitBtn.innerHTML = isProcessing
767
+ ? '<div class="voxepay-spinner"></div><span>Processing...</span>'
768
+ : `<span>Pay Now ${formatAmount(this.options.amount, this.options.currency)}</span>`;
769
+ }
770
+ }
771
+ async processPayment() {
772
+ await new Promise(resolve => setTimeout(resolve, 2000));
773
+ return {
774
+ id: `pay_${Date.now()}`,
775
+ status: 'success',
776
+ amount: this.options.amount,
777
+ currency: this.options.currency,
778
+ timestamp: new Date().toISOString(),
779
+ };
780
+ }
781
+ /**
782
+ * Get VoxePay branded styles
783
+ */
784
+ getStyles() {
785
+ return `
786
+ :root {
787
+ /* VoxePay Blue Color Palette */
788
+ --voxepay-primary: #0061FF;
789
+ --voxepay-primary-hover: #0056E0;
790
+ --voxepay-secondary: #0047CC;
791
+ --voxepay-accent: #60A5FA;
792
+ --voxepay-glow: rgba(0, 97, 255, 0.35);
793
+ --voxepay-success: #10B981;
794
+ --voxepay-success-bg: rgba(16, 185, 129, 0.1);
795
+ --voxepay-error: #EF4444;
796
+
797
+ /* Dark Mode (Default) */
798
+ --voxepay-bg: #0C0C1D;
799
+ --voxepay-surface: rgba(255, 255, 255, 0.04);
800
+ --voxepay-surface-hover: rgba(255, 255, 255, 0.08);
801
+ --voxepay-border: rgba(255, 255, 255, 0.08);
802
+ --voxepay-text: #FFFFFF;
803
+ --voxepay-text-muted: #B4B4C7;
804
+ --voxepay-text-subtle: #6B7280;
805
+ --voxepay-input-bg: rgba(255, 255, 255, 0.06);
806
+
807
+ /* Effects */
808
+ --voxepay-backdrop-blur: blur(24px);
809
+ --voxepay-border-radius: 12px;
810
+ --voxepay-border-radius-lg: 16px;
811
+ --voxepay-border-radius-xl: 24px;
812
+ --voxepay-glow-shadow: 0 0 60px var(--voxepay-glow);
813
+ --voxepay-shadow: 0 25px 60px -15px rgba(0, 0, 0, 0.6);
814
+ --voxepay-transition-fast: 150ms ease;
815
+ --voxepay-transition: 300ms cubic-bezier(0.4, 0, 0.2, 1);
816
+ }
817
+
818
+ /* Light Mode */
819
+ html.voxepay-light {
820
+ --voxepay-primary: #0061FF;
821
+ --voxepay-primary-hover: #0056E0;
822
+ --voxepay-secondary: #0047CC;
823
+ --voxepay-glow: rgba(0, 97, 255, 0.2);
824
+
825
+ --voxepay-bg: #FFFFFF;
826
+ --voxepay-surface: #F8FAFC;
827
+ --voxepay-surface-hover: #F1F5F9;
828
+ --voxepay-border: #E2E8F0;
829
+ --voxepay-text: #0F172A;
830
+ --voxepay-text-muted: #475569;
831
+ --voxepay-text-subtle: #94A3B8;
832
+ --voxepay-input-bg: #F8FAFC;
833
+
834
+ --voxepay-glow-shadow: 0 0 40px rgba(0, 97, 255, 0.12);
835
+ --voxepay-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.15);
836
+ }
837
+
838
+ .voxepay-checkout * { box-sizing: border-box; margin: 0; padding: 0; }
839
+ .voxepay-checkout { font-family: 'DM Sans', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 1rem; color: var(--voxepay-text); line-height: 1.5; -webkit-font-smoothing: antialiased; }
840
+
841
+ .voxepay-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); backdrop-filter: var(--voxepay-backdrop-blur); -webkit-backdrop-filter: var(--voxepay-backdrop-blur); display: flex; align-items: center; justify-content: center; z-index: 999999; opacity: 0; visibility: hidden; transition: opacity var(--voxepay-transition), visibility var(--voxepay-transition); }
842
+ html.voxepay-light .voxepay-overlay { background: rgba(15, 23, 42, 0.4); }
843
+ .voxepay-overlay.voxepay-visible { opacity: 1; visibility: visible; }
844
+
845
+ .voxepay-modal { background: var(--voxepay-bg); border: 1px solid var(--voxepay-border); border-radius: var(--voxepay-border-radius-xl); width: 100%; max-width: 420px; max-height: 90vh; overflow: hidden; box-shadow: var(--voxepay-shadow), var(--voxepay-glow-shadow); transform: scale(0.95) translateY(20px); opacity: 0; transition: transform var(--voxepay-transition), opacity var(--voxepay-transition); }
846
+ .voxepay-overlay.voxepay-visible .voxepay-modal { transform: scale(1) translateY(0); opacity: 1; }
847
+
848
+ .voxepay-header { display: flex; align-items: center; justify-content: space-between; padding: 20px 24px; border-bottom: 1px solid var(--voxepay-border); background: var(--voxepay-surface); }
849
+ .voxepay-header-left { display: flex; align-items: center; gap: 12px; }
850
+ .voxepay-logo { width: 36px; height: 36px; border-radius: var(--voxepay-border-radius); background: linear-gradient(135deg, var(--voxepay-primary), var(--voxepay-secondary)); display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 1.25rem; color: white; box-shadow: 0 4px 12px rgba(0, 97, 255, 0.3); }
851
+ .voxepay-amount { font-size: 1.25rem; font-weight: 600; }
852
+ .voxepay-close { width: 36px; height: 36px; border: none; background: var(--voxepay-surface-hover); border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; color: var(--voxepay-text-muted); transition: all var(--voxepay-transition-fast); }
853
+ .voxepay-close:hover { background: var(--voxepay-border); color: var(--voxepay-text); transform: rotate(90deg); }
854
+ .voxepay-close svg { width: 18px; height: 18px; }
855
+
856
+ .voxepay-body { padding: 24px; background: var(--voxepay-bg); }
857
+ .voxepay-form-group { margin-bottom: 20px; }
858
+ .voxepay-label { display: flex; align-items: center; gap: 8px; font-size: 0.875rem; font-weight: 500; color: var(--voxepay-text-muted); margin-bottom: 8px; }
859
+ .voxepay-label-icon { font-size: 1rem; }
860
+
861
+ .voxepay-input { width: 100%; padding: 14px 16px; background: var(--voxepay-input-bg); border: 1.5px solid var(--voxepay-border); border-radius: var(--voxepay-border-radius); color: var(--voxepay-text); font-size: 1rem; font-family: inherit; outline: none; transition: border-color var(--voxepay-transition-fast), box-shadow var(--voxepay-transition-fast), background var(--voxepay-transition-fast); }
862
+ .voxepay-input::placeholder { color: var(--voxepay-text-subtle); }
863
+ .voxepay-input:hover { border-color: var(--voxepay-text-subtle); }
864
+ .voxepay-input:focus { border-color: var(--voxepay-primary); box-shadow: 0 0 0 4px var(--voxepay-glow); background: var(--voxepay-bg); }
865
+ .voxepay-input.voxepay-error { border-color: var(--voxepay-error); box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.15); }
866
+
867
+ .voxepay-card-input-wrapper { position: relative; }
868
+ .voxepay-card-brand { position: absolute; right: 14px; top: 50%; transform: translateY(-50%); padding: 4px 8px; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, var(--voxepay-primary), var(--voxepay-secondary)); border-radius: 6px; font-size: 0.7rem; font-weight: 700; color: white; opacity: 0; transition: opacity var(--voxepay-transition-fast); letter-spacing: 0.5px; }
869
+ .voxepay-card-input-wrapper .voxepay-input { padding-right: 70px; }
870
+
871
+ .voxepay-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
872
+
873
+ .voxepay-error-message { display: flex; align-items: center; gap: 6px; font-size: 0.813rem; color: var(--voxepay-error); margin-top: 8px; animation: voxepay-shake 0.4s ease; }
874
+ .voxepay-error-message svg { width: 16px; height: 16px; flex-shrink: 0; }
875
+ @keyframes voxepay-shake { 0%, 100% { transform: translateX(0); } 20%, 60% { transform: translateX(-4px); } 40%, 80% { transform: translateX(4px); } }
876
+
877
+ .voxepay-submit-btn { width: 100%; padding: 16px 24px; background: linear-gradient(135deg, var(--voxepay-primary), var(--voxepay-secondary)); border: none; border-radius: var(--voxepay-border-radius); color: white; font-size: 1.063rem; font-weight: 600; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 10px; transition: all var(--voxepay-transition-fast); position: relative; overflow: hidden; box-shadow: 0 4px 15px rgba(0, 97, 255, 0.35); }
878
+ .voxepay-submit-btn:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 8px 25px rgba(0, 97, 255, 0.45); }
879
+ .voxepay-submit-btn:active:not(:disabled) { transform: translateY(0); }
880
+ .voxepay-submit-btn:disabled { opacity: 0.6; cursor: not-allowed; box-shadow: none; }
881
+ .voxepay-submit-btn span { position: relative; z-index: 1; }
882
+
883
+ .voxepay-spinner { width: 20px; height: 20px; border: 2.5px solid rgba(255, 255, 255, 0.3); border-top-color: white; border-radius: 50%; animation: voxepay-spin 0.8s linear infinite; }
884
+ @keyframes voxepay-spin { to { transform: rotate(360deg); } }
885
+
886
+ .voxepay-footer { text-align: center; padding: 16px 24px 20px; border-top: 1px solid var(--voxepay-border); background: var(--voxepay-surface); }
887
+ .voxepay-powered-by { font-size: 0.75rem; color: var(--voxepay-text-subtle); display: flex; align-items: center; justify-content: center; gap: 6px; }
888
+ .voxepay-powered-by svg { width: 14px; height: 14px; color: var(--voxepay-primary); }
889
+ .voxepay-powered-by strong { color: var(--voxepay-primary); font-weight: 600; }
890
+
891
+ .voxepay-success-view { text-align: center; padding: 40px 24px; }
892
+ .voxepay-success-icon { width: 80px; height: 80px; margin: 0 auto 24px; background: linear-gradient(135deg, var(--voxepay-success), #059669); border-radius: 50%; display: flex; align-items: center; justify-content: center; animation: voxepay-success-pop 0.5s ease; box-shadow: 0 8px 25px rgba(16, 185, 129, 0.35); }
893
+ @keyframes voxepay-success-pop { 0% { transform: scale(0); opacity: 0; } 50% { transform: scale(1.1); } 100% { transform: scale(1); opacity: 1; } }
894
+ .voxepay-success-icon svg { width: 40px; height: 40px; color: white; }
895
+ .voxepay-success-title { font-size: 1.5rem; font-weight: 700; margin-bottom: 8px; color: var(--voxepay-text); }
896
+ .voxepay-success-message { font-size: 1rem; color: var(--voxepay-text-muted); margin-bottom: 24px; }
897
+ .voxepay-success-btn { padding: 12px 32px; background: var(--voxepay-surface); border: 1.5px solid var(--voxepay-border); border-radius: var(--voxepay-border-radius); color: var(--voxepay-text); font-size: 1rem; font-weight: 500; cursor: pointer; transition: all var(--voxepay-transition-fast); }
898
+ .voxepay-success-btn:hover { background: var(--voxepay-surface-hover); border-color: var(--voxepay-primary); color: var(--voxepay-primary); }
899
+
900
+ /* OTP View */
901
+ .voxepay-otp-view { text-align: center; padding: 24px 16px; }
902
+ .voxepay-otp-header { margin-bottom: 24px; }
903
+ .voxepay-otp-icon { font-size: 3rem; margin-bottom: 12px; }
904
+ .voxepay-otp-title { font-size: 1.25rem; font-weight: 700; color: var(--voxepay-text); margin-bottom: 8px; }
905
+ .voxepay-otp-subtitle { font-size: 0.875rem; color: var(--voxepay-text-muted); }
906
+
907
+ .voxepay-otp-inputs-container { margin-bottom: 16px; }
908
+ .voxepay-otp-inputs { display: flex; justify-content: center; gap: 8px; margin-bottom: 8px; }
909
+ .voxepay-otp-digit { width: 48px; height: 56px; text-align: center; font-size: 1.5rem; font-weight: 700; border: 2px solid var(--voxepay-border); border-radius: var(--voxepay-border-radius); background: var(--voxepay-input-bg); color: var(--voxepay-text); outline: none; transition: all var(--voxepay-transition-fast); }
910
+ .voxepay-otp-digit:focus { border-color: var(--voxepay-primary); box-shadow: 0 0 0 4px var(--voxepay-glow); }
911
+
912
+ .voxepay-otp-timer { font-size: 0.875rem; color: var(--voxepay-text-muted); margin-bottom: 12px; }
913
+ .voxepay-otp-timer span { font-weight: 700; color: var(--voxepay-primary); }
914
+
915
+ .voxepay-resend-btn { padding: 8px 16px; background: transparent; border: 1px solid var(--voxepay-border); border-radius: var(--voxepay-border-radius); color: var(--voxepay-text-muted); font-size: 0.875rem; cursor: pointer; margin-bottom: 16px; transition: all var(--voxepay-transition-fast); }
916
+ .voxepay-resend-btn:hover:not(:disabled) { border-color: var(--voxepay-primary); color: var(--voxepay-primary); }
917
+ .voxepay-resend-btn:disabled { opacity: 0.5; cursor: not-allowed; }
918
+
919
+ .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
+ .voxepay-back-btn:hover { color: var(--voxepay-primary); }
921
+
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; } }
923
+ `;
924
+ }
925
+ }
926
+
927
+ /**
928
+ * VoxePay - Modern Payment Checkout SDK
929
+ *
930
+ * A beautiful, modern payment modal for the web.
931
+ *
932
+ * @example
933
+ * ```javascript
934
+ * // Initialize VoxePay
935
+ * VoxePay.init({ apiKey: 'pk_live_xxxxx' });
936
+ *
937
+ * // Open checkout
938
+ * VoxePay.checkout({
939
+ * amount: 4999,
940
+ * currency: 'NGN',
941
+ * description: 'Premium Plan',
942
+ * onSuccess: (result) => console.log('Paid!', result),
943
+ * onError: (error) => console.error('Failed', error),
944
+ * });
945
+ * ```
946
+ */
947
+ class VoxePaySDK {
948
+ constructor() {
949
+ this.config = null;
950
+ this.currentModal = null;
951
+ this.initialized = false;
952
+ }
953
+ /**
954
+ * Initialize VoxePay with your configuration
955
+ * @param config - Configuration object with API key and optional settings
956
+ */
957
+ init(config) {
958
+ if (!config.apiKey) {
959
+ console.error('[VoxePay] API key is required');
960
+ return;
961
+ }
962
+ this.config = {
963
+ theme: 'dark',
964
+ locale: 'en-US',
965
+ ...config,
966
+ };
967
+ this.initialized = true;
968
+ // Apply theme
969
+ if (config.theme === 'auto') {
970
+ this.applyAutoTheme();
971
+ }
972
+ else if (config.theme === 'light') {
973
+ document.documentElement.classList.add('voxepay-light');
974
+ }
975
+ // Apply custom styles
976
+ if (config.customStyles) {
977
+ this.applyCustomStyles(config.customStyles);
978
+ }
979
+ console.log('[VoxePay] Initialized successfully');
980
+ }
981
+ /**
982
+ * Open the checkout modal
983
+ * @param options - Checkout options including amount, currency, and callbacks
984
+ */
985
+ checkout(options) {
986
+ if (!this.initialized) {
987
+ console.error('[VoxePay] Not initialized. Call VoxePay.init() first.');
988
+ options.onError({
989
+ code: 'NOT_INITIALIZED',
990
+ message: 'VoxePay SDK not initialized. Call VoxePay.init() first.',
991
+ recoverable: false,
992
+ });
993
+ return;
994
+ }
995
+ if (!options.amount || options.amount <= 0) {
996
+ console.error('[VoxePay] Invalid amount');
997
+ options.onError({
998
+ code: 'INVALID_AMOUNT',
999
+ message: 'Payment amount must be greater than 0',
1000
+ recoverable: false,
1001
+ });
1002
+ return;
1003
+ }
1004
+ if (!options.currency) {
1005
+ console.error('[VoxePay] Currency is required');
1006
+ options.onError({
1007
+ code: 'INVALID_CURRENCY',
1008
+ message: 'Currency code is required',
1009
+ recoverable: false,
1010
+ });
1011
+ return;
1012
+ }
1013
+ // Close any existing modal
1014
+ this.closeModal();
1015
+ // Create and open new modal
1016
+ this.currentModal = new VoxePayModal({
1017
+ ...options,
1018
+ onClose: () => {
1019
+ var _a;
1020
+ this.currentModal = null;
1021
+ (_a = options.onClose) === null || _a === void 0 ? void 0 : _a.call(options);
1022
+ },
1023
+ });
1024
+ this.currentModal.open();
1025
+ }
1026
+ /**
1027
+ * Close the current checkout modal
1028
+ */
1029
+ closeModal() {
1030
+ if (this.currentModal) {
1031
+ this.currentModal.close();
1032
+ this.currentModal = null;
1033
+ }
1034
+ }
1035
+ /**
1036
+ * Set the theme
1037
+ * @param theme - 'dark', 'light', or 'auto'
1038
+ */
1039
+ setTheme(theme) {
1040
+ document.documentElement.classList.remove('voxepay-light');
1041
+ if (theme === 'light') {
1042
+ document.documentElement.classList.add('voxepay-light');
1043
+ }
1044
+ else if (theme === 'auto') {
1045
+ this.applyAutoTheme();
1046
+ }
1047
+ if (this.config) {
1048
+ this.config.theme = theme;
1049
+ }
1050
+ }
1051
+ /**
1052
+ * Apply system theme preference
1053
+ */
1054
+ applyAutoTheme() {
1055
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
1056
+ if (!prefersDark) {
1057
+ document.documentElement.classList.add('voxepay-light');
1058
+ }
1059
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
1060
+ var _a;
1061
+ if (((_a = this.config) === null || _a === void 0 ? void 0 : _a.theme) === 'auto') {
1062
+ document.documentElement.classList.toggle('voxepay-light', !e.matches);
1063
+ }
1064
+ });
1065
+ }
1066
+ /**
1067
+ * Apply custom CSS variables
1068
+ */
1069
+ applyCustomStyles(styles) {
1070
+ const root = document.documentElement;
1071
+ for (const [key, value] of Object.entries(styles)) {
1072
+ if (value) {
1073
+ root.style.setProperty(key, value);
1074
+ }
1075
+ }
1076
+ }
1077
+ /**
1078
+ * Get the SDK version
1079
+ */
1080
+ get version() {
1081
+ return '0.1.0';
1082
+ }
1083
+ /**
1084
+ * Check if SDK is initialized
1085
+ */
1086
+ get isInitialized() {
1087
+ return this.initialized;
1088
+ }
1089
+ }
1090
+ // Create singleton instance
1091
+ const VoxePay = new VoxePaySDK();
1092
+ // Make available on window for script tag usage
1093
+ if (typeof window !== 'undefined') {
1094
+ window.VoxePay = VoxePay;
1095
+ }
1096
+
1097
+ export { VoxePay, VoxePaySDK, VoxePay as default, detectCardBrand, formatAmount, formatCVV, formatCardNumber, formatExpiry, getCurrencySymbol, luhnCheck, validateCVV, validateCardNumber, validateExpiry };
1098
+ //# sourceMappingURL=index.esm.js.map