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
text/typescript
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
};
}