- 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>
250 lines
6.9 KiB
TypeScript
250 lines
6.9 KiB
TypeScript
// 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;
|
|
}
|