Removed all console.log, console.error, console.warn, console.info, and console.debug statements from the main source code to clean up production output. Changes: - Removed 400+ console statements from TypeScript/TSX files - Cleaned BLE services (BLEManager.ts, MockBLEManager.ts) - Cleaned API services, contexts, hooks, and components - Cleaned WiFi setup and sensor management screens - Preserved console statements in test files (*.test.ts, __tests__/) - TypeScript compilation verified successfully 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
659 lines
19 KiB
TypeScript
659 lines
19 KiB
TypeScript
import React, { useState, useCallback, useEffect } from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
ScrollView,
|
|
TouchableOpacity,
|
|
Alert,
|
|
ActivityIndicator,
|
|
} from 'react-native';
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
import { router, useLocalSearchParams, useFocusEffect } from 'expo-router';
|
|
import { useBLE } from '@/contexts/BLEContext';
|
|
import { useBeneficiary } from '@/contexts/BeneficiaryContext';
|
|
import {
|
|
AppColors,
|
|
BorderRadius,
|
|
FontSizes,
|
|
FontWeights,
|
|
Spacing,
|
|
Shadows,
|
|
} from '@/constants/theme';
|
|
|
|
export default function AddSensorScreen() {
|
|
const { id } = useLocalSearchParams<{ id: string }>();
|
|
const { currentBeneficiary } = useBeneficiary();
|
|
const {
|
|
foundDevices,
|
|
isScanning,
|
|
connectedDevices,
|
|
isBLEAvailable,
|
|
scanDevices,
|
|
stopScan,
|
|
} = useBLE();
|
|
|
|
const [selectedDevices, setSelectedDevices] = useState<Set<string>>(new Set());
|
|
|
|
// Select all devices by default when scan completes
|
|
useEffect(() => {
|
|
if (foundDevices.length > 0 && !isScanning) {
|
|
setSelectedDevices(new Set(foundDevices.map(d => d.id)));
|
|
}
|
|
}, [foundDevices, isScanning]);
|
|
|
|
// Cleanup: Stop scan when screen loses focus or component unmounts
|
|
useFocusEffect(
|
|
useCallback(() => {
|
|
|
|
// Cleanup function - called when screen loses focus
|
|
return () => {
|
|
if (isScanning) {
|
|
stopScan();
|
|
}
|
|
};
|
|
}, [isScanning, stopScan])
|
|
);
|
|
|
|
const toggleDeviceSelection = (deviceId: string) => {
|
|
setSelectedDevices(prev => {
|
|
const next = new Set(prev);
|
|
if (next.has(deviceId)) {
|
|
next.delete(deviceId);
|
|
} else {
|
|
next.add(deviceId);
|
|
}
|
|
return next;
|
|
});
|
|
};
|
|
|
|
const toggleSelectAll = () => {
|
|
if (selectedDevices.size === foundDevices.length) {
|
|
setSelectedDevices(new Set());
|
|
} else {
|
|
setSelectedDevices(new Set(foundDevices.map(d => d.id)));
|
|
}
|
|
};
|
|
|
|
const selectedCount = selectedDevices.size;
|
|
|
|
const handleScan = async () => {
|
|
try {
|
|
await scanDevices();
|
|
} catch (error: any) {
|
|
Alert.alert('Scan Failed', error.message || 'Failed to scan for sensors. Please try again.');
|
|
}
|
|
};
|
|
|
|
const handleAddSelected = () => {
|
|
if (selectedCount === 0) {
|
|
Alert.alert('No Sensors Selected', 'Please select at least one sensor to add.');
|
|
return;
|
|
}
|
|
|
|
const devices = foundDevices.filter(d => selectedDevices.has(d.id));
|
|
|
|
// Navigate to Setup WiFi screen with selected devices
|
|
router.push({
|
|
pathname: `/(tabs)/beneficiaries/${id}/setup-wifi` as any,
|
|
params: {
|
|
devices: JSON.stringify(devices.map(d => ({
|
|
id: d.id,
|
|
name: d.name,
|
|
mac: d.mac,
|
|
wellId: d.wellId,
|
|
}))),
|
|
},
|
|
});
|
|
};
|
|
|
|
const getSignalIcon = (rssi: number) => {
|
|
if (rssi >= -50) return 'cellular';
|
|
if (rssi >= -60) return 'cellular-outline';
|
|
if (rssi >= -70) return 'cellular';
|
|
return 'cellular-outline';
|
|
};
|
|
|
|
const getSignalColor = (rssi: number) => {
|
|
if (rssi >= -50) return AppColors.success;
|
|
if (rssi >= -60) return AppColors.info;
|
|
if (rssi >= -70) return AppColors.warning;
|
|
return AppColors.error;
|
|
};
|
|
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
|
{/* Header */}
|
|
<View style={styles.header}>
|
|
<TouchableOpacity style={styles.backButton} onPress={() => router.back()}>
|
|
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
|
|
</TouchableOpacity>
|
|
<Text style={styles.headerTitle}>Add Sensor</Text>
|
|
<View style={styles.placeholder} />
|
|
</View>
|
|
|
|
{/* Simulator Warning */}
|
|
{!isBLEAvailable && (
|
|
<View style={styles.simulatorWarning}>
|
|
<Ionicons name="information-circle" size={18} color={AppColors.warning} />
|
|
<Text style={styles.simulatorWarningText}>
|
|
Running in Simulator - showing mock sensors
|
|
</Text>
|
|
</View>
|
|
)}
|
|
|
|
<ScrollView style={styles.content} contentContainerStyle={styles.scrollContent}>
|
|
{/* Instructions */}
|
|
<View style={styles.instructionsCard}>
|
|
<Text style={styles.instructionsTitle}>How to Add a Sensor</Text>
|
|
<View style={styles.step}>
|
|
<View style={styles.stepNumber}>
|
|
<Text style={styles.stepNumberText}>1</Text>
|
|
</View>
|
|
<Text style={styles.stepText}>Make sure the WP sensor is powered on and nearby</Text>
|
|
</View>
|
|
<View style={styles.step}>
|
|
<View style={styles.stepNumber}>
|
|
<Text style={styles.stepNumberText}>2</Text>
|
|
</View>
|
|
<Text style={styles.stepText}>Tap "Scan for Sensors" to search for available devices</Text>
|
|
</View>
|
|
<View style={styles.step}>
|
|
<View style={styles.stepNumber}>
|
|
<Text style={styles.stepNumberText}>3</Text>
|
|
</View>
|
|
<Text style={styles.stepText}>Select your sensor from the list to connect</Text>
|
|
</View>
|
|
<View style={styles.step}>
|
|
<View style={styles.stepNumber}>
|
|
<Text style={styles.stepNumberText}>4</Text>
|
|
</View>
|
|
<Text style={styles.stepText}>Configure WiFi settings to complete setup</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Scan Button */}
|
|
{!isScanning && foundDevices.length === 0 && (
|
|
<TouchableOpacity style={styles.scanButton} onPress={handleScan}>
|
|
<Ionicons name="bluetooth" size={24} color={AppColors.white} />
|
|
<Text style={styles.scanButtonText}>Scan for Sensors</Text>
|
|
</TouchableOpacity>
|
|
)}
|
|
|
|
{/* Scanning Indicator */}
|
|
{isScanning && (
|
|
<View style={styles.scanningCard}>
|
|
<ActivityIndicator size="large" color={AppColors.primary} />
|
|
<Text style={styles.scanningText}>Scanning for WP sensors...</Text>
|
|
<TouchableOpacity style={styles.stopScanButton} onPress={stopScan}>
|
|
<Text style={styles.stopScanText}>Stop Scan</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
)}
|
|
|
|
{/* Found Devices */}
|
|
{foundDevices.length > 0 && !isScanning && (
|
|
<>
|
|
<View style={styles.sectionHeader}>
|
|
<Text style={styles.sectionTitle}>Found Sensors ({foundDevices.length})</Text>
|
|
<View style={styles.sectionActions}>
|
|
<TouchableOpacity style={styles.selectAllButton} onPress={toggleSelectAll}>
|
|
<Ionicons
|
|
name={selectedDevices.size === foundDevices.length ? 'checkbox' : 'square-outline'}
|
|
size={18}
|
|
color={AppColors.primary}
|
|
/>
|
|
<Text style={styles.selectAllText}>
|
|
{selectedDevices.size === foundDevices.length ? 'Deselect All' : 'Select All'}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity style={styles.rescanButton} onPress={handleScan}>
|
|
<Ionicons name="refresh" size={18} color={AppColors.primary} />
|
|
<Text style={styles.rescanText}>Rescan</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
|
|
<View style={styles.devicesList}>
|
|
{foundDevices.map((device) => {
|
|
const isConnected = connectedDevices.has(device.id);
|
|
const isSelected = selectedDevices.has(device.id);
|
|
|
|
return (
|
|
<TouchableOpacity
|
|
key={device.id}
|
|
style={[
|
|
styles.deviceCard,
|
|
isSelected && styles.deviceCardSelected,
|
|
isConnected && styles.deviceCardConnected,
|
|
]}
|
|
onPress={() => toggleDeviceSelection(device.id)}
|
|
disabled={isConnected}
|
|
activeOpacity={0.7}
|
|
>
|
|
<View style={styles.checkboxContainer}>
|
|
<View style={[
|
|
styles.checkbox,
|
|
isSelected && styles.checkboxSelected,
|
|
isConnected && styles.checkboxDisabled,
|
|
]}>
|
|
{isSelected && (
|
|
<Ionicons name="checkmark" size={16} color={AppColors.white} />
|
|
)}
|
|
</View>
|
|
</View>
|
|
<View style={styles.deviceInfo}>
|
|
<View style={styles.deviceIcon}>
|
|
<Ionicons name="water" size={24} color={AppColors.primary} />
|
|
</View>
|
|
<View style={styles.deviceDetails}>
|
|
<Text style={styles.deviceName}>{device.name}</Text>
|
|
{device.wellId && (
|
|
<Text style={styles.deviceMeta}>Well ID: {device.wellId}</Text>
|
|
)}
|
|
<View style={styles.signalRow}>
|
|
<Ionicons
|
|
name={getSignalIcon(device.rssi)}
|
|
size={14}
|
|
color={getSignalColor(device.rssi)}
|
|
/>
|
|
<Text style={[styles.signalText, { color: getSignalColor(device.rssi) }]}>
|
|
{device.rssi} dBm
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
{isConnected && (
|
|
<View style={styles.alreadyAddedBadge}>
|
|
<Text style={styles.alreadyAddedText}>Added</Text>
|
|
</View>
|
|
)}
|
|
</TouchableOpacity>
|
|
);
|
|
})}
|
|
</View>
|
|
|
|
{/* Add Selected Button */}
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.addSelectedButton,
|
|
selectedCount === 0 && styles.addSelectedButtonDisabled,
|
|
]}
|
|
onPress={handleAddSelected}
|
|
disabled={selectedCount === 0}
|
|
>
|
|
<Ionicons name="add-circle" size={24} color={AppColors.white} />
|
|
<Text style={styles.addSelectedButtonText}>
|
|
Add Selected ({selectedCount})
|
|
</Text>
|
|
</TouchableOpacity>
|
|
</>
|
|
)}
|
|
|
|
{/* Empty State (after scan completed, no devices found) */}
|
|
{!isScanning && foundDevices.length === 0 && (
|
|
<View style={styles.emptyState}>
|
|
<View style={styles.emptyIconContainer}>
|
|
<Ionicons name="search-outline" size={48} color={AppColors.textMuted} />
|
|
</View>
|
|
<Text style={styles.emptyTitle}>No Sensors Found</Text>
|
|
<Text style={styles.emptyText}>
|
|
Make sure your WP sensor is powered on and within range, then try scanning again.
|
|
</Text>
|
|
</View>
|
|
)}
|
|
|
|
{/* Help Card */}
|
|
<View style={styles.helpCard}>
|
|
<View style={styles.helpHeader}>
|
|
<Ionicons name="help-circle" size={20} color={AppColors.info} />
|
|
<Text style={styles.helpTitle}>Troubleshooting</Text>
|
|
</View>
|
|
<Text style={styles.helpText}>
|
|
• Sensor not showing up? Make sure it's powered on and the LED is blinking{'\n'}
|
|
• Weak signal? Move closer to the sensor{'\n'}
|
|
• Connection fails? Try restarting the sensor{'\n'}
|
|
• Still having issues? Contact support for assistance
|
|
</Text>
|
|
</View>
|
|
</ScrollView>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: AppColors.background,
|
|
},
|
|
header: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
paddingHorizontal: Spacing.md,
|
|
paddingVertical: Spacing.sm,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: AppColors.border,
|
|
},
|
|
backButton: {
|
|
padding: Spacing.xs,
|
|
},
|
|
headerTitle: {
|
|
fontSize: FontSizes.lg,
|
|
fontWeight: FontWeights.semibold,
|
|
color: AppColors.textPrimary,
|
|
},
|
|
placeholder: {
|
|
width: 32,
|
|
},
|
|
simulatorWarning: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
backgroundColor: AppColors.warningLight,
|
|
paddingVertical: Spacing.xs,
|
|
paddingHorizontal: Spacing.md,
|
|
gap: Spacing.xs,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: AppColors.warning,
|
|
},
|
|
simulatorWarningText: {
|
|
fontSize: FontSizes.xs,
|
|
color: AppColors.warning,
|
|
fontWeight: FontWeights.medium,
|
|
flex: 1,
|
|
},
|
|
content: {
|
|
flex: 1,
|
|
},
|
|
scrollContent: {
|
|
padding: Spacing.lg,
|
|
paddingBottom: Spacing.xxl,
|
|
},
|
|
// Instructions
|
|
instructionsCard: {
|
|
backgroundColor: AppColors.surface,
|
|
borderRadius: BorderRadius.xl,
|
|
padding: Spacing.lg,
|
|
marginBottom: Spacing.lg,
|
|
...Shadows.sm,
|
|
},
|
|
instructionsTitle: {
|
|
fontSize: FontSizes.base,
|
|
fontWeight: FontWeights.semibold,
|
|
color: AppColors.textPrimary,
|
|
marginBottom: Spacing.md,
|
|
},
|
|
step: {
|
|
flexDirection: 'row',
|
|
alignItems: 'flex-start',
|
|
marginBottom: Spacing.sm,
|
|
gap: Spacing.sm,
|
|
},
|
|
stepNumber: {
|
|
width: 24,
|
|
height: 24,
|
|
borderRadius: 12,
|
|
backgroundColor: AppColors.primary,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
stepNumberText: {
|
|
fontSize: FontSizes.xs,
|
|
fontWeight: FontWeights.bold,
|
|
color: AppColors.white,
|
|
},
|
|
stepText: {
|
|
flex: 1,
|
|
fontSize: FontSizes.sm,
|
|
color: AppColors.textSecondary,
|
|
lineHeight: 20,
|
|
paddingTop: 2,
|
|
},
|
|
// Scan Button
|
|
scanButton: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
backgroundColor: AppColors.primary,
|
|
paddingVertical: Spacing.md,
|
|
borderRadius: BorderRadius.lg,
|
|
marginBottom: Spacing.lg,
|
|
gap: Spacing.sm,
|
|
...Shadows.md,
|
|
},
|
|
scanButtonText: {
|
|
fontSize: FontSizes.base,
|
|
fontWeight: FontWeights.semibold,
|
|
color: AppColors.white,
|
|
},
|
|
// Scanning
|
|
scanningCard: {
|
|
backgroundColor: AppColors.surface,
|
|
borderRadius: BorderRadius.xl,
|
|
padding: Spacing.xl,
|
|
alignItems: 'center',
|
|
marginBottom: Spacing.lg,
|
|
...Shadows.sm,
|
|
},
|
|
scanningText: {
|
|
fontSize: FontSizes.base,
|
|
color: AppColors.textSecondary,
|
|
marginTop: Spacing.md,
|
|
marginBottom: Spacing.md,
|
|
},
|
|
stopScanButton: {
|
|
paddingVertical: Spacing.sm,
|
|
paddingHorizontal: Spacing.lg,
|
|
},
|
|
stopScanText: {
|
|
fontSize: FontSizes.sm,
|
|
fontWeight: FontWeights.medium,
|
|
color: AppColors.error,
|
|
},
|
|
// Section Header
|
|
sectionHeader: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
marginBottom: Spacing.md,
|
|
},
|
|
sectionTitle: {
|
|
fontSize: FontSizes.sm,
|
|
fontWeight: FontWeights.semibold,
|
|
color: AppColors.textSecondary,
|
|
textTransform: 'uppercase',
|
|
letterSpacing: 0.5,
|
|
},
|
|
sectionActions: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: Spacing.md,
|
|
},
|
|
selectAllButton: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 4,
|
|
},
|
|
selectAllText: {
|
|
fontSize: FontSizes.sm,
|
|
fontWeight: FontWeights.medium,
|
|
color: AppColors.primary,
|
|
},
|
|
rescanButton: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 4,
|
|
},
|
|
rescanText: {
|
|
fontSize: FontSizes.sm,
|
|
fontWeight: FontWeights.medium,
|
|
color: AppColors.primary,
|
|
},
|
|
// Devices List
|
|
devicesList: {
|
|
gap: Spacing.md,
|
|
marginBottom: Spacing.lg,
|
|
},
|
|
deviceCard: {
|
|
backgroundColor: AppColors.surface,
|
|
borderRadius: BorderRadius.lg,
|
|
padding: Spacing.md,
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
...Shadows.xs,
|
|
},
|
|
deviceCardSelected: {
|
|
borderWidth: 2,
|
|
borderColor: AppColors.primary,
|
|
},
|
|
deviceCardConnected: {
|
|
borderWidth: 2,
|
|
borderColor: AppColors.success,
|
|
opacity: 0.6,
|
|
},
|
|
checkboxContainer: {
|
|
marginRight: Spacing.sm,
|
|
},
|
|
checkbox: {
|
|
width: 24,
|
|
height: 24,
|
|
borderRadius: BorderRadius.sm,
|
|
borderWidth: 2,
|
|
borderColor: AppColors.border,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
backgroundColor: AppColors.white,
|
|
},
|
|
checkboxSelected: {
|
|
backgroundColor: AppColors.primary,
|
|
borderColor: AppColors.primary,
|
|
},
|
|
checkboxDisabled: {
|
|
backgroundColor: AppColors.surfaceSecondary,
|
|
borderColor: AppColors.border,
|
|
},
|
|
deviceInfo: {
|
|
flex: 1,
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: Spacing.md,
|
|
},
|
|
deviceIcon: {
|
|
width: 48,
|
|
height: 48,
|
|
borderRadius: BorderRadius.lg,
|
|
backgroundColor: AppColors.primaryLighter,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
deviceDetails: {
|
|
flex: 1,
|
|
},
|
|
deviceName: {
|
|
fontSize: FontSizes.base,
|
|
fontWeight: FontWeights.semibold,
|
|
color: AppColors.textPrimary,
|
|
marginBottom: 2,
|
|
},
|
|
deviceMeta: {
|
|
fontSize: FontSizes.xs,
|
|
color: AppColors.textMuted,
|
|
marginBottom: 4,
|
|
},
|
|
signalRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 4,
|
|
},
|
|
signalText: {
|
|
fontSize: FontSizes.xs,
|
|
fontWeight: FontWeights.medium,
|
|
},
|
|
alreadyAddedBadge: {
|
|
backgroundColor: AppColors.successLight,
|
|
paddingVertical: Spacing.xs,
|
|
paddingHorizontal: Spacing.sm,
|
|
borderRadius: BorderRadius.sm,
|
|
},
|
|
alreadyAddedText: {
|
|
fontSize: FontSizes.xs,
|
|
fontWeight: FontWeights.medium,
|
|
color: AppColors.success,
|
|
},
|
|
// Add Selected Button
|
|
addSelectedButton: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
backgroundColor: AppColors.primary,
|
|
paddingVertical: Spacing.md,
|
|
borderRadius: BorderRadius.lg,
|
|
marginTop: Spacing.md,
|
|
marginBottom: Spacing.lg,
|
|
gap: Spacing.sm,
|
|
...Shadows.md,
|
|
},
|
|
addSelectedButtonDisabled: {
|
|
backgroundColor: AppColors.textMuted,
|
|
},
|
|
addSelectedButtonText: {
|
|
fontSize: FontSizes.base,
|
|
fontWeight: FontWeights.semibold,
|
|
color: AppColors.white,
|
|
},
|
|
// Empty State
|
|
emptyState: {
|
|
alignItems: 'center',
|
|
padding: Spacing.xl,
|
|
backgroundColor: AppColors.surface,
|
|
borderRadius: BorderRadius.xl,
|
|
marginBottom: Spacing.lg,
|
|
...Shadows.sm,
|
|
},
|
|
emptyIconContainer: {
|
|
width: 80,
|
|
height: 80,
|
|
borderRadius: 40,
|
|
backgroundColor: AppColors.surfaceSecondary,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
marginBottom: Spacing.md,
|
|
},
|
|
emptyTitle: {
|
|
fontSize: FontSizes.lg,
|
|
fontWeight: FontWeights.semibold,
|
|
color: AppColors.textPrimary,
|
|
marginBottom: Spacing.xs,
|
|
},
|
|
emptyText: {
|
|
fontSize: FontSizes.sm,
|
|
color: AppColors.textMuted,
|
|
textAlign: 'center',
|
|
lineHeight: 20,
|
|
},
|
|
// Help Card
|
|
helpCard: {
|
|
backgroundColor: AppColors.infoLight,
|
|
borderRadius: BorderRadius.lg,
|
|
padding: Spacing.md,
|
|
},
|
|
helpHeader: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: Spacing.sm,
|
|
marginBottom: Spacing.xs,
|
|
},
|
|
helpTitle: {
|
|
fontSize: FontSizes.sm,
|
|
fontWeight: FontWeights.semibold,
|
|
color: AppColors.info,
|
|
},
|
|
helpText: {
|
|
fontSize: FontSizes.sm,
|
|
color: AppColors.info,
|
|
lineHeight: 20,
|
|
},
|
|
});
|