- 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>
273 lines
6.5 KiB
TypeScript
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;
|