188 lines
5.5 KiB
TypeScript
188 lines
5.5 KiB
TypeScript
import * as v from 'valibot';
|
|
import { isValidPhoneNumber } from './phoneUtils';
|
|
|
|
// Common validation schemas
|
|
export const fullNameSchema = v.pipe(
|
|
v.string(),
|
|
v.minLength(1, 'Please enter your full name'),
|
|
v.trim(),
|
|
v.minLength(1, 'Full name cannot be empty')
|
|
);
|
|
|
|
export const emailSchema = v.pipe(
|
|
v.string(),
|
|
v.minLength(1, 'Please enter your email address'),
|
|
v.email('Please enter a valid email address'),
|
|
v.trim()
|
|
);
|
|
|
|
export const phoneNumberSchema = v.pipe(
|
|
v.string(),
|
|
v.minLength(1, 'Please enter a phone number'),
|
|
v.trim(),
|
|
v.custom((value) => {
|
|
if (typeof value !== 'string') return false;
|
|
const cleanPhone = value.replace(/[^+\d]/g, '');
|
|
if (cleanPhone.length < 7) {
|
|
return false;
|
|
}
|
|
// Use the existing phone validation utility
|
|
return isValidPhoneNumber(value);
|
|
}, 'Please enter a valid phone number')
|
|
);
|
|
|
|
export const pinSchema = v.pipe(
|
|
v.string(),
|
|
v.minLength(1, 'Please enter a PIN'),
|
|
v.length(6, 'PIN must be exactly 6 digits'),
|
|
v.regex(/^\d{6}$/, 'PIN must contain only digits')
|
|
);
|
|
|
|
export const confirmPinSchema = (pin: string) => v.pipe(
|
|
v.string(),
|
|
v.minLength(1, 'Please confirm your PIN'),
|
|
v.custom((value) => {
|
|
return typeof value === 'string' && value === pin;
|
|
}, 'PIN and confirm PIN do not match')
|
|
);
|
|
|
|
export const addressSchema = v.optional(
|
|
v.pipe(
|
|
v.string(),
|
|
v.trim()
|
|
)
|
|
);
|
|
|
|
export const cardNumberSchema = v.pipe(
|
|
v.string(),
|
|
v.minLength(1, 'Card number is required'),
|
|
v.trim(),
|
|
v.custom((value) => {
|
|
if (typeof value !== 'string') return false;
|
|
const cleanNumber = value.replace(/\s/g, '');
|
|
// Basic card number validation (13-19 digits)
|
|
return /^\d{13,19}$/.test(cleanNumber);
|
|
}, 'Please enter a valid card number')
|
|
);
|
|
|
|
export const expiryDateSchema = v.pipe(
|
|
v.string(),
|
|
v.minLength(1, 'Expiry date is required'),
|
|
v.trim(),
|
|
v.custom((value) => {
|
|
if (typeof value !== 'string') return false;
|
|
// Validate MM/YY format
|
|
if (!/^\d{2}\/\d{2}$/.test(value)) {
|
|
return false;
|
|
}
|
|
const [month, year] = value.split('/');
|
|
const monthNum = parseInt(month, 10);
|
|
const yearNum = parseInt(year, 10);
|
|
|
|
if (monthNum < 1 || monthNum > 12) {
|
|
return false;
|
|
}
|
|
|
|
// Check if card is expired (basic check - assumes 20XX)
|
|
const currentYear = new Date().getFullYear() % 100;
|
|
const currentMonth = new Date().getMonth() + 1;
|
|
|
|
if (yearNum < currentYear || (yearNum === currentYear && monthNum < currentMonth)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}, 'Please enter a valid expiry date')
|
|
);
|
|
|
|
export const cvvSchema = v.pipe(
|
|
v.string(),
|
|
v.minLength(1, 'CVV is required'),
|
|
v.trim(),
|
|
v.regex(/^\d{3,4}$/, 'CVV must be 3 or 4 digits')
|
|
);
|
|
|
|
// Amount validation schemas
|
|
export const amountSchema = (options?: {
|
|
min?: number; // in cents
|
|
max?: number; // in cents
|
|
minDisplay?: string; // for error messages
|
|
maxDisplay?: string; // for error messages
|
|
}) => {
|
|
const min = options?.min ?? 1; // Default 1 cent
|
|
const max = options?.max ?? 99999; // Default $999.99
|
|
const minDisplay = options?.minDisplay ?? '$0.01';
|
|
const maxDisplay = options?.maxDisplay ?? '$999.99';
|
|
|
|
return v.pipe(
|
|
v.string(),
|
|
v.custom((value) => {
|
|
if (typeof value !== 'string') return false;
|
|
return !(!value || value === '' || value === '0' || value === '0.' || value === '0.00');
|
|
}, 'Please enter an amount'),
|
|
v.custom((value) => {
|
|
if (typeof value !== 'string') return false;
|
|
// Parse amount to cents
|
|
const numValue = parseFloat(value);
|
|
if (isNaN(numValue)) {
|
|
return false;
|
|
}
|
|
const amountInCents = Math.round(numValue * 100);
|
|
|
|
return amountInCents >= min && amountInCents <= max;
|
|
}, `Amount must be between ${minDisplay} and ${maxDisplay}`)
|
|
);
|
|
};
|
|
|
|
// Form schemas
|
|
export const phoneSetupSchema = v.object({
|
|
fullName: fullNameSchema,
|
|
email: emailSchema,
|
|
address: addressSchema,
|
|
pin: pinSchema,
|
|
confirmPin: v.string(), // Will be validated separately with confirmPinSchema
|
|
});
|
|
|
|
export const addRecipientSchema = v.object({
|
|
fullName: fullNameSchema,
|
|
phoneNumber: phoneNumberSchema,
|
|
});
|
|
|
|
export const addCardSchema = v.object({
|
|
cardNumber: cardNumberSchema,
|
|
expiryDate: expiryDateSchema,
|
|
cvv: cvvSchema,
|
|
});
|
|
|
|
export const profileUpdateSchema = v.object({
|
|
fullName: fullNameSchema,
|
|
phoneNumber: v.optional(phoneNumberSchema),
|
|
email: v.optional(emailSchema),
|
|
address: addressSchema,
|
|
});
|
|
|
|
// Helper function to get validation errors
|
|
export const getValidationError = (error: v.ValiError<any>): string => {
|
|
if (error.issues && error.issues.length > 0) {
|
|
return error.issues[0].message || 'Validation error';
|
|
}
|
|
return 'Validation error';
|
|
};
|
|
|
|
// Helper function to validate and get errors
|
|
export const validate = <T extends v.BaseSchema<any, any, any>>(
|
|
schema: T,
|
|
input: unknown
|
|
): { success: true; data: v.InferInput<T> } | { success: false; error: string } => {
|
|
try {
|
|
const data = v.parse(schema, input);
|
|
return { success: true, data };
|
|
} catch (error) {
|
|
if (error instanceof v.ValiError) {
|
|
return { success: false, error: getValidationError(error) };
|
|
}
|
|
return { success: false, error: 'Validation error' };
|
|
}
|
|
};
|
|
|