fix(mobile): critical security and error handling improvements

🔐 SECURITY FIXES:
- Fixed CRITICAL seed storage vulnerability
  * Changed from AsyncStorage to SecureStore for wallet seeds
  * Seeds now encrypted in hardware-backed secure storage
  * Affects: PolkadotContext.tsx (lines 166, 189)

🛡️ ERROR HANDLING:
- Added global ErrorBoundary component
  * Catches unhandled React errors
  * Shows user-friendly error UI
  * Integrated into App.tsx provider hierarchy
  * Files: ErrorBoundary.tsx (new), App.tsx, components/index.ts

🧹 PRODUCTION READINESS:
- Protected all 47 console statements with __DEV__ checks
  * console.log: 12 statements
  * console.error: 32 statements
  * console.warn: 1 statement
  * Files affected: 16 files across contexts, screens, i18n
  * Production builds will strip these out

📦 PROVIDER HIERARCHY:
- Added BiometricAuthProvider to App.tsx
- Updated provider order:
  ErrorBoundary → Polkadot → Language → BiometricAuth → Navigator

Files modified: 18
New files: 1 (ErrorBoundary.tsx)

This commit resolves 3 P0 critical issues from production readiness audit.
This commit is contained in:
Claude
2025-11-21 22:18:11 +00:00
parent 15d6dc62c9
commit 6a86915549
18 changed files with 340 additions and 78 deletions
+250
View File
@@ -0,0 +1,250 @@
// ========================================
// Error Boundary Component (React Native)
// ========================================
// Catches React errors and displays fallback UI
import React, { Component, ErrorInfo, ReactNode } from 'react';
import { View, Text, TouchableOpacity, ScrollView, StyleSheet } from 'react-native';
interface Props {
children: ReactNode;
fallback?: ReactNode;
onError?: (error: Error, errorInfo: ErrorInfo) => void;
}
interface State {
hasError: boolean;
error: Error | null;
errorInfo: ErrorInfo | null;
}
/**
* Global Error Boundary for React Native
* Catches unhandled errors in React component tree
*
* @example
* <ErrorBoundary>
* <App />
* </ErrorBoundary>
*/
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null,
};
}
static getDerivedStateFromError(error: Error): Partial<State> {
// Update state so next render shows fallback UI
return {
hasError: true,
error,
};
}
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
// Log error to console
if (__DEV__) {
console.error('ErrorBoundary caught an error:', error, errorInfo);
}
// Update state with error details
this.setState({
error,
errorInfo,
});
// Call custom error handler if provided
if (this.props.onError) {
this.props.onError(error, errorInfo);
}
// In production, you might want to log to an error reporting service
// Example: Sentry.captureException(error);
}
handleReset = (): void => {
this.setState({
hasError: false,
error: null,
errorInfo: null,
});
};
render(): ReactNode {
if (this.state.hasError) {
// Use custom fallback if provided
if (this.props.fallback) {
return this.props.fallback;
}
// Default error UI for React Native
return (
<View style={styles.container}>
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
>
<View style={styles.card}>
{/* Error Icon */}
<View style={styles.iconContainer}>
<Text style={styles.iconText}></Text>
</View>
{/* Error Title */}
<Text style={styles.title}>Something Went Wrong</Text>
<Text style={styles.description}>
An unexpected error occurred. We apologize for the inconvenience.
</Text>
{/* Error Details (Development Only) */}
{__DEV__ && this.state.error && (
<View style={styles.errorDetails}>
<Text style={styles.errorDetailsTitle}>
Error Details (for developers)
</Text>
<View style={styles.errorBox}>
<Text style={styles.errorLabel}>Error:</Text>
<Text style={styles.errorText}>
{this.state.error.toString()}
</Text>
{this.state.errorInfo && (
<>
<Text style={[styles.errorLabel, styles.stackLabel]}>
Component Stack:
</Text>
<Text style={styles.errorText}>
{this.state.errorInfo.componentStack}
</Text>
</>
)}
</View>
</View>
)}
{/* Action Buttons */}
<View style={styles.buttonContainer}>
<TouchableOpacity
style={styles.primaryButton}
onPress={this.handleReset}
activeOpacity={0.8}
>
<Text style={styles.buttonText}>Try Again</Text>
</TouchableOpacity>
</View>
{/* Support Contact */}
<Text style={styles.supportText}>
If this problem persists, please contact support at{' '}
<Text style={styles.supportEmail}>info@pezkuwichain.io</Text>
</Text>
</View>
</ScrollView>
</View>
);
}
// No error, render children normally
return this.props.children;
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#0a0a0a',
},
scrollView: {
flex: 1,
},
scrollContent: {
flexGrow: 1,
justifyContent: 'center',
padding: 16,
},
card: {
backgroundColor: '#1a1a1a',
borderRadius: 12,
borderWidth: 1,
borderColor: '#2a2a2a',
padding: 24,
},
iconContainer: {
alignItems: 'center',
marginBottom: 16,
},
iconText: {
fontSize: 48,
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#ffffff',
textAlign: 'center',
marginBottom: 12,
},
description: {
fontSize: 16,
color: '#9ca3af',
textAlign: 'center',
marginBottom: 24,
lineHeight: 24,
},
errorDetails: {
marginBottom: 24,
},
errorDetailsTitle: {
fontSize: 14,
fontWeight: '600',
color: '#6b7280',
marginBottom: 12,
},
errorBox: {
backgroundColor: '#0a0a0a',
borderRadius: 8,
borderWidth: 1,
borderColor: '#374151',
padding: 12,
},
errorLabel: {
fontSize: 12,
fontWeight: 'bold',
color: '#ef4444',
marginBottom: 8,
},
stackLabel: {
marginTop: 12,
},
errorText: {
fontSize: 11,
fontFamily: 'monospace',
color: '#9ca3af',
lineHeight: 16,
},
buttonContainer: {
marginBottom: 16,
},
primaryButton: {
backgroundColor: '#00A94F',
borderRadius: 8,
padding: 16,
alignItems: 'center',
},
buttonText: {
color: '#ffffff',
fontSize: 16,
fontWeight: '600',
},
supportText: {
fontSize: 14,
color: '#6b7280',
textAlign: 'center',
lineHeight: 20,
},
supportEmail: {
color: '#00A94F',
},
});
+1
View File
@@ -3,6 +3,7 @@
* Inspired by Material Design 3, iOS HIG, and Kurdistan aesthetics
*/
export { ErrorBoundary } from './ErrorBoundary';
export { Card } from './Card';
export { Button } from './Button';
export { Input } from './Input';