UNPKG

react-native-healthkit-bridge

Version:

A comprehensive React Native bridge for Apple HealthKit with TypeScript support, advanced authorization, and flexible data queries

562 lines (475 loc) 17.6 kB
import { useState, useCallback } from 'react'; import { HealthKitBridge } from '../core/HealthKitBridge'; import { QuantityTypeIdentifier, CategoryTypeIdentifier, CharacteristicTypeIdentifier, AuthorizationStatus } from '../types/healthkit.types'; type HealthKitIdentifier = QuantityTypeIdentifier | CategoryTypeIdentifier | CharacteristicTypeIdentifier; interface AuthorizationResult { success: boolean; authorizedTypes: string[]; deniedTypes: string[]; notDeterminedTypes: string[]; message: string; } // Mapping of identifiers to friendly names const FRIENDLY_NAMES: Record<string, string> = { // Activity & Fitness 'HKQuantityTypeIdentifierStepCount': 'Steps', 'HKQuantityTypeIdentifierDistanceWalkingRunning': 'Distance Walking/Running', 'HKQuantityTypeIdentifierActiveEnergyBurned': 'Active Energy', 'HKQuantityTypeIdentifierAppleExerciseTime': 'Exercise Minutes', 'HKQuantityTypeIdentifierBasalEnergyBurned': 'Resting Energy', 'HKCategoryTypeIdentifierAppleStandHour': 'Stand Hour', // Heart & Cardiovascular 'HKQuantityTypeIdentifierHeartRate': 'Heart Rate', 'HKQuantityTypeIdentifierHeartRateVariabilitySDNN': 'Heart Rate Variability', 'HKQuantityTypeIdentifierRestingHeartRate': 'Resting Heart Rate', 'HKQuantityTypeIdentifierWalkingHeartRateAverage': 'Walking Heart Rate Average', 'HKQuantityTypeIdentifierVO2Max': 'VO2 Max', // Body Measurements 'HKQuantityTypeIdentifierBodyMass': 'Weight', 'HKQuantityTypeIdentifierHeight': 'Height', 'HKQuantityTypeIdentifierBodyFatPercentage': 'Body Fat Percentage', // Vitals & Health Metrics 'HKQuantityTypeIdentifierBloodPressureSystolic': 'Blood Pressure Systolic', 'HKQuantityTypeIdentifierBloodPressureDiastolic': 'Blood Pressure Diastolic', 'HKQuantityTypeIdentifierBloodGlucose': 'Blood Glucose', 'HKQuantityTypeIdentifierOxygenSaturation': 'Oxygen Saturation', 'HKQuantityTypeIdentifierRespiratoryRate': 'Respiratory Rate', // Sleep & Mindfulness 'HKCategoryTypeIdentifierSleepAnalysis': 'Sleep Analysis', 'HKCategoryTypeIdentifierMindfulSession': 'Mindful Session', // Reproductive Health 'HKCategoryTypeIdentifierMenstrualFlow': 'Menstrual Flow', 'HKCategoryTypeIdentifierSexualActivity': 'Sexual Activity' }; const getFriendlyName = (identifier: string): string => { return FRIENDLY_NAMES[identifier] || identifier; }; const formatDeniedTypes = (deniedTypes: string[]): string => { if (deniedTypes.length === 0) return ''; const friendlyNames = deniedTypes.map(getFriendlyName); if (friendlyNames.length === 1) { return friendlyNames[0]; } if (friendlyNames.length === 2) { return `${friendlyNames[0]} e ${friendlyNames[1]}`; } const last = friendlyNames.pop(); return `${friendlyNames.join(', ')} e ${last}`; }; export function useHealthKitAuthorization() { const [loading, setLoading] = useState(false); const [error, setError] = useState<string | null>(null); const [authorized, setAuthorized] = useState<boolean | null>(null); /** * 🔐 Check Authorization Status */ const checkAuthorizationStatus = useCallback(async ( types: HealthKitIdentifier[] ): Promise<AuthorizationStatus[]> => { if (!types || types.length === 0) { setError('No types provided for authorization check'); return []; } setLoading(true); setError(null); try { const bridge = new HealthKitBridge(); // Add timeout to prevent hanging const timeoutPromise = new Promise<never>((_, reject) => { setTimeout(() => reject(new Error('Authorization check timeout')), 10000); }); const statusPromise = bridge.getAuthorizationStatus(types); const status = await Promise.race([statusPromise, timeoutPromise]); const allAuthorized = status.every(item => item.status === 'sharingAuthorized'); setAuthorized(allAuthorized); return status; } catch (e: any) { const errorMessage = e?.message || 'Error checking authorization status'; setError(errorMessage); return []; } finally { setLoading(false); } }, []); /** * 🔍 Check Type Availability (NEW) */ const checkTypeAvailability = useCallback(async ( types: string[] ): Promise<Record<string, boolean>> => { if (!types || types.length === 0) { setError('No types provided for availability check'); return {}; } setLoading(true); setError(null); try { console.log('🔍 [Hook] checkTypeAvailability - Iniciando...'); console.log('🔍 [Hook] Tipos:', types); const bridge = new HealthKitBridge(); console.log('🔍 [Hook] Bridge criado'); const availability = await bridge.isTypeAvailable(types); console.log('🔍 [Hook] Resultado availability:', availability); return availability; } catch (e: any) { console.error('❌ [Hook] Erro em checkTypeAvailability:', e); const errorMessage = e?.message || 'Error checking type availability'; setError(errorMessage); return {}; } finally { setLoading(false); } }, []); /** * 📖 Check Read Permissions (NEW) */ const checkReadPermissions = useCallback(async ( types: string[] ): Promise<Record<string, boolean>> => { if (!types || types.length == 0) { setError('No types provided for read permission check'); return {}; } setLoading(true); setError(null); try { console.log('🔍 [Hook] checkReadPermissions - Iniciando...'); console.log('🔍 [Hook] Tipos:', types); const bridge = new HealthKitBridge(); console.log('🔍 [Hook] Bridge criado'); const canRead = await bridge.canReadType(types); console.log('🔍 [Hook] Resultado canRead:', canRead); return canRead; } catch (e: any) { console.error('❌ [Hook] Erro em checkReadPermissions:', e); const errorMessage = e?.message || 'Error checking read permissions'; setError(errorMessage); return {}; } finally { setLoading(false); } }, []); /** * ✏️ Check Write Permissions (NEW) */ const checkWritePermissions = useCallback(async ( types: string[] ): Promise<Record<string, boolean>> => { if (!types || types.length == 0) { setError('No types provided for write permission check'); return {}; } setLoading(true); setError(null); try { console.log('🔍 [Hook] checkWritePermissions - Iniciando...'); console.log('🔍 [Hook] Tipos:', types); const bridge = new HealthKitBridge(); console.log('🔍 [Hook] Bridge criado'); const canWrite = await bridge.canWriteType(types); console.log('🔍 [Hook] Resultado canWrite:', canWrite); return canWrite; } catch (e: any) { console.error('❌ [Hook] Erro em checkWritePermissions:', e); const errorMessage = e?.message || 'Error checking write permissions'; setError(errorMessage); return {}; } finally { setLoading(false); } }, []); /** * 📊 Get Detailed Authorization Status (NEW) */ const getDetailedAuthorizationStatus = useCallback(async ( types: string[] ): Promise<Array<{ identifier: string; status: string; canRead: boolean; canWrite: boolean; isAvailable: boolean; }>> => { if (!types || types.length == 0) { setError('No types provided for detailed status check'); return []; } setLoading(true); setError(null); try { console.log('🔍 [Hook] getDetailedAuthorizationStatus - Iniciando...'); console.log('🔍 [Hook] Tipos:', types); const bridge = new HealthKitBridge(); console.log('🔍 [Hook] Bridge criado'); const detailedStatus = await bridge.getDetailedAuthorizationStatus(types); console.log('🔍 [Hook] Resultado detailedStatus:', detailedStatus); return detailedStatus; } catch (e: any) { console.error('❌ [Hook] Erro em getDetailedAuthorizationStatus:', e); const errorMessage = e?.message || 'Error getting detailed authorization status'; setError(errorMessage); return []; } finally { setLoading(false); } }, []); /** * 🔐 Request Authorization */ const requestAuthorization = useCallback(async ( types: HealthKitIdentifier[] ): Promise<AuthorizationResult> => { if (!types || types.length === 0) { setError('No types provided for authorization request'); return { success: false, authorizedTypes: [], deniedTypes: [], notDeterminedTypes: [], message: 'No types provided for authorization request' }; } setLoading(true); setError(null); try { console.log('🔐 [Hook] requestAuthorization - Iniciando...'); console.log('🔐 [Hook] Tipos:', types); const bridge = new HealthKitBridge(); console.log('🔐 [Hook] Bridge criado'); // 1. Request authorization directly (without checking first) let authSuccess = false; try { console.log('🔐 [Hook] Tentando autorização com todos os tipos...'); authSuccess = await bridge.requestReadOnlyAuthorization(types as any); console.log('🔐 [Hook] Resultado autorização:', authSuccess); } catch (authError: any) { console.log('❌ [Hook] Erro na primeira tentativa:', authError.message); // 2. If first attempt fails, try with a smaller subset if (types.length > 2) { console.log('🔄 [Hook] Tentando com subconjunto de tipos...'); try { const subset = types.slice(0, 2); console.log('🔄 [Hook] Subconjunto:', subset); authSuccess = await bridge.requestReadOnlyAuthorization(subset as any); console.log('🔄 [Hook] Resultado subconjunto:', authSuccess); } catch (subsetError: any) { console.log('❌ [Hook] Erro no subconjunto:', subsetError.message); throw subsetError; } } else { throw authError; } } // 3. Check final authorization status const finalStatus = await bridge.getAuthorizationStatus(types); console.log('🔍 [Hook] Status final:', finalStatus); const authorizedTypes: string[] = []; const deniedTypes: string[] = []; const notDeterminedTypes: string[] = []; finalStatus.forEach(item => { const friendlyName = getFriendlyName(item.identifier); switch (item.status) { case 'sharingAuthorized': authorizedTypes.push(friendlyName); break; case 'sharingDenied': deniedTypes.push(friendlyName); break; case 'notDetermined': notDeterminedTypes.push(friendlyName); break; } }); const success = authorizedTypes.length > 0; const message = success ? `Autorização bem-sucedida para: ${authorizedTypes.join(', ')}` : `Falha na autorização. Negado: ${formatDeniedTypes(deniedTypes)}`; setAuthorized(success); return { success, authorizedTypes, deniedTypes, notDeterminedTypes, message }; } catch (e: any) { console.error('❌ [Hook] Erro em requestAuthorization:', e); const errorMessage = e?.message || 'Error requesting authorization'; setError(errorMessage); setAuthorized(false); return { success: false, authorizedTypes: [], deniedTypes: [], notDeterminedTypes: [], message: errorMessage }; } finally { setLoading(false); } }, []); /** * 📖 Request Read-Only Authorization */ const requestReadOnlyAuthorization = useCallback(async ( types: HealthKitIdentifier[] ): Promise<AuthorizationResult> => { if (!types || types.length === 0) { setError('No types provided for read-only authorization request'); return { success: false, authorizedTypes: [], deniedTypes: [], notDeterminedTypes: [], message: 'No types provided for read-only authorization request' }; } setLoading(true); setError(null); try { console.log('📖 [Hook] requestReadOnlyAuthorization - Iniciando...'); console.log('📖 [Hook] Tipos:', types); const bridge = new HealthKitBridge(); console.log('📖 [Hook] Bridge criado'); const authSuccess = await bridge.requestReadOnlyAuthorization(types as any); console.log('📖 [Hook] Resultado autorização de leitura:', authSuccess); // Check final authorization status const finalStatus = await bridge.getAuthorizationStatus(types); console.log('📖 [Hook] Status final:', finalStatus); const authorizedTypes: string[] = []; const deniedTypes: string[] = []; const notDeterminedTypes: string[] = []; finalStatus.forEach(item => { const friendlyName = getFriendlyName(item.identifier); switch (item.status) { case 'sharingAuthorized': authorizedTypes.push(friendlyName); break; case 'sharingDenied': deniedTypes.push(friendlyName); break; case 'notDetermined': notDeterminedTypes.push(friendlyName); break; } }); const success = authorizedTypes.length > 0; const message = success ? `Autorização de leitura bem-sucedida para: ${authorizedTypes.join(', ')}` : `Falha na autorização de leitura. Negado: ${formatDeniedTypes(deniedTypes)}`; setAuthorized(success); return { success, authorizedTypes, deniedTypes, notDeterminedTypes, message }; } catch (e: any) { console.error('❌ [Hook] Erro em requestReadOnlyAuthorization:', e); const errorMessage = e?.message || 'Error requesting read-only authorization'; setError(errorMessage); setAuthorized(false); return { success: false, authorizedTypes: [], deniedTypes: [], notDeterminedTypes: [], message: errorMessage }; } finally { setLoading(false); } }, []); /** * ✏️ Request Write-Only Authorization */ const requestWriteOnlyAuthorization = useCallback(async ( types: HealthKitIdentifier[] ): Promise<AuthorizationResult> => { if (!types || types.length === 0) { setError('No types provided for write-only authorization request'); return { success: false, authorizedTypes: [], deniedTypes: [], notDeterminedTypes: [], message: 'No types provided for write-only authorization request' }; } setLoading(true); setError(null); try { console.log('✏️ [Hook] requestWriteOnlyAuthorization - Iniciando...'); console.log('✏️ [Hook] Tipos:', types); const bridge = new HealthKitBridge(); console.log('✏️ [Hook] Bridge criado'); const authSuccess = await bridge.requestWriteOnlyAuthorization(types as any); console.log('✏️ [Hook] Resultado autorização de escrita:', authSuccess); // Check final authorization status const finalStatus = await bridge.getAuthorizationStatus(types); console.log('✏️ [Hook] Status final:', finalStatus); const authorizedTypes: string[] = []; const deniedTypes: string[] = []; const notDeterminedTypes: string[] = []; finalStatus.forEach(item => { const friendlyName = getFriendlyName(item.identifier); switch (item.status) { case 'sharingAuthorized': authorizedTypes.push(friendlyName); break; case 'sharingDenied': deniedTypes.push(friendlyName); break; case 'notDetermined': notDeterminedTypes.push(friendlyName); break; } }); const success = authorizedTypes.length > 0; const message = success ? `Autorização de escrita bem-sucedida para: ${authorizedTypes.join(', ')}` : `Falha na autorização de escrita. Negado: ${formatDeniedTypes(deniedTypes)}`; setAuthorized(success); return { success, authorizedTypes, deniedTypes, notDeterminedTypes, message }; } catch (e: any) { console.error('❌ [Hook] Erro em requestWriteOnlyAuthorization:', e); const errorMessage = e?.message || 'Error requesting write-only authorization'; setError(errorMessage); setAuthorized(false); return { success: false, authorizedTypes: [], deniedTypes: [], notDeterminedTypes: [], message: errorMessage }; } finally { setLoading(false); } }, []); return { loading, error, authorized, checkAuthorizationStatus, requestAuthorization, checkTypeAvailability, checkReadPermissions, checkWritePermissions, getDetailedAuthorizationStatus, requestReadOnlyAuthorization, requestWriteOnlyAuthorization }; }