UNPKG

react-native-healthkit-bridge

Version:

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

491 lines (490 loc) 20.3 kB
import { ProviderFactory } from './providers/ProviderFactory'; import { withMetrics } from '../utils/Metrics'; import { HealthKitCache } from '../utils/Cache'; import { withRetry } from '../utils/Retry'; export class HealthKitBridge { constructor(config) { this.factory = ProviderFactory.getInstance(); this.cache = HealthKitCache.getInstance(); // Check if native module is available if (!this.factory.isNativeAvailable()) { // Create mock provider to prevent app crash this.provider = this.createMockProvider(); return; } this.provider = this.factory.createProvider(config); } createMockProvider() { return { // Authorization methods requestAuthorization: async () => false, requestSelectiveAuthorization: async () => false, requestReadOnlyAuthorization: async () => false, requestWriteOnlyAuthorization: async () => false, getAuthorizationStatus: async () => [], diagnoseAuthorizationRequirements: async () => [], // New authorization methods isTypeAvailable: async () => ({}), canReadType: async () => ({}), canWriteType: async () => ({}), getDetailedAuthorizationStatus: async () => [], // Type methods getAvailableTypes: async () => [], getTypeInfo: async () => ({ identifier: '', name: 'Not Available', description: 'Native module not available', category: 'other', type: 'quantity' }), // Day-based methods getQuantitySamplesForDays: async () => [], getQuantitySamplesForHours: async () => [], getQuantitySamplesForLastDayWithData: async () => [], getCategorySamplesForDays: async () => [], getWorkoutsForDays: async () => [], getAudiogramsForDays: async () => [], getECGForDays: async () => [], // Range methods getQuantitySamplesWithRange: async () => [], getCategorySamplesWithRange: async () => [], getWorkoutsWithRange: async () => [], getAudiogramsWithRange: async () => [], getECGWithRange: async () => [], // Generic methods getQuantitySamples: async () => [], getCategorySamples: async () => [], getWorkoutsGeneric: async () => [], getAudiograms: async () => [], getECG: async () => [], // Advanced methods queryQuantitySamples: async () => ({ data: [], hasMore: false, count: 0 }), queryCategorySamples: async () => ({ data: [], hasMore: false, count: 0 }), queryWorkouts: async () => ({ data: [], hasMore: false, count: 0 }), // Utilities isAvailable: () => false, getProviderName: () => 'MockProvider (Native module not available)', getProviderVersion: () => '1.0.0' }; } // Authorization methods with specific types async checkAvailability() { return this.provider.isAvailable(); } /** * 1. Full authorization for reading and writing all data * Requests permission to read and write all available data types */ async requestFullAuthorization() { return this.provider.requestAuthorization(); } /** * 2. Read authorization showing all types for user selection * Returns all available types for user to choose which to authorize */ async getAvailableTypesForAuthorization() { return this.provider.getAvailableTypes(); } /** * 3. Read authorization where we pass the data types we want to request authorization for * Requests permission only for reading specific types */ async requestReadAuthorization(readTypes) { return this.provider.requestSelectiveAuthorization(readTypes); } /** * 4. Write authorization where we pass the data types we want to request authorization for * Requests permission only for writing specific types */ async requestWriteAuthorization(writeTypes) { return this.provider.requestSelectiveAuthorization([], writeTypes); } /** * 5. Selective authorization (read and write) - original method maintained for compatibility * Requests permission for specific types */ async requestSelectiveAuthorization(readTypes, writeTypes = []) { return this.provider.requestSelectiveAuthorization(readTypes, writeTypes); } async requestReadOnlyAuthorization(readTypes) { return this.provider.requestReadOnlyAuthorization(readTypes); } async requestWriteOnlyAuthorization(writeTypes) { return this.provider.requestWriteOnlyAuthorization(writeTypes); } async diagnoseAuthorizationRequirements(types) { return this.provider.diagnoseAuthorizationRequirements(types); } /** * 6. Legacy method maintained for compatibility */ async requestAuthorization() { return this.requestFullAuthorization(); } async getAuthorizationStatus(identifiers) { const cacheKey = HealthKitCache.createAuthKey(identifiers); // Check cache first const cached = this.cache.get(cacheKey); if (cached) { return cached; } // Execute with retry and metrics const result = await withMetrics('getAuthorizationStatus', () => withRetry(() => this.provider.getAuthorizationStatus(identifiers), { retryableErrors: ['ERR_HEALTHKIT_UNAVAILABLE', 'ERR_TIMEOUT'] })); if (result.success && result.data) { // Cache successful results for 5 minutes this.cache.set(cacheKey, result.data, 5 * 60 * 1000); return result.data; } throw result.error || new Error('Failed to get authorization status'); } /** * 🚀 NEW FEATURE: Smart Authorization * Checks which types are not authorized and requests only the necessary ones */ async requestSmartAuthorization(requiredTypes) { try { // 1. Check current status const currentStatus = await this.getAuthorizationStatus(requiredTypes); // 2. Separate types by status const authorizedTypes = currentStatus .filter(item => item.status === 'sharingAuthorized') .map(item => item.identifier); const deniedTypes = currentStatus .filter(item => item.status === 'sharingDenied') .map(item => item.identifier); const notDeterminedTypes = currentStatus .filter(item => item.status === 'notDetermined' || item.status === 'unknown') .map(item => item.identifier); // 3. If all are already authorized, return success if (notDeterminedTypes.length === 0 && deniedTypes.length === 0) { return { success: true, authorizedTypes, deniedTypes, notDeterminedTypes, message: 'All types are already authorized' }; } // 4. If there are denied types, we cannot request again if (deniedTypes.length > 0) { return { success: false, authorizedTypes, deniedTypes, notDeterminedTypes, message: `Some types were denied: ${deniedTypes.join(', ')}. Go to Settings > Privacy > Health to authorize manually.` }; } // 5. Request authorization only for undetermined types if (notDeterminedTypes.length > 0) { const authSuccess = await this.requestSelectiveAuthorization(notDeterminedTypes, []); if (authSuccess) { // 6. Check again after authorization const newStatus = await this.getAuthorizationStatus(notDeterminedTypes); const newlyAuthorized = newStatus .filter(item => item.status === 'sharingAuthorized') .map(item => item.identifier); return { success: true, authorizedTypes: [...authorizedTypes, ...newlyAuthorized], deniedTypes, notDeterminedTypes: notDeterminedTypes.filter(type => !newlyAuthorized.includes(type)), message: `Authorization requested successfully for: ${newlyAuthorized.join(', ')}` }; } else { return { success: false, authorizedTypes, deniedTypes, notDeterminedTypes, message: 'Failed to request authorization' }; } } return { success: false, authorizedTypes, deniedTypes, notDeterminedTypes, message: 'No types can be automatically authorized' }; } catch (error) { return { success: false, authorizedTypes: [], deniedTypes: [], notDeterminedTypes: requiredTypes, message: `Error checking authorization: ${error.message}` }; } } /** * 🚀 NEW FEATURE: Check and Request Authorization for Specific Data * Checks if a specific type is authorized and requests if necessary */ async ensureDataTypeAuthorization(dataType) { var _a; try { // 1. Check current status const status = await this.getAuthorizationStatus([dataType]); const currentStatus = status[0]; if (!currentStatus) { return { isAuthorized: false, wasRequested: false, message: 'Data type not found' }; } // 2. If already authorized, return success if (currentStatus.status === 'sharingAuthorized') { return { isAuthorized: true, wasRequested: false, message: 'Data already authorized' }; } // 3. If denied, we cannot request again if (currentStatus.status === 'sharingDenied') { return { isAuthorized: false, wasRequested: false, message: 'Access denied. Go to Settings > Privacy > Health to authorize manually.' }; } // 4. Request authorization const authSuccess = await this.requestSelectiveAuthorization([dataType], []); if (authSuccess) { // 5. Check again const newStatus = await this.getAuthorizationStatus([dataType]); const isNowAuthorized = ((_a = newStatus[0]) === null || _a === void 0 ? void 0 : _a.status) === 'sharingAuthorized'; return { isAuthorized: isNowAuthorized, wasRequested: true, message: isNowAuthorized ? 'Authorization granted successfully' : 'Authorization requested, but not yet granted' }; } else { return { isAuthorized: false, wasRequested: true, message: 'Failed to request authorization' }; } } catch (error) { return { isAuthorized: false, wasRequested: false, message: `Error: ${error.message}` }; } } // Type methods with specific types async getAvailableTypes() { return this.provider.getAvailableTypes(); } async getTypeInfo(identifier) { return this.provider.getTypeInfo(identifier); } // Day-based methods with specific types async getQuantitySamplesForDays(identifier, unit, days) { return this.provider.getQuantitySamplesForDays(identifier, unit, days); } /** * 📊 Get quantity samples by hours * Fetches data from the last X hours */ async getQuantitySamplesForHours(identifier, unit, hours) { return this.provider.getQuantitySamplesForHours(identifier, unit, hours); } /** * 📅 Get samples from the last day with data * Finds the last day that has records and returns all data from that day */ async getQuantitySamplesForLastDayWithData(identifier, unit) { return this.provider.getQuantitySamplesForLastDayWithData(identifier, unit); } async getCategorySamplesForDays(identifier, days) { const data = await this.provider.getCategorySamplesForDays(identifier, days); return data.map((item) => ({ value: item.value, startDate: new Date(item.startDate).toISOString(), endDate: new Date(item.endDate).toISOString() })); } async getWorkoutsForDays(days) { const data = await this.provider.getWorkoutsForDays(days); return data.map((item) => ({ workoutActivityType: item.activityType, duration: item.duration, startDate: new Date(item.startDate).toISOString(), endDate: new Date(item.endDate).toISOString() })); } // Range methods with specific types async getQuantitySamplesForRange(identifier, unit, startDate, endDate) { const startTimestamp = new Date(startDate).getTime() / 1000; const endTimestamp = new Date(endDate).getTime() / 1000; const data = await this.provider.getQuantitySamplesWithRange(identifier, unit, startTimestamp, endTimestamp); return data.map((value, index) => ({ value, startDate: new Date(startTimestamp * 1000 + (index * (endTimestamp - startTimestamp) * 1000 / data.length)).toISOString(), endDate: new Date(startTimestamp * 1000 + ((index + 1) * (endTimestamp - startTimestamp) * 1000 / data.length)).toISOString() })); } async getCategorySamplesForRange(identifier, startDate, endDate) { const startTimestamp = new Date(startDate).getTime() / 1000; const endTimestamp = new Date(endDate).getTime() / 1000; const data = await this.provider.getCategorySamplesWithRange(identifier, startTimestamp, endTimestamp); return data.map((item) => ({ value: item.value, startDate: new Date(item.startDate).toISOString(), endDate: new Date(item.endDate).toISOString() })); } // Legacy methods maintained for compatibility async getQuantitySamplesWithRange(identifier, unit, startDate, endDate) { return this.provider.getQuantitySamplesWithRange(identifier, unit, startDate, endDate); } async getCategorySamplesWithRange(identifier, startDate, endDate) { return this.provider.getCategorySamplesWithRange(identifier, startDate, endDate); } async getWorkoutsWithRange(startDate, endDate) { return this.provider.getWorkoutsWithRange(startDate, endDate); } async getAudiogramsWithRange(startDate, endDate) { return this.provider.getAudiogramsWithRange(startDate, endDate); } async getECGWithRange(startDate, endDate) { return this.provider.getECGWithRange(startDate, endDate); } async getAudiogramsForDays(days) { return this.provider.getAudiogramsForDays(days); } async getECGForDays(days) { return this.provider.getECGForDays(days); } async getQuantitySamples(identifier, unit) { return this.provider.getQuantitySamples(identifier, unit); } async getCategorySamples(identifier) { return this.provider.getCategorySamples(identifier); } async getWorkoutsGeneric() { return this.provider.getWorkoutsGeneric(); } async getAudiograms() { return this.provider.getAudiograms(); } async getECG() { return this.provider.getECG(); } // Advanced methods async queryQuantitySamples(identifier, unit, options) { return this.provider.queryQuantitySamples(identifier, unit, options); } async queryCategorySamples(identifier, options) { return this.provider.queryCategorySamples(identifier, options); } async queryWorkouts(options) { return this.provider.queryWorkouts(options); } // Utilities isAvailable() { return this.provider.isAvailable(); } getProviderName() { return this.provider.getProviderName(); } getProviderVersion() { return this.provider.getProviderVersion(); } // Configuration methods getAvailableProviders() { return this.factory.getAvailableProviders(); } getProviderInfo() { return { name: this.getProviderName(), version: this.getProviderVersion(), available: this.isAvailable() }; } getAllProvidersInfo() { return this.factory.getAllProvidersInfo(); } /** * 🔍 Check if specific types are available */ async isTypeAvailable(identifiers) { try { console.log('🔍 [Bridge] isTypeAvailable - Iniciando...'); console.log('🔍 [Bridge] Tipos:', identifiers); // Usar o provider diretamente ao invés de criar nova instância const results = await this.provider.isTypeAvailable(identifiers); console.log('🔍 [Bridge] Resultado isTypeAvailable:', results); return results; } catch (e) { console.error('❌ [Bridge] Erro em isTypeAvailable:', e); return {}; } } /** * 📖 Check if can read specific types */ async canReadType(identifiers) { try { console.log('🔍 [Bridge] canReadType - Iniciando...'); console.log('🔍 [Bridge] Tipos:', identifiers); // Usar o provider diretamente ao invés de criar nova instância const results = await this.provider.canReadType(identifiers); console.log('🔍 [Bridge] Resultado canReadType:', results); return results; } catch (e) { console.error('❌ [Bridge] Erro em canReadType:', e); return {}; } } /** * ✏️ Check if can write specific types */ async canWriteType(identifiers) { try { console.log('🔍 [Bridge] canWriteType - Iniciando...'); console.log('🔍 [Bridge] Tipos:', identifiers); // Usar o provider diretamente ao invés de criar nova instância const results = await this.provider.canWriteType(identifiers); console.log('🔍 [Bridge] Resultado canWriteType:', results); return results; } catch (e) { console.error('❌ [Bridge] Erro em canWriteType:', e); return {}; } } /** * 🔍 Get detailed authorization status for specific types */ async getDetailedAuthorizationStatus(identifiers) { try { console.log('🔍 [Bridge] getDetailedAuthorizationStatus - Iniciando...'); console.log('🔍 [Bridge] Tipos:', identifiers); // Usar o provider diretamente ao invés de criar nova instância const status = await this.provider.getDetailedAuthorizationStatus(identifiers); console.log('🔍 [Bridge] Resultado getDetailedAuthorizationStatus:', status); return status; } catch (e) { console.error('❌ [Bridge] Erro em getDetailedAuthorizationStatus:', e); throw e; } } }