WellNuo/components/errors/FullScreenError.tsx
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

273 lines
6.5 KiB
TypeScript

/**
* FullScreenError - Full screen error state component
*
* Used for:
* - Critical errors that block the UI
* - Data loading failures
* - Network offline states
*/
import React from 'react';
import {
View,
Text,
TouchableOpacity,
StyleSheet,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { AppError, ErrorCategory } from '@/types/errors';
interface FullScreenErrorProps {
error?: AppError;
title?: string;
message?: string;
onRetry?: () => void;
onDismiss?: () => void;
showDismiss?: boolean;
retryLabel?: string;
dismissLabel?: string;
}
// Category-specific icons
const categoryIcons: Record<ErrorCategory, keyof typeof Ionicons.glyphMap> = {
network: 'cloud-offline',
timeout: 'time-outline',
authentication: 'lock-closed',
permission: 'shield-outline',
notFound: 'search-outline',
validation: 'warning',
conflict: 'git-merge',
rateLimit: 'hourglass',
server: 'server-outline',
client: 'alert-circle',
ble: 'bluetooth',
sensor: 'hardware-chip',
subscription: 'card',
unknown: 'help-circle',
};
export function FullScreenError({
error,
title,
message,
onRetry,
onDismiss,
showDismiss = false,
retryLabel = 'Try Again',
dismissLabel = 'Dismiss',
}: FullScreenErrorProps) {
const displayTitle = title || (error?.category === 'network' ? 'Connection Error' : 'Something Went Wrong');
const displayMessage = message || error?.userMessage || 'An unexpected error occurred. Please try again.';
const icon = error?.category ? categoryIcons[error.category] : 'alert-circle';
const isRetryable = error?.retry.isRetryable ?? !!onRetry;
return (
<View style={styles.container}>
<View style={styles.content}>
{/* Icon */}
<View style={styles.iconContainer}>
<Ionicons name={icon} size={64} color="#9CA3AF" />
</View>
{/* Title */}
<Text style={styles.title}>{displayTitle}</Text>
{/* Message */}
<Text style={styles.message}>{displayMessage}</Text>
{/* Action hint */}
{error?.actionHint && (
<Text style={styles.hint}>{error.actionHint}</Text>
)}
{/* Buttons */}
<View style={styles.buttons}>
{isRetryable && onRetry && (
<TouchableOpacity
style={styles.retryButton}
onPress={onRetry}
activeOpacity={0.8}
>
<Ionicons name="refresh" size={20} color="#fff" style={styles.buttonIcon} />
<Text style={styles.retryButtonText}>{retryLabel}</Text>
</TouchableOpacity>
)}
{showDismiss && onDismiss && (
<TouchableOpacity
style={styles.dismissButton}
onPress={onDismiss}
activeOpacity={0.8}
>
<Text style={styles.dismissButtonText}>{dismissLabel}</Text>
</TouchableOpacity>
)}
</View>
{/* Error ID for support */}
{error?.errorId && __DEV__ && (
<Text style={styles.errorId}>Error ID: {error.errorId}</Text>
)}
</View>
</View>
);
}
/**
* EmptyState - Component for empty data states (not strictly an error)
*/
interface EmptyStateProps {
icon?: keyof typeof Ionicons.glyphMap;
title: string;
message?: string;
actionLabel?: string;
onAction?: () => void;
}
export function EmptyState({
icon = 'folder-open-outline',
title,
message,
actionLabel,
onAction,
}: EmptyStateProps) {
return (
<View style={styles.container}>
<View style={styles.content}>
<View style={styles.iconContainer}>
<Ionicons name={icon} size={64} color="#9CA3AF" />
</View>
<Text style={styles.title}>{title}</Text>
{message && <Text style={styles.message}>{message}</Text>}
{actionLabel && onAction && (
<TouchableOpacity
style={styles.retryButton}
onPress={onAction}
activeOpacity={0.8}
>
<Text style={styles.retryButtonText}>{actionLabel}</Text>
</TouchableOpacity>
)}
</View>
</View>
);
}
/**
* OfflineState - Specific state for offline/no connection
*/
interface OfflineStateProps {
onRetry?: () => void;
}
export function OfflineState({ onRetry }: OfflineStateProps) {
return (
<View style={styles.container}>
<View style={styles.content}>
<View style={styles.iconContainer}>
<Ionicons name="cloud-offline" size={64} color="#9CA3AF" />
</View>
<Text style={styles.title}>No Internet Connection</Text>
<Text style={styles.message}>
Please check your connection and try again.
</Text>
{onRetry && (
<TouchableOpacity
style={styles.retryButton}
onPress={onRetry}
activeOpacity={0.8}
>
<Ionicons name="refresh" size={20} color="#fff" style={styles.buttonIcon} />
<Text style={styles.retryButtonText}>Try Again</Text>
</TouchableOpacity>
)}
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#fff',
padding: 24,
},
content: {
alignItems: 'center',
maxWidth: 320,
},
iconContainer: {
width: 120,
height: 120,
borderRadius: 60,
backgroundColor: '#F3F4F6',
justifyContent: 'center',
alignItems: 'center',
marginBottom: 24,
},
title: {
fontSize: 20,
fontWeight: '700',
color: '#1F2937',
textAlign: 'center',
marginBottom: 8,
},
message: {
fontSize: 15,
color: '#6B7280',
textAlign: 'center',
lineHeight: 22,
marginBottom: 8,
},
hint: {
fontSize: 13,
color: '#9CA3AF',
textAlign: 'center',
marginBottom: 24,
},
buttons: {
marginTop: 16,
gap: 12,
width: '100%',
},
retryButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#3B82F6',
paddingVertical: 14,
paddingHorizontal: 24,
borderRadius: 12,
width: '100%',
},
buttonIcon: {
marginRight: 8,
},
retryButtonText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
dismissButton: {
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 12,
paddingHorizontal: 24,
},
dismissButtonText: {
color: '#6B7280',
fontSize: 15,
fontWeight: '500',
},
errorId: {
marginTop: 24,
fontSize: 11,
color: '#D1D5DB',
fontFamily: 'monospace',
},
});
export default FullScreenError;