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