WellNuo/components/OfflineBanner.tsx
Sergei 91e677178e Add offline mode graceful degradation
Implements comprehensive offline handling for API-first architecture:

Network Detection:
- Real-time connectivity monitoring via @react-native-community/netinfo
- useNetworkStatus hook for React components
- Utility functions: getNetworkStatus(), isOnline()
- Retry logic with exponential backoff

Offline-Aware API Layer:
- Wraps all API methods with network detection
- User-friendly error messages for offline states
- Automatic retries for read operations
- Custom offline messages for write operations

UI Components:
- OfflineBanner: Animated banner at top/bottom
- InlineOfflineBanner: Non-animated inline version
- Auto-shows/hides based on network status

Data Fetching Hooks:
- useOfflineAwareData: Hook for data fetching with offline handling
- useOfflineAwareMutation: Hook for create/update/delete operations
- Auto-refetch when network returns
- Optional polling support

Error Handling:
- Consistent error messages across app
- Network error detection
- Retry functionality with user feedback

Tests:
- Network status detection tests
- Offline-aware API wrapper tests
- 23 passing tests with full coverage

Documentation:
- Complete offline mode guide (docs/OFFLINE_MODE.md)
- Usage examples (components/examples/OfflineAwareExample.tsx)
- Best practices and troubleshooting

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-31 16:49:15 -08:00

146 lines
3.2 KiB
TypeScript

/**
* Offline Banner Component
*
* Displays a banner at the top of the screen when the device is offline.
* Automatically shows/hides based on network connectivity.
*/
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import Animated, {
useAnimatedStyle,
withTiming,
useSharedValue,
withSequence,
} from 'react-native-reanimated';
import { useNetworkStatus } from '@/utils/networkStatus';
interface OfflineBannerProps {
/**
* Custom message to display when offline
* Default: "No internet connection"
*/
message?: string;
/**
* Position of the banner
* Default: "top"
*/
position?: 'top' | 'bottom';
/**
* Background color
* Default: "#FF3B30" (red)
*/
backgroundColor?: string;
/**
* Text color
* Default: "#FFFFFF" (white)
*/
textColor?: string;
/**
* Height of the banner
* Default: 40
*/
height?: number;
}
export function OfflineBanner({
message = 'No internet connection',
position = 'top',
backgroundColor = '#FF3B30',
textColor = '#FFFFFF',
height = 40,
}: OfflineBannerProps) {
const { isOffline } = useNetworkStatus();
const translateY = useSharedValue(position === 'top' ? -height : height);
// Animate banner in/out based on network status
React.useEffect(() => {
if (isOffline) {
// Slide in with slight bounce
translateY.value = withSequence(
withTiming(0, { duration: 300 }),
withTiming(-2, { duration: 100 }),
withTiming(0, { duration: 100 })
);
} else {
// Slide out
translateY.value = withTiming(position === 'top' ? -height : height, {
duration: 200,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOffline, position, height]);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateY: translateY.value }],
}));
return (
<Animated.View
style={[
styles.container,
{
backgroundColor,
height,
[position]: 0,
},
animatedStyle,
]}
>
<Text style={[styles.text, { color: textColor }]}>{message}</Text>
</Animated.View>
);
}
/**
* Inline Offline Banner
* Displays within the component tree (not positioned absolutely)
* Use this for inline offline indicators
*/
export function InlineOfflineBanner({
message = 'No internet connection',
backgroundColor = '#FF3B30',
textColor = '#FFFFFF',
}: Omit<OfflineBannerProps, 'position' | 'height'>) {
const { isOffline } = useNetworkStatus();
if (!isOffline) {
return null;
}
return (
<View style={[styles.inlineContainer, { backgroundColor }]}>
<Text style={[styles.text, { color: textColor }]}>{message}</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
position: 'absolute',
left: 0,
right: 0,
justifyContent: 'center',
alignItems: 'center',
zIndex: 9999,
paddingHorizontal: 16,
},
inlineContainer: {
paddingVertical: 12,
paddingHorizontal: 16,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 16,
borderRadius: 8,
},
text: {
fontSize: 14,
fontWeight: '600',
textAlign: 'center',
},
});