@voxepay/checkout 0.3.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs.js +1 -0
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +1 -0
- package/dist/index.esm.js.map +1 -1
- package/dist/voxepay-checkout.min.js +1 -1
- package/dist/voxepay-checkout.min.js.map +1 -1
- package/package.json +2 -1
- package/src/api/client.ts +110 -0
- package/src/components/modal.ts +1245 -0
- package/src/index.ts +73 -0
- package/src/types.ts +141 -0
- package/src/utils/card-validator.ts +181 -0
- package/src/utils/encryption.ts +496 -0
- package/src/utils/formatter.ts +71 -0
- package/src/voxepay.ts +202 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @voxepay/checkout - Modern Payment Checkout SDK
|
|
3
|
+
*
|
|
4
|
+
* A beautiful, modern payment modal for the web by VoxePay.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```html
|
|
8
|
+
* <script src="https://unpkg.com/@voxepay/checkout"></script>
|
|
9
|
+
* <script>
|
|
10
|
+
* VoxePay.init({ apiKey: 'pk_live_xxxxx' });
|
|
11
|
+
*
|
|
12
|
+
* VoxePay.checkout({
|
|
13
|
+
* amount: 4999,
|
|
14
|
+
* currency: 'NGN',
|
|
15
|
+
* onSuccess: (result) => console.log('Paid!', result),
|
|
16
|
+
* onError: (error) => console.error('Failed', error),
|
|
17
|
+
* });
|
|
18
|
+
* </script>
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* @packageDocumentation
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// Main SDK
|
|
25
|
+
export { VoxePay, VoxePaySDK } from './voxepay';
|
|
26
|
+
|
|
27
|
+
// Types
|
|
28
|
+
export type {
|
|
29
|
+
VoxePayConfig,
|
|
30
|
+
VoxePayTheme,
|
|
31
|
+
CheckoutOptions,
|
|
32
|
+
PaymentResult,
|
|
33
|
+
PaymentError,
|
|
34
|
+
CardInfo,
|
|
35
|
+
BankTransferDetails,
|
|
36
|
+
PaymentMethod,
|
|
37
|
+
} from './types';
|
|
38
|
+
|
|
39
|
+
// Utilities (for advanced usage)
|
|
40
|
+
export {
|
|
41
|
+
detectCardBrand,
|
|
42
|
+
validateCardNumber,
|
|
43
|
+
validateExpiry,
|
|
44
|
+
validateCVV,
|
|
45
|
+
luhnCheck,
|
|
46
|
+
} from './utils/card-validator';
|
|
47
|
+
|
|
48
|
+
export {
|
|
49
|
+
formatCardNumber,
|
|
50
|
+
formatExpiry,
|
|
51
|
+
formatCVV,
|
|
52
|
+
formatAmount,
|
|
53
|
+
getCurrencySymbol,
|
|
54
|
+
} from './utils/formatter';
|
|
55
|
+
|
|
56
|
+
export {
|
|
57
|
+
generateAuthData,
|
|
58
|
+
formatExpiryForApi,
|
|
59
|
+
cleanPan,
|
|
60
|
+
} from './utils/encryption';
|
|
61
|
+
|
|
62
|
+
export type { AuthDataParams } from './utils/encryption';
|
|
63
|
+
|
|
64
|
+
export type {
|
|
65
|
+
InitiatePaymentRequest,
|
|
66
|
+
InitiatePaymentResponse,
|
|
67
|
+
ValidateOTPRequest,
|
|
68
|
+
ValidateOTPResponse,
|
|
69
|
+
ResendOTPRequest,
|
|
70
|
+
} from './api/client';
|
|
71
|
+
|
|
72
|
+
// Default export for convenience
|
|
73
|
+
export { VoxePay as default } from './voxepay';
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VoxePay Checkout Type Definitions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Configuration for initializing VoxePay
|
|
7
|
+
*/
|
|
8
|
+
export interface VoxePayConfig {
|
|
9
|
+
/** Your VoxePay API key (e.g., vxp_live_xxxxx) */
|
|
10
|
+
apiKey: string;
|
|
11
|
+
/** Your VoxePay organization ID */
|
|
12
|
+
organizationId: string;
|
|
13
|
+
/** API base URL (defaults to https://devpay.voxepay.app) */
|
|
14
|
+
baseUrl?: string;
|
|
15
|
+
/** Theme mode: 'dark' (default), 'light', or 'auto' (follows system) */
|
|
16
|
+
theme?: 'dark' | 'light' | 'auto';
|
|
17
|
+
/** Locale for formatting (e.g., 'en-US', 'en-NG') */
|
|
18
|
+
locale?: string;
|
|
19
|
+
/** Custom CSS variables to override default theme */
|
|
20
|
+
customStyles?: Partial<VoxePayTheme>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Custom theme variables
|
|
25
|
+
*/
|
|
26
|
+
export interface VoxePayTheme {
|
|
27
|
+
'--voxepay-primary': string;
|
|
28
|
+
'--voxepay-secondary': string;
|
|
29
|
+
'--voxepay-accent': string;
|
|
30
|
+
'--voxepay-bg': string;
|
|
31
|
+
'--voxepay-surface': string;
|
|
32
|
+
'--voxepay-border': string;
|
|
33
|
+
'--voxepay-text': string;
|
|
34
|
+
'--voxepay-text-muted': string;
|
|
35
|
+
'--voxepay-border-radius': string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Bank transfer account details shown to the user
|
|
40
|
+
*/
|
|
41
|
+
export interface BankTransferDetails {
|
|
42
|
+
/** Account number to transfer to */
|
|
43
|
+
accountNumber: string;
|
|
44
|
+
/** Name of the bank */
|
|
45
|
+
bankName: string;
|
|
46
|
+
/** Account holder name */
|
|
47
|
+
accountName: string;
|
|
48
|
+
/** Payment reference code (user should include in transfer) */
|
|
49
|
+
reference: string;
|
|
50
|
+
/** Expiry time in seconds (how long the account is valid for, e.g. 1800 = 30 minutes) */
|
|
51
|
+
expiresIn: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Supported payment method types */
|
|
55
|
+
export type PaymentMethod = 'card' | 'bank_transfer';
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Options for opening a checkout session
|
|
59
|
+
*/
|
|
60
|
+
export interface CheckoutOptions {
|
|
61
|
+
/** Payment amount in smallest currency unit (e.g., kobo for NGN, cents for USD) */
|
|
62
|
+
amount: number;
|
|
63
|
+
/** ISO 4217 currency code (e.g., 'NGN', 'USD', 'EUR') */
|
|
64
|
+
currency: string;
|
|
65
|
+
/** Short description of the payment (shown in modal) */
|
|
66
|
+
description?: string;
|
|
67
|
+
/** Customer's email address (optional, for receipts) */
|
|
68
|
+
customerEmail?: string;
|
|
69
|
+
/** Customer's phone number */
|
|
70
|
+
customerPhone?: string;
|
|
71
|
+
/** Additional metadata to attach to the payment */
|
|
72
|
+
metadata?: Record<string, unknown>;
|
|
73
|
+
/** Payment methods to show. Defaults to ['card', 'bank_transfer'] */
|
|
74
|
+
paymentMethods?: PaymentMethod[];
|
|
75
|
+
/** Static bank transfer details (if known upfront) */
|
|
76
|
+
bankTransferDetails?: BankTransferDetails;
|
|
77
|
+
/** Callback to request bank transfer details dynamically (called when user selects bank transfer) */
|
|
78
|
+
onBankTransferRequested?: () => Promise<BankTransferDetails>;
|
|
79
|
+
/** @internal SDK config passed from VoxePaySDK to modal */
|
|
80
|
+
_sdkConfig?: {
|
|
81
|
+
apiKey: string;
|
|
82
|
+
organizationId: string;
|
|
83
|
+
baseUrl?: string;
|
|
84
|
+
};
|
|
85
|
+
/** Callback when payment succeeds */
|
|
86
|
+
onSuccess: (result: PaymentResult) => void;
|
|
87
|
+
/** Callback when payment fails */
|
|
88
|
+
onError: (error: PaymentError) => void;
|
|
89
|
+
/** Callback when modal is closed (optional) */
|
|
90
|
+
onClose?: () => void;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Result returned on successful payment
|
|
95
|
+
*/
|
|
96
|
+
export interface PaymentResult {
|
|
97
|
+
/** Unique payment ID */
|
|
98
|
+
id: string;
|
|
99
|
+
/** Payment status */
|
|
100
|
+
status: 'success' | 'pending';
|
|
101
|
+
/** Amount charged in smallest currency unit */
|
|
102
|
+
amount: number;
|
|
103
|
+
/** Currency code */
|
|
104
|
+
currency: string;
|
|
105
|
+
/** ISO timestamp of the payment */
|
|
106
|
+
timestamp: string;
|
|
107
|
+
/** Transaction reference (if available) */
|
|
108
|
+
reference?: string;
|
|
109
|
+
/** Payment method used */
|
|
110
|
+
paymentMethod?: PaymentMethod;
|
|
111
|
+
/** Additional data from payment processor */
|
|
112
|
+
data?: Record<string, unknown>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Error returned on payment failure
|
|
117
|
+
*/
|
|
118
|
+
export interface PaymentError {
|
|
119
|
+
/** Error code */
|
|
120
|
+
code: string;
|
|
121
|
+
/** Human-readable error message */
|
|
122
|
+
message: string;
|
|
123
|
+
/** Whether the error is recoverable (user can retry) */
|
|
124
|
+
recoverable?: boolean;
|
|
125
|
+
/** Additional error details */
|
|
126
|
+
details?: Record<string, unknown>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Card information (sanitized - no full card numbers)
|
|
131
|
+
*/
|
|
132
|
+
export interface CardInfo {
|
|
133
|
+
/** Card brand (visa, mastercard, verve, etc.) */
|
|
134
|
+
brand: string;
|
|
135
|
+
/** Last 4 digits of the card */
|
|
136
|
+
last4: string;
|
|
137
|
+
/** Expiry month (1-12) */
|
|
138
|
+
expiryMonth: number;
|
|
139
|
+
/** Expiry year (full year) */
|
|
140
|
+
expiryYear: number;
|
|
141
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Card validation utilities for VoxePay Checkout
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface CardBrand {
|
|
6
|
+
name: string;
|
|
7
|
+
code: string;
|
|
8
|
+
pattern: RegExp;
|
|
9
|
+
lengths: number[];
|
|
10
|
+
cvvLength: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const CARD_BRANDS: CardBrand[] = [
|
|
14
|
+
{
|
|
15
|
+
name: 'Visa',
|
|
16
|
+
code: 'visa',
|
|
17
|
+
pattern: /^4/,
|
|
18
|
+
lengths: [13, 16, 19],
|
|
19
|
+
cvvLength: 3,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'Mastercard',
|
|
23
|
+
code: 'mastercard',
|
|
24
|
+
pattern: /^(5[1-5]|2[2-7])/,
|
|
25
|
+
lengths: [16],
|
|
26
|
+
cvvLength: 3,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'Verve',
|
|
30
|
+
code: 'verve',
|
|
31
|
+
pattern: /^(506[0-9]|507[0-9]|6500)/,
|
|
32
|
+
lengths: [16, 18, 19],
|
|
33
|
+
cvvLength: 3,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'American Express',
|
|
37
|
+
code: 'amex',
|
|
38
|
+
pattern: /^3[47]/,
|
|
39
|
+
lengths: [15],
|
|
40
|
+
cvvLength: 4,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'Discover',
|
|
44
|
+
code: 'discover',
|
|
45
|
+
pattern: /^(6011|65|64[4-9])/,
|
|
46
|
+
lengths: [16, 19],
|
|
47
|
+
cvvLength: 3,
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Detect card brand from card number
|
|
53
|
+
*/
|
|
54
|
+
export function detectCardBrand(cardNumber: string): CardBrand | null {
|
|
55
|
+
const cleaned = cardNumber.replace(/\s/g, '');
|
|
56
|
+
for (const brand of CARD_BRANDS) {
|
|
57
|
+
if (brand.pattern.test(cleaned)) {
|
|
58
|
+
return brand;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Luhn algorithm for card validation
|
|
66
|
+
*/
|
|
67
|
+
export function luhnCheck(cardNumber: string): boolean {
|
|
68
|
+
const cleaned = cardNumber.replace(/\s/g, '');
|
|
69
|
+
if (!/^\d+$/.test(cleaned)) return false;
|
|
70
|
+
|
|
71
|
+
let sum = 0;
|
|
72
|
+
let isEven = false;
|
|
73
|
+
|
|
74
|
+
for (let i = cleaned.length - 1; i >= 0; i--) {
|
|
75
|
+
let digit = parseInt(cleaned[i], 10);
|
|
76
|
+
|
|
77
|
+
if (isEven) {
|
|
78
|
+
digit *= 2;
|
|
79
|
+
if (digit > 9) {
|
|
80
|
+
digit -= 9;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
sum += digit;
|
|
85
|
+
isEven = !isEven;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return sum % 10 === 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Validate card number
|
|
93
|
+
*/
|
|
94
|
+
export function validateCardNumber(cardNumber: string): { valid: boolean; error?: string } {
|
|
95
|
+
const cleaned = cardNumber.replace(/\s/g, '');
|
|
96
|
+
|
|
97
|
+
if (!cleaned) {
|
|
98
|
+
return { valid: false, error: 'Card number is required' };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!/^\d+$/.test(cleaned)) {
|
|
102
|
+
return { valid: false, error: 'Invalid card number' };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const brand = detectCardBrand(cleaned);
|
|
106
|
+
|
|
107
|
+
if (!brand) {
|
|
108
|
+
return { valid: false, error: 'Unsupported card type' };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!brand.lengths.includes(cleaned.length)) {
|
|
112
|
+
return { valid: false, error: 'Invalid card number length' };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!luhnCheck(cleaned)) {
|
|
116
|
+
return { valid: false, error: 'Invalid card number' };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { valid: true };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Validate expiry date
|
|
124
|
+
*/
|
|
125
|
+
export function validateExpiry(expiry: string): { valid: boolean; error?: string } {
|
|
126
|
+
const cleaned = expiry.replace(/\s/g, '');
|
|
127
|
+
|
|
128
|
+
if (!cleaned) {
|
|
129
|
+
return { valid: false, error: 'Expiry date is required' };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const match = cleaned.match(/^(\d{2})\/(\d{2})$/);
|
|
133
|
+
if (!match) {
|
|
134
|
+
return { valid: false, error: 'Invalid format (MM/YY)' };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const month = parseInt(match[1], 10);
|
|
138
|
+
const year = parseInt(match[2], 10) + 2000;
|
|
139
|
+
|
|
140
|
+
if (month < 1 || month > 12) {
|
|
141
|
+
return { valid: false, error: 'Invalid month' };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const now = new Date();
|
|
145
|
+
const currentYear = now.getFullYear();
|
|
146
|
+
const currentMonth = now.getMonth() + 1;
|
|
147
|
+
|
|
148
|
+
if (year < currentYear || (year === currentYear && month < currentMonth)) {
|
|
149
|
+
return { valid: false, error: 'Card has expired' };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (year > currentYear + 20) {
|
|
153
|
+
return { valid: false, error: 'Invalid expiry year' };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return { valid: true };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Validate CVV
|
|
161
|
+
*/
|
|
162
|
+
export function validateCVV(cvv: string, cardNumber?: string): { valid: boolean; error?: string } {
|
|
163
|
+
const cleaned = cvv.replace(/\s/g, '');
|
|
164
|
+
|
|
165
|
+
if (!cleaned) {
|
|
166
|
+
return { valid: false, error: 'CVV is required' };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!/^\d+$/.test(cleaned)) {
|
|
170
|
+
return { valid: false, error: 'Invalid CVV' };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const brand = cardNumber ? detectCardBrand(cardNumber) : null;
|
|
174
|
+
const expectedLength = brand?.cvvLength || 3;
|
|
175
|
+
|
|
176
|
+
if (cleaned.length !== expectedLength && cleaned.length !== 3 && cleaned.length !== 4) {
|
|
177
|
+
return { valid: false, error: `CVV must be ${expectedLength} digits` };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return { valid: true };
|
|
181
|
+
}
|