Add BLE permissions handling with graceful fallback
- Create permissions helper module with comprehensive error handling - Update BLEManager to use new permission system - Add permission state tracking in BLEContext - Improve add-sensor screen with permission error banner - Add "Open Settings" button for permission issues - Handle Android 12+ and older permission models - Provide user-friendly error messages for all states 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
d499d9d62a
commit
5d40da0409
@ -7,12 +7,12 @@ import {
|
|||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
Alert,
|
Alert,
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
|
Linking,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { router, useLocalSearchParams, useFocusEffect } from 'expo-router';
|
import { router, useLocalSearchParams, useFocusEffect } from 'expo-router';
|
||||||
import { useBLE } from '@/contexts/BLEContext';
|
import { useBLE } from '@/contexts/BLEContext';
|
||||||
import { useBeneficiary } from '@/contexts/BeneficiaryContext';
|
|
||||||
import {
|
import {
|
||||||
AppColors,
|
AppColors,
|
||||||
BorderRadius,
|
BorderRadius,
|
||||||
@ -24,14 +24,16 @@ import {
|
|||||||
|
|
||||||
export default function AddSensorScreen() {
|
export default function AddSensorScreen() {
|
||||||
const { id } = useLocalSearchParams<{ id: string }>();
|
const { id } = useLocalSearchParams<{ id: string }>();
|
||||||
const { currentBeneficiary } = useBeneficiary();
|
|
||||||
const {
|
const {
|
||||||
foundDevices,
|
foundDevices,
|
||||||
isScanning,
|
isScanning,
|
||||||
connectedDevices,
|
connectedDevices,
|
||||||
isBLEAvailable,
|
isBLEAvailable,
|
||||||
|
error,
|
||||||
|
permissionError,
|
||||||
scanDevices,
|
scanDevices,
|
||||||
stopScan,
|
stopScan,
|
||||||
|
clearError,
|
||||||
} = useBLE();
|
} = useBLE();
|
||||||
|
|
||||||
const [selectedDevices, setSelectedDevices] = useState<Set<string>>(new Set());
|
const [selectedDevices, setSelectedDevices] = useState<Set<string>>(new Set());
|
||||||
@ -80,12 +82,25 @@ export default function AddSensorScreen() {
|
|||||||
|
|
||||||
const handleScan = async () => {
|
const handleScan = async () => {
|
||||||
try {
|
try {
|
||||||
|
// Clear any previous errors
|
||||||
|
clearError();
|
||||||
|
|
||||||
|
// Perform scan
|
||||||
await scanDevices();
|
await scanDevices();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
Alert.alert('Scan Failed', error.message || 'Failed to scan for sensors. Please try again.');
|
// Error is already set in BLE context, but show alert for critical issues
|
||||||
|
if (!permissionError) {
|
||||||
|
// Non-permission errors - show generic alert
|
||||||
|
Alert.alert('Scan Failed', error.message || 'Failed to scan for sensors. Please try again.');
|
||||||
|
}
|
||||||
|
// Permission errors are already shown with proper dialogs by checkBLEReadiness
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOpenSettings = () => {
|
||||||
|
Linking.openSettings();
|
||||||
|
};
|
||||||
|
|
||||||
const handleAddSelected = () => {
|
const handleAddSelected = () => {
|
||||||
if (selectedCount === 0) {
|
if (selectedCount === 0) {
|
||||||
Alert.alert('No Sensors Selected', 'Please select at least one sensor to add.');
|
Alert.alert('No Sensors Selected', 'Please select at least one sensor to add.');
|
||||||
@ -143,6 +158,20 @@ export default function AddSensorScreen() {
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Permission Error Banner */}
|
||||||
|
{permissionError && error && (
|
||||||
|
<View style={styles.permissionError}>
|
||||||
|
<View style={styles.permissionErrorContent}>
|
||||||
|
<Ionicons name="warning" size={20} color={AppColors.error} />
|
||||||
|
<Text style={styles.permissionErrorText}>{error}</Text>
|
||||||
|
</View>
|
||||||
|
<TouchableOpacity style={styles.settingsButton} onPress={handleOpenSettings}>
|
||||||
|
<Text style={styles.settingsButtonText}>Open Settings</Text>
|
||||||
|
<Ionicons name="settings-outline" size={16} color={AppColors.error} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
<ScrollView style={styles.content} contentContainerStyle={styles.scrollContent}>
|
<ScrollView style={styles.content} contentContainerStyle={styles.scrollContent}>
|
||||||
{/* Instructions */}
|
{/* Instructions */}
|
||||||
<View style={styles.instructionsCard}>
|
<View style={styles.instructionsCard}>
|
||||||
@ -157,7 +186,7 @@ export default function AddSensorScreen() {
|
|||||||
<View style={styles.stepNumber}>
|
<View style={styles.stepNumber}>
|
||||||
<Text style={styles.stepNumberText}>2</Text>
|
<Text style={styles.stepNumberText}>2</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.stepText}>Tap "Scan for Sensors" to search for available devices</Text>
|
<Text style={styles.stepText}>Tap “Scan for Sensors” to search for available devices</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.step}>
|
<View style={styles.step}>
|
||||||
<View style={styles.stepNumber}>
|
<View style={styles.stepNumber}>
|
||||||
@ -312,7 +341,7 @@ export default function AddSensorScreen() {
|
|||||||
<Text style={styles.helpTitle}>Troubleshooting</Text>
|
<Text style={styles.helpTitle}>Troubleshooting</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.helpText}>
|
<Text style={styles.helpText}>
|
||||||
• Sensor not showing up? Make sure it's powered on and the LED is blinking{'\n'}
|
• Sensor not showing up? Make sure it's powered on and the LED is blinking{'\n'}
|
||||||
• Weak signal? Move closer to the sensor{'\n'}
|
• Weak signal? Move closer to the sensor{'\n'}
|
||||||
• Connection fails? Try restarting the sensor{'\n'}
|
• Connection fails? Try restarting the sensor{'\n'}
|
||||||
• Still having issues? Contact support for assistance
|
• Still having issues? Contact support for assistance
|
||||||
@ -364,6 +393,42 @@ const styles = StyleSheet.create({
|
|||||||
fontWeight: FontWeights.medium,
|
fontWeight: FontWeights.medium,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
|
permissionError: {
|
||||||
|
backgroundColor: AppColors.errorLight,
|
||||||
|
paddingVertical: Spacing.sm,
|
||||||
|
paddingHorizontal: Spacing.md,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: AppColors.error,
|
||||||
|
gap: Spacing.sm,
|
||||||
|
},
|
||||||
|
permissionErrorContent: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
gap: Spacing.xs,
|
||||||
|
},
|
||||||
|
permissionErrorText: {
|
||||||
|
fontSize: FontSizes.sm,
|
||||||
|
color: AppColors.error,
|
||||||
|
fontWeight: FontWeights.medium,
|
||||||
|
flex: 1,
|
||||||
|
lineHeight: 20,
|
||||||
|
},
|
||||||
|
settingsButton: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: AppColors.white,
|
||||||
|
paddingVertical: Spacing.xs,
|
||||||
|
paddingHorizontal: Spacing.md,
|
||||||
|
borderRadius: BorderRadius.sm,
|
||||||
|
gap: Spacing.xs,
|
||||||
|
alignSelf: 'flex-start',
|
||||||
|
},
|
||||||
|
settingsButtonText: {
|
||||||
|
fontSize: FontSizes.sm,
|
||||||
|
fontWeight: FontWeights.semibold,
|
||||||
|
color: AppColors.error,
|
||||||
|
},
|
||||||
content: {
|
content: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
// BLE Context - Global state for Bluetooth management
|
// BLE Context - Global state for Bluetooth management
|
||||||
|
|
||||||
import React, { createContext, useContext, useState, useCallback, useEffect, ReactNode } from 'react';
|
import React, { createContext, useContext, useState, useCallback, useEffect, ReactNode } from 'react';
|
||||||
import { bleManager, WPDevice, WiFiNetwork, WiFiStatus, isBLEAvailable } from '@/services/ble';
|
import { bleManager, WPDevice, WiFiNetwork, WiFiStatus, isBLEAvailable, checkBLEReadiness } from '@/services/ble';
|
||||||
import { setOnLogoutBLECleanupCallback } from '@/services/api';
|
import { setOnLogoutBLECleanupCallback } from '@/services/api';
|
||||||
|
import { BleManager } from 'react-native-ble-plx';
|
||||||
|
|
||||||
interface BLEContextType {
|
interface BLEContextType {
|
||||||
// State
|
// State
|
||||||
@ -11,6 +12,7 @@ interface BLEContextType {
|
|||||||
connectedDevices: Set<string>;
|
connectedDevices: Set<string>;
|
||||||
isBLEAvailable: boolean;
|
isBLEAvailable: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
|
permissionError: boolean; // true if error is related to permissions
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
scanDevices: () => Promise<void>;
|
scanDevices: () => Promise<void>;
|
||||||
@ -23,6 +25,7 @@ interface BLEContextType {
|
|||||||
rebootDevice: (deviceId: string) => Promise<void>;
|
rebootDevice: (deviceId: string) => Promise<void>;
|
||||||
cleanupBLE: () => Promise<void>;
|
cleanupBLE: () => Promise<void>;
|
||||||
clearError: () => void;
|
clearError: () => void;
|
||||||
|
checkPermissions: () => Promise<boolean>; // Manual permission check with UI prompts
|
||||||
}
|
}
|
||||||
|
|
||||||
const BLEContext = createContext<BLEContextType | undefined>(undefined);
|
const BLEContext = createContext<BLEContextType | undefined>(undefined);
|
||||||
@ -32,17 +35,51 @@ export function BLEProvider({ children }: { children: ReactNode }) {
|
|||||||
const [isScanning, setIsScanning] = useState(false);
|
const [isScanning, setIsScanning] = useState(false);
|
||||||
const [connectedDevices, setConnectedDevices] = useState<Set<string>>(new Set());
|
const [connectedDevices, setConnectedDevices] = useState<Set<string>>(new Set());
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [permissionError, setPermissionError] = useState(false);
|
||||||
|
|
||||||
|
// Lazy BleManager instance for permission checks
|
||||||
|
const [bleManagerInstance] = useState(() => new BleManager());
|
||||||
|
|
||||||
|
const isPermissionError = (errorMessage: string): boolean => {
|
||||||
|
const permissionKeywords = [
|
||||||
|
'permission',
|
||||||
|
'unauthorized',
|
||||||
|
'not granted',
|
||||||
|
'denied',
|
||||||
|
'bluetooth is disabled',
|
||||||
|
'enable it in settings',
|
||||||
|
];
|
||||||
|
const lowerMessage = errorMessage.toLowerCase();
|
||||||
|
return permissionKeywords.some(keyword => lowerMessage.includes(keyword));
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkPermissions = useCallback(async (): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
setError(null);
|
||||||
|
setPermissionError(false);
|
||||||
|
const ready = await checkBLEReadiness(bleManagerInstance);
|
||||||
|
return ready;
|
||||||
|
} catch {
|
||||||
|
const errorMsg = 'Failed to check Bluetooth permissions';
|
||||||
|
setError(errorMsg);
|
||||||
|
setPermissionError(true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, [bleManagerInstance]);
|
||||||
|
|
||||||
const scanDevices = useCallback(async () => {
|
const scanDevices = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setError(null);
|
setError(null);
|
||||||
|
setPermissionError(false);
|
||||||
setIsScanning(true);
|
setIsScanning(true);
|
||||||
const devices = await bleManager.scanDevices();
|
const devices = await bleManager.scanDevices();
|
||||||
// Sort by RSSI (strongest first)
|
// Sort by RSSI (strongest first)
|
||||||
const sorted = devices.sort((a, b) => b.rssi - a.rssi);
|
const sorted = devices.sort((a, b) => b.rssi - a.rssi);
|
||||||
setFoundDevices(sorted);
|
setFoundDevices(sorted);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.message || 'Failed to scan for devices');
|
const errorMsg = err.message || 'Failed to scan for devices';
|
||||||
|
setError(errorMsg);
|
||||||
|
setPermissionError(isPermissionError(errorMsg));
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
setIsScanning(false);
|
setIsScanning(false);
|
||||||
@ -57,16 +94,21 @@ export function BLEProvider({ children }: { children: ReactNode }) {
|
|||||||
const connectDevice = useCallback(async (deviceId: string): Promise<boolean> => {
|
const connectDevice = useCallback(async (deviceId: string): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
setError(null);
|
setError(null);
|
||||||
|
setPermissionError(false);
|
||||||
const success = await bleManager.connectDevice(deviceId);
|
const success = await bleManager.connectDevice(deviceId);
|
||||||
if (success) {
|
if (success) {
|
||||||
setConnectedDevices(prev => new Set(prev).add(deviceId));
|
setConnectedDevices(prev => new Set(prev).add(deviceId));
|
||||||
} else {
|
} else {
|
||||||
// Connection failed but no exception - set user-friendly error
|
// Connection failed but no exception - set user-friendly error
|
||||||
setError('Could not connect to sensor. Please move closer and try again.');
|
const errorMsg = 'Could not connect to sensor. Please move closer and try again.';
|
||||||
|
setError(errorMsg);
|
||||||
|
setPermissionError(false);
|
||||||
}
|
}
|
||||||
return success;
|
return success;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.message || 'Failed to connect to device');
|
const errorMsg = err.message || 'Failed to connect to device';
|
||||||
|
setError(errorMsg);
|
||||||
|
setPermissionError(isPermissionError(errorMsg));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
@ -139,6 +181,7 @@ export function BLEProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
const clearError = useCallback(() => {
|
const clearError = useCallback(() => {
|
||||||
setError(null);
|
setError(null);
|
||||||
|
setPermissionError(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const cleanupBLE = useCallback(async () => {
|
const cleanupBLE = useCallback(async () => {
|
||||||
@ -157,7 +200,7 @@ export function BLEProvider({ children }: { children: ReactNode }) {
|
|||||||
setConnectedDevices(new Set());
|
setConnectedDevices(new Set());
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
} catch (err: any) {
|
} catch {
|
||||||
// Don't throw - we want to allow logout to proceed even if BLE cleanup fails
|
// Don't throw - we want to allow logout to proceed even if BLE cleanup fails
|
||||||
}
|
}
|
||||||
}, [isScanning, stopScan]);
|
}, [isScanning, stopScan]);
|
||||||
@ -177,6 +220,7 @@ export function BLEProvider({ children }: { children: ReactNode }) {
|
|||||||
connectedDevices,
|
connectedDevices,
|
||||||
isBLEAvailable,
|
isBLEAvailable,
|
||||||
error,
|
error,
|
||||||
|
permissionError,
|
||||||
scanDevices,
|
scanDevices,
|
||||||
stopScan,
|
stopScan,
|
||||||
connectDevice,
|
connectDevice,
|
||||||
@ -187,6 +231,7 @@ export function BLEProvider({ children }: { children: ReactNode }) {
|
|||||||
rebootDevice,
|
rebootDevice,
|
||||||
cleanupBLE,
|
cleanupBLE,
|
||||||
clearError,
|
clearError,
|
||||||
|
checkPermissions,
|
||||||
};
|
};
|
||||||
|
|
||||||
return <BLEContext.Provider value={value}>{children}</BLEContext.Provider>;
|
return <BLEContext.Provider value={value}>{children}</BLEContext.Provider>;
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
// Real BLE Manager для физических устройств
|
// Real BLE Manager для физических устройств
|
||||||
|
|
||||||
import { BleManager, Device, State } from 'react-native-ble-plx';
|
import { BleManager, Device, State } from 'react-native-ble-plx';
|
||||||
import { PermissionsAndroid, Platform } from 'react-native';
|
import { Platform } from 'react-native';
|
||||||
import { IBLEManager, WPDevice, WiFiNetwork, WiFiStatus, BLE_CONFIG, BLE_COMMANDS } from './types';
|
import { IBLEManager, WPDevice, WiFiNetwork, WiFiStatus, BLE_CONFIG, BLE_COMMANDS } from './types';
|
||||||
|
import { requestBLEPermissions, checkBluetoothEnabled } from './permissions';
|
||||||
import base64 from 'react-native-base64';
|
import base64 from 'react-native-base64';
|
||||||
|
|
||||||
export class RealBLEManager implements IBLEManager {
|
export class RealBLEManager implements IBLEManager {
|
||||||
@ -22,52 +23,17 @@ export class RealBLEManager implements IBLEManager {
|
|||||||
// Don't initialize BleManager here - use lazy initialization
|
// Don't initialize BleManager here - use lazy initialization
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check and request permissions
|
|
||||||
private async requestPermissions(): Promise<boolean> {
|
|
||||||
if (Platform.OS === 'ios') {
|
|
||||||
// iOS handles permissions automatically via Info.plist
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Platform.OS === 'android') {
|
|
||||||
if (Platform.Version >= 31) {
|
|
||||||
// Android 12+
|
|
||||||
const granted = await PermissionsAndroid.requestMultiple([
|
|
||||||
PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN!,
|
|
||||||
PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT!,
|
|
||||||
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION!,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return Object.values(granted).every(
|
|
||||||
status => status === PermissionsAndroid.RESULTS.GRANTED
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Android < 12
|
|
||||||
const granted = await PermissionsAndroid.request(
|
|
||||||
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION!
|
|
||||||
);
|
|
||||||
return granted === PermissionsAndroid.RESULTS.GRANTED;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if Bluetooth is enabled
|
|
||||||
private async isBluetoothEnabled(): Promise<boolean> {
|
|
||||||
const state = await this.manager.state();
|
|
||||||
return state === State.PoweredOn;
|
|
||||||
}
|
|
||||||
|
|
||||||
async scanDevices(): Promise<WPDevice[]> {
|
async scanDevices(): Promise<WPDevice[]> {
|
||||||
const hasPermission = await this.requestPermissions();
|
// Check permissions with graceful fallback
|
||||||
if (!hasPermission) {
|
const permissionStatus = await requestBLEPermissions();
|
||||||
throw new Error('Bluetooth permissions not granted');
|
if (!permissionStatus.granted) {
|
||||||
|
throw new Error(permissionStatus.error || 'Bluetooth permissions not granted');
|
||||||
}
|
}
|
||||||
|
|
||||||
const isEnabled = await this.isBluetoothEnabled();
|
// Check Bluetooth state
|
||||||
if (!isEnabled) {
|
const bluetoothStatus = await checkBluetoothEnabled(this.manager);
|
||||||
throw new Error('Bluetooth is disabled. Please enable it in settings.');
|
if (!bluetoothStatus.enabled) {
|
||||||
|
throw new Error(bluetoothStatus.error || 'Bluetooth is disabled. Please enable it in settings.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const foundDevices = new Map<string, WPDevice>();
|
const foundDevices = new Map<string, WPDevice>();
|
||||||
@ -123,15 +89,15 @@ export class RealBLEManager implements IBLEManager {
|
|||||||
async connectDevice(deviceId: string): Promise<boolean> {
|
async connectDevice(deviceId: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
// Step 0: Check permissions (required for Android 12+)
|
// Step 0: Check permissions (required for Android 12+)
|
||||||
const hasPermission = await this.requestPermissions();
|
const permissionStatus = await requestBLEPermissions();
|
||||||
if (!hasPermission) {
|
if (!permissionStatus.granted) {
|
||||||
throw new Error('Bluetooth permissions not granted');
|
throw new Error(permissionStatus.error || 'Bluetooth permissions not granted');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 0.5: Check Bluetooth is enabled
|
// Step 0.5: Check Bluetooth is enabled
|
||||||
const isEnabled = await this.isBluetoothEnabled();
|
const bluetoothStatus = await checkBluetoothEnabled(this.manager);
|
||||||
if (!isEnabled) {
|
if (!bluetoothStatus.enabled) {
|
||||||
throw new Error('Bluetooth is disabled. Please enable it in settings.');
|
throw new Error(bluetoothStatus.error || 'Bluetooth is disabled. Please enable it in settings.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if already connected
|
// Check if already connected
|
||||||
|
|||||||
@ -40,3 +40,6 @@ export const bleManager: IBLEManager = {
|
|||||||
|
|
||||||
// Re-export types
|
// Re-export types
|
||||||
export * from './types';
|
export * from './types';
|
||||||
|
|
||||||
|
// Re-export permission utilities
|
||||||
|
export * from './permissions';
|
||||||
|
|||||||
249
services/ble/permissions.ts
Normal file
249
services/ble/permissions.ts
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
// BLE Permissions Helper
|
||||||
|
// Handles Bluetooth permission requests with graceful fallback
|
||||||
|
|
||||||
|
import { PermissionsAndroid, Platform, Linking, Alert } from 'react-native';
|
||||||
|
import { BleManager, State } from 'react-native-ble-plx';
|
||||||
|
|
||||||
|
export interface PermissionStatus {
|
||||||
|
granted: boolean;
|
||||||
|
canRequest: boolean; // false if user previously denied with "Don't ask again"
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BluetoothStatus {
|
||||||
|
enabled: boolean;
|
||||||
|
canEnable: boolean;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check and request BLE permissions based on platform
|
||||||
|
* Returns permission status with graceful fallback info
|
||||||
|
*/
|
||||||
|
export async function requestBLEPermissions(): Promise<PermissionStatus> {
|
||||||
|
if (Platform.OS === 'ios') {
|
||||||
|
// iOS handles permissions automatically via Info.plist
|
||||||
|
// BLE permission dialog shows on first BLE operation
|
||||||
|
return { granted: true, canRequest: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Platform.OS === 'android') {
|
||||||
|
try {
|
||||||
|
if (Platform.Version >= 31) {
|
||||||
|
// Android 12+ requires BLUETOOTH_SCAN and BLUETOOTH_CONNECT
|
||||||
|
const results = await PermissionsAndroid.requestMultiple([
|
||||||
|
PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN!,
|
||||||
|
PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT!,
|
||||||
|
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION!,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const allGranted = Object.values(results).every(
|
||||||
|
status => status === PermissionsAndroid.RESULTS.GRANTED
|
||||||
|
);
|
||||||
|
|
||||||
|
const anyNeverAskAgain = Object.values(results).some(
|
||||||
|
status => status === PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN
|
||||||
|
);
|
||||||
|
|
||||||
|
if (allGranted) {
|
||||||
|
return { granted: true, canRequest: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anyNeverAskAgain) {
|
||||||
|
return {
|
||||||
|
granted: false,
|
||||||
|
canRequest: false,
|
||||||
|
error: 'Bluetooth permissions were previously denied. Please enable them in Settings.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
granted: false,
|
||||||
|
canRequest: true,
|
||||||
|
error: 'Bluetooth permissions are required to scan for sensors.',
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Android < 12 requires ACCESS_FINE_LOCATION
|
||||||
|
const result = await PermissionsAndroid.request(
|
||||||
|
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION!
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result === PermissionsAndroid.RESULTS.GRANTED) {
|
||||||
|
return { granted: true, canRequest: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result === PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN) {
|
||||||
|
return {
|
||||||
|
granted: false,
|
||||||
|
canRequest: false,
|
||||||
|
error: 'Location permission was previously denied. Please enable it in Settings.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
granted: false,
|
||||||
|
canRequest: true,
|
||||||
|
error: 'Location permission is required to scan for Bluetooth devices.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
granted: false,
|
||||||
|
canRequest: false,
|
||||||
|
error: `Failed to request permissions: ${error.message}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown platform
|
||||||
|
return {
|
||||||
|
granted: false,
|
||||||
|
canRequest: false,
|
||||||
|
error: 'Bluetooth permissions not supported on this platform',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check Bluetooth state (enabled/disabled)
|
||||||
|
* Returns status with helpful error messages
|
||||||
|
*/
|
||||||
|
export async function checkBluetoothEnabled(manager: BleManager): Promise<BluetoothStatus> {
|
||||||
|
try {
|
||||||
|
const state = await manager.state();
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case State.PoweredOn:
|
||||||
|
return { enabled: true, canEnable: true };
|
||||||
|
|
||||||
|
case State.PoweredOff:
|
||||||
|
return {
|
||||||
|
enabled: false,
|
||||||
|
canEnable: true,
|
||||||
|
error: 'Bluetooth is turned off. Please enable it in your device settings.',
|
||||||
|
};
|
||||||
|
|
||||||
|
case State.Unauthorized:
|
||||||
|
return {
|
||||||
|
enabled: false,
|
||||||
|
canEnable: false,
|
||||||
|
error: 'Bluetooth access is not authorized. Please enable permissions in Settings.',
|
||||||
|
};
|
||||||
|
|
||||||
|
case State.Unsupported:
|
||||||
|
return {
|
||||||
|
enabled: false,
|
||||||
|
canEnable: false,
|
||||||
|
error: 'Bluetooth is not supported on this device.',
|
||||||
|
};
|
||||||
|
|
||||||
|
case State.Resetting:
|
||||||
|
return {
|
||||||
|
enabled: false,
|
||||||
|
canEnable: true,
|
||||||
|
error: 'Bluetooth is resetting. Please wait a moment and try again.',
|
||||||
|
};
|
||||||
|
|
||||||
|
case State.Unknown:
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
enabled: false,
|
||||||
|
canEnable: true,
|
||||||
|
error: 'Bluetooth state is unknown. Please check your device settings.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
enabled: false,
|
||||||
|
canEnable: false,
|
||||||
|
error: `Failed to check Bluetooth state: ${error.message}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a user-friendly alert for permission/Bluetooth issues
|
||||||
|
* Offers to open Settings if needed
|
||||||
|
*/
|
||||||
|
export function showPermissionAlert(
|
||||||
|
permissionStatus: PermissionStatus,
|
||||||
|
bluetoothStatus: BluetoothStatus
|
||||||
|
): void {
|
||||||
|
// Bluetooth disabled (can be fixed easily)
|
||||||
|
if (!bluetoothStatus.enabled && bluetoothStatus.canEnable) {
|
||||||
|
Alert.alert(
|
||||||
|
'Bluetooth Required',
|
||||||
|
bluetoothStatus.error || 'Please enable Bluetooth to scan for sensors.',
|
||||||
|
[
|
||||||
|
{ text: 'Cancel', style: 'cancel' },
|
||||||
|
{
|
||||||
|
text: 'Open Settings',
|
||||||
|
onPress: () => {
|
||||||
|
if (Platform.OS === 'ios') {
|
||||||
|
Linking.openURL('App-Prefs:Bluetooth');
|
||||||
|
} else {
|
||||||
|
Linking.sendIntent('android.settings.BLUETOOTH_SETTINGS');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permission denied with "Never ask again"
|
||||||
|
if (!permissionStatus.granted && !permissionStatus.canRequest) {
|
||||||
|
Alert.alert(
|
||||||
|
'Permissions Required',
|
||||||
|
permissionStatus.error || 'Bluetooth permissions are required to scan for sensors. Please enable them in Settings.',
|
||||||
|
[
|
||||||
|
{ text: 'Cancel', style: 'cancel' },
|
||||||
|
{
|
||||||
|
text: 'Open Settings',
|
||||||
|
onPress: () => {
|
||||||
|
Linking.openSettings();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permission denied (can retry)
|
||||||
|
if (!permissionStatus.granted) {
|
||||||
|
Alert.alert(
|
||||||
|
'Permissions Required',
|
||||||
|
permissionStatus.error || 'Bluetooth permissions are required to scan for sensors.',
|
||||||
|
[{ text: 'OK' }]
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bluetooth not supported or other unrecoverable error
|
||||||
|
if (!bluetoothStatus.canEnable) {
|
||||||
|
Alert.alert(
|
||||||
|
'Bluetooth Unavailable',
|
||||||
|
bluetoothStatus.error || 'Bluetooth is not available on this device.',
|
||||||
|
[{ text: 'OK' }]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Comprehensive pre-scan check
|
||||||
|
* Returns true if ready to scan, false otherwise (with user alert shown)
|
||||||
|
*/
|
||||||
|
export async function checkBLEReadiness(manager: BleManager): Promise<boolean> {
|
||||||
|
// Step 1: Check permissions
|
||||||
|
const permissionStatus = await requestBLEPermissions();
|
||||||
|
|
||||||
|
// Step 2: Check Bluetooth state
|
||||||
|
const bluetoothStatus = await checkBluetoothEnabled(manager);
|
||||||
|
|
||||||
|
// Step 3: If not ready, show appropriate alert
|
||||||
|
if (!permissionStatus.granted || !bluetoothStatus.enabled) {
|
||||||
|
showPermissionAlert(permissionStatus, bluetoothStatus);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user