- Add extended error types with severity levels, retry policies, and contextual information (types/errors.ts) - Create centralized error handler service with user-friendly message translation and classification (services/errorHandler.ts) - Add ErrorContext for global error state management with auto-dismiss and error queue support (contexts/ErrorContext.tsx) - Create error UI components: ErrorToast, FieldError, FieldErrorSummary, FullScreenError, EmptyState, OfflineState - Add useError hook with retry strategies and API response handling - Add useAsync hook for async operations with comprehensive state - Create error message utilities with validation helpers - Add tests for errorHandler and errorMessages (88 tests) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
239 lines
7.6 KiB
TypeScript
239 lines
7.6 KiB
TypeScript
/**
|
|
* Error Message Utilities
|
|
*
|
|
* Provides user-friendly error message translation and formatting.
|
|
* Includes helpers for common validation scenarios.
|
|
*/
|
|
|
|
import { ErrorCodes } from '@/types/errors';
|
|
|
|
// Generic error messages for common scenarios
|
|
export const ErrorMessages = {
|
|
// Network errors
|
|
networkOffline: 'Unable to connect. Please check your internet connection.',
|
|
networkTimeout: 'The request took too long. Please try again.',
|
|
networkError: 'Connection failed. Please check your network.',
|
|
|
|
// Auth errors
|
|
sessionExpired: 'Your session has expired. Please log in again.',
|
|
unauthorized: 'You need to log in to continue.',
|
|
forbidden: 'You don\'t have permission to do this.',
|
|
|
|
// Validation errors
|
|
required: 'This field is required.',
|
|
invalidEmail: 'Please enter a valid email address.',
|
|
invalidPhone: 'Please enter a valid phone number.',
|
|
invalidOtp: 'Invalid code. Please try again.',
|
|
tooShort: (min: number) => `Must be at least ${min} characters.`,
|
|
tooLong: (max: number) => `Must be ${max} characters or less.`,
|
|
passwordMismatch: 'Passwords do not match.',
|
|
|
|
// Generic errors
|
|
genericError: 'Something went wrong. Please try again.',
|
|
tryAgainLater: 'Something went wrong. Please try again later.',
|
|
notFound: 'The item you\'re looking for could not be found.',
|
|
|
|
// Form errors
|
|
formInvalid: 'Please check the form for errors.',
|
|
fileTooBig: (maxMb: number) => `File is too large. Maximum size is ${maxMb}MB.`,
|
|
invalidFileType: 'This file type is not supported.',
|
|
|
|
// Subscription errors
|
|
subscriptionRequired: 'A subscription is required to use this feature.',
|
|
subscriptionExpired: 'Your subscription has expired.',
|
|
paymentFailed: 'Payment could not be processed. Please try again.',
|
|
|
|
// BLE errors
|
|
bluetoothDisabled: 'Please enable Bluetooth to continue.',
|
|
bluetoothPermissionDenied: 'Bluetooth permission is required.',
|
|
deviceNotFound: 'Device not found. Make sure it\'s powered on.',
|
|
|
|
// Sensor errors
|
|
sensorSetupFailed: 'Sensor setup failed. Please try again.',
|
|
sensorOffline: 'Sensor is offline.',
|
|
wifiConfigFailed: 'Failed to configure WiFi. Check your credentials.',
|
|
} as const;
|
|
|
|
// Action hints for guiding users
|
|
export const ActionHints = {
|
|
checkConnection: 'Check your Wi-Fi or cellular connection',
|
|
enableBluetooth: 'Enable Bluetooth in Settings',
|
|
loginAgain: 'Tap here to log in again',
|
|
contactSupport: 'Contact support if the problem persists',
|
|
tryDifferentPayment: 'Try a different payment method',
|
|
refreshPage: 'Pull down to refresh',
|
|
checkCredentials: 'Double-check your email and password',
|
|
} as const;
|
|
|
|
/**
|
|
* Get a user-friendly message for an error code
|
|
*/
|
|
export function getErrorMessage(code: string, fallback?: string): string {
|
|
const messages: Record<string, string> = {
|
|
[ErrorCodes.NETWORK_ERROR]: ErrorMessages.networkError,
|
|
[ErrorCodes.NETWORK_TIMEOUT]: ErrorMessages.networkTimeout,
|
|
[ErrorCodes.NETWORK_OFFLINE]: ErrorMessages.networkOffline,
|
|
[ErrorCodes.UNAUTHORIZED]: ErrorMessages.sessionExpired,
|
|
[ErrorCodes.TOKEN_EXPIRED]: ErrorMessages.sessionExpired,
|
|
[ErrorCodes.SESSION_EXPIRED]: ErrorMessages.sessionExpired,
|
|
[ErrorCodes.FORBIDDEN]: ErrorMessages.forbidden,
|
|
[ErrorCodes.NOT_FOUND]: ErrorMessages.notFound,
|
|
[ErrorCodes.VALIDATION_ERROR]: ErrorMessages.formInvalid,
|
|
[ErrorCodes.INVALID_EMAIL]: ErrorMessages.invalidEmail,
|
|
[ErrorCodes.INVALID_PHONE]: ErrorMessages.invalidPhone,
|
|
[ErrorCodes.INVALID_OTP]: ErrorMessages.invalidOtp,
|
|
[ErrorCodes.SUBSCRIPTION_REQUIRED]: ErrorMessages.subscriptionRequired,
|
|
[ErrorCodes.SUBSCRIPTION_EXPIRED]: ErrorMessages.subscriptionExpired,
|
|
[ErrorCodes.PAYMENT_FAILED]: ErrorMessages.paymentFailed,
|
|
[ErrorCodes.BLE_NOT_ENABLED]: ErrorMessages.bluetoothDisabled,
|
|
[ErrorCodes.BLE_PERMISSION_DENIED]: ErrorMessages.bluetoothPermissionDenied,
|
|
[ErrorCodes.BLE_DEVICE_NOT_FOUND]: ErrorMessages.deviceNotFound,
|
|
[ErrorCodes.SENSOR_SETUP_FAILED]: ErrorMessages.sensorSetupFailed,
|
|
[ErrorCodes.SENSOR_OFFLINE]: ErrorMessages.sensorOffline,
|
|
[ErrorCodes.SENSOR_WIFI_FAILED]: ErrorMessages.wifiConfigFailed,
|
|
};
|
|
|
|
return messages[code] || fallback || ErrorMessages.genericError;
|
|
}
|
|
|
|
/**
|
|
* Get an action hint for an error code
|
|
*/
|
|
export function getActionHint(code: string): string | undefined {
|
|
const hints: Record<string, string> = {
|
|
[ErrorCodes.NETWORK_ERROR]: ActionHints.checkConnection,
|
|
[ErrorCodes.NETWORK_OFFLINE]: ActionHints.checkConnection,
|
|
[ErrorCodes.UNAUTHORIZED]: ActionHints.loginAgain,
|
|
[ErrorCodes.TOKEN_EXPIRED]: ActionHints.loginAgain,
|
|
[ErrorCodes.BLE_NOT_ENABLED]: ActionHints.enableBluetooth,
|
|
[ErrorCodes.BLE_PERMISSION_DENIED]: ActionHints.enableBluetooth,
|
|
[ErrorCodes.PAYMENT_FAILED]: ActionHints.tryDifferentPayment,
|
|
};
|
|
|
|
return hints[code];
|
|
}
|
|
|
|
// Validation helpers
|
|
|
|
/**
|
|
* Validate email format
|
|
*/
|
|
export function validateEmail(email: string): string | null {
|
|
if (!email || email.trim() === '') {
|
|
return ErrorMessages.required;
|
|
}
|
|
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
if (!emailRegex.test(email.trim())) {
|
|
return ErrorMessages.invalidEmail;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Validate phone number format
|
|
*/
|
|
export function validatePhone(phone: string): string | null {
|
|
if (!phone || phone.trim() === '') {
|
|
return null; // Phone is usually optional
|
|
}
|
|
|
|
// Remove common formatting characters
|
|
const cleaned = phone.replace(/[\s\-\(\)\.]/g, '');
|
|
|
|
// Check for valid phone format (with or without country code)
|
|
const phoneRegex = /^\+?[1-9]\d{9,14}$/;
|
|
if (!phoneRegex.test(cleaned)) {
|
|
return ErrorMessages.invalidPhone;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Validate required field
|
|
*/
|
|
export function validateRequired(value: string | undefined | null, fieldName?: string): string | null {
|
|
if (!value || value.trim() === '') {
|
|
return fieldName ? `${fieldName} is required.` : ErrorMessages.required;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Validate minimum length
|
|
*/
|
|
export function validateMinLength(value: string, minLength: number): string | null {
|
|
if (value.length < minLength) {
|
|
return ErrorMessages.tooShort(minLength);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Validate maximum length
|
|
*/
|
|
export function validateMaxLength(value: string, maxLength: number): string | null {
|
|
if (value.length > maxLength) {
|
|
return ErrorMessages.tooLong(maxLength);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Validate OTP code format
|
|
*/
|
|
export function validateOtp(otp: string, length: number = 6): string | null {
|
|
if (!otp || otp.trim() === '') {
|
|
return ErrorMessages.required;
|
|
}
|
|
|
|
const cleaned = otp.trim();
|
|
if (cleaned.length !== length || !/^\d+$/.test(cleaned)) {
|
|
return ErrorMessages.invalidOtp;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Format error for display (capitalize first letter, add period if missing)
|
|
*/
|
|
export function formatErrorMessage(message: string): string {
|
|
if (!message) return ErrorMessages.genericError;
|
|
|
|
let formatted = message.trim();
|
|
|
|
// Capitalize first letter
|
|
formatted = formatted.charAt(0).toUpperCase() + formatted.slice(1);
|
|
|
|
// Add period if missing
|
|
if (!formatted.endsWith('.') && !formatted.endsWith('!') && !formatted.endsWith('?')) {
|
|
formatted += '.';
|
|
}
|
|
|
|
return formatted;
|
|
}
|
|
|
|
/**
|
|
* Combine multiple field errors into a single message
|
|
*/
|
|
export function combineFieldErrors(errors: { field: string; message: string }[]): string {
|
|
if (errors.length === 0) return '';
|
|
if (errors.length === 1) return errors[0].message;
|
|
|
|
return `Please fix ${errors.length} errors: ${errors.map((e) => e.message).join(', ')}`;
|
|
}
|
|
|
|
/**
|
|
* Get plural form for error count
|
|
*/
|
|
export function getErrorCountText(count: number): string {
|
|
if (count === 0) return 'No errors';
|
|
if (count === 1) return '1 error';
|
|
return `${count} errors`;
|
|
}
|
|
|
|
export default ErrorMessages;
|