WellNuo/services/ble/permissions.ts
Sergei 5d40da0409 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>
2026-01-31 15:23:06 -08:00

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;
}