- 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>
226 lines
5.9 KiB
TypeScript
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;
|