WellNuo/components/ble/ConnectionStatusIndicator.tsx
Sergei f8156b2dc7 Add BLE auto-reconnect with exponential backoff
- Add ReconnectConfig and ReconnectState types for configurable reconnect behavior
- Implement auto-reconnect in BLEManager with exponential backoff (default: 3 attempts, 1.5x multiplier)
- Add connection monitoring via device.onDisconnected() for unexpected disconnections
- Update BLEContext with reconnectingDevices state and reconnect actions
- Create ConnectionStatusIndicator component for visual connection feedback
- Enhance device settings screen with reconnect UI and manual reconnect capability
- Add comprehensive tests for reconnect logic and UI component

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

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

226 lines
5.9 KiB
TypeScript

import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { BLEConnectionState } from '@/services/ble/types';
import {
AppColors,
BorderRadius,
FontSizes,
FontWeights,
Spacing,
} from '@/constants/theme';
interface ConnectionStatusIndicatorProps {
connectionState: BLEConnectionState;
isReconnecting?: boolean;
reconnectAttempts?: number;
maxAttempts?: number;
onReconnect?: () => void;
onCancel?: () => void;
compact?: boolean;
}
/**
* Visual indicator for BLE connection status with reconnect controls
*/
export function ConnectionStatusIndicator({
connectionState,
isReconnecting = false,
reconnectAttempts = 0,
maxAttempts = 3,
onReconnect,
onCancel,
compact = false,
}: ConnectionStatusIndicatorProps) {
const getStatusConfig = () => {
if (isReconnecting) {
return {
icon: 'sync' as const,
color: AppColors.warning,
bgColor: AppColors.warningLight,
label: `Reconnecting... (${reconnectAttempts}/${maxAttempts})`,
showSpinner: true,
};
}
switch (connectionState) {
case BLEConnectionState.READY:
return {
icon: 'bluetooth' as const,
color: AppColors.success,
bgColor: AppColors.successLight,
label: 'Connected',
showSpinner: false,
};
case BLEConnectionState.CONNECTING:
case BLEConnectionState.DISCOVERING:
return {
icon: 'bluetooth' as const,
color: AppColors.primary,
bgColor: AppColors.primaryLighter,
label: 'Connecting...',
showSpinner: true,
};
case BLEConnectionState.DISCONNECTING:
return {
icon: 'bluetooth' as const,
color: AppColors.textMuted,
bgColor: AppColors.background,
label: 'Disconnecting...',
showSpinner: true,
};
case BLEConnectionState.ERROR:
return {
icon: 'warning' as const,
color: AppColors.error,
bgColor: AppColors.errorLight,
label: 'Connection Error',
showSpinner: false,
};
case BLEConnectionState.DISCONNECTED:
default:
return {
icon: 'bluetooth-outline' as const,
color: AppColors.textMuted,
bgColor: AppColors.background,
label: 'Not Connected',
showSpinner: false,
};
}
};
const config = getStatusConfig();
if (compact) {
return (
<View style={[styles.compactContainer, { backgroundColor: config.bgColor }]}>
{config.showSpinner ? (
<ActivityIndicator size="small" color={config.color} />
) : (
<Ionicons name={config.icon} size={16} color={config.color} />
)}
<Text style={[styles.compactLabel, { color: config.color }]}>
{config.label}
</Text>
</View>
);
}
return (
<View style={[styles.container, { backgroundColor: config.bgColor }]}>
<View style={styles.statusRow}>
<View style={styles.iconContainer}>
{config.showSpinner ? (
<ActivityIndicator size="small" color={config.color} />
) : (
<Ionicons name={config.icon} size={24} color={config.color} />
)}
</View>
<View style={styles.textContainer}>
<Text style={[styles.label, { color: config.color }]}>
{config.label}
</Text>
{isReconnecting && (
<Text style={styles.sublabel}>
Attempting to restore connection...
</Text>
)}
</View>
</View>
{/* Action buttons */}
{(connectionState === BLEConnectionState.ERROR ||
connectionState === BLEConnectionState.DISCONNECTED) &&
!isReconnecting &&
onReconnect && (
<TouchableOpacity
style={styles.actionButton}
onPress={onReconnect}
>
<Ionicons name="refresh" size={16} color={AppColors.primary} />
<Text style={styles.actionButtonText}>Reconnect</Text>
</TouchableOpacity>
)}
{isReconnecting && onCancel && (
<TouchableOpacity
style={[styles.actionButton, styles.cancelButton]}
onPress={onCancel}
>
<Ionicons name="close" size={16} color={AppColors.error} />
<Text style={[styles.actionButtonText, styles.cancelButtonText]}>
Cancel
</Text>
</TouchableOpacity>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
borderRadius: BorderRadius.lg,
padding: Spacing.md,
},
compactContainer: {
flexDirection: 'row',
alignItems: 'center',
gap: Spacing.xs,
paddingHorizontal: Spacing.sm,
paddingVertical: Spacing.xs,
borderRadius: BorderRadius.md,
},
compactLabel: {
fontSize: FontSizes.xs,
fontWeight: FontWeights.medium,
},
statusRow: {
flexDirection: 'row',
alignItems: 'center',
gap: Spacing.md,
},
iconContainer: {
width: 32,
height: 32,
justifyContent: 'center',
alignItems: 'center',
},
textContainer: {
flex: 1,
},
label: {
fontSize: FontSizes.base,
fontWeight: FontWeights.semibold,
},
sublabel: {
fontSize: FontSizes.sm,
color: AppColors.textMuted,
marginTop: 2,
},
actionButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: Spacing.xs,
marginTop: Spacing.md,
paddingVertical: Spacing.sm,
paddingHorizontal: Spacing.md,
borderRadius: BorderRadius.md,
borderWidth: 1,
borderColor: AppColors.primary,
},
actionButtonText: {
fontSize: FontSizes.sm,
fontWeight: FontWeights.medium,
color: AppColors.primary,
},
cancelButton: {
borderColor: AppColors.error,
},
cancelButtonText: {
color: AppColors.error,
},
});
export default ConnectionStatusIndicator;