WellNuo/utils/errorMessages.ts
Sergei a238b7e35f Add comprehensive error handling system
- 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>
2026-01-31 17:43:07 -08:00

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;