react-native-healthkit-bridge
Version:
A comprehensive React Native bridge for Apple HealthKit with TypeScript support, advanced authorization, and flexible data queries
646 lines (562 loc) • 21.8 kB
text/typescript
import { HealthKitProvider, HealthKitProviderConfig } from './types/ProviderTypes';
import { ProviderFactory } from './providers/ProviderFactory';
import { withMetrics } from '../utils/Metrics';
import { HealthKitCache } from '../utils/Cache';
import { withRetry } from '../utils/Retry';
import {
QuantityTypeIdentifier,
CategoryTypeIdentifier,
CharacteristicTypeIdentifier,
HealthKitSample,
HealthKitWorkout,
AuthorizationStatus,
HealthKitTypeInfo,
HealthKitBridgeAPICompat
} from '../types/healthkit.types';
export class HealthKitBridge implements HealthKitBridgeAPICompat {
private provider: HealthKitProvider;
private factory: ProviderFactory;
private cache: HealthKitCache;
constructor(config?: HealthKitProviderConfig) {
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);
}
private createMockProvider(): HealthKitProvider {
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(): Promise<boolean> {
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(): Promise<boolean> {
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(): Promise<HealthKitTypeInfo[]> {
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: Array<QuantityTypeIdentifier | CategoryTypeIdentifier | CharacteristicTypeIdentifier>
): Promise<boolean> {
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: Array<QuantityTypeIdentifier | CategoryTypeIdentifier>
): Promise<boolean> {
return this.provider.requestSelectiveAuthorization([], writeTypes);
}
/**
* 5. Selective authorization (read and write) - original method maintained for compatibility
* Requests permission for specific types
*/
async requestSelectiveAuthorization(
readTypes: Array<QuantityTypeIdentifier | CategoryTypeIdentifier | CharacteristicTypeIdentifier>,
writeTypes: Array<QuantityTypeIdentifier | CategoryTypeIdentifier> = []
): Promise<boolean> {
return this.provider.requestSelectiveAuthorization(readTypes, writeTypes);
}
async requestReadOnlyAuthorization(
readTypes: Array<QuantityTypeIdentifier | CategoryTypeIdentifier | CharacteristicTypeIdentifier>
): Promise<boolean> {
return this.provider.requestReadOnlyAuthorization(readTypes);
}
async requestWriteOnlyAuthorization(
writeTypes: Array<QuantityTypeIdentifier | CategoryTypeIdentifier | CharacteristicTypeIdentifier>
): Promise<boolean> {
return this.provider.requestWriteOnlyAuthorization(writeTypes);
}
async diagnoseAuthorizationRequirements(
types: Array<QuantityTypeIdentifier | CategoryTypeIdentifier | CharacteristicTypeIdentifier>
): Promise<Array<{
identifier: string;
readStatus: number;
writeStatus: number;
requiresWriteForRead: boolean;
}>> {
return this.provider.diagnoseAuthorizationRequirements(types);
}
/**
* 6. Legacy method maintained for compatibility
*/
async requestAuthorization(): Promise<boolean> {
return this.requestFullAuthorization();
}
async getAuthorizationStatus(
identifiers: Array<QuantityTypeIdentifier | CategoryTypeIdentifier | CharacteristicTypeIdentifier>
): Promise<AuthorizationStatus[]> {
const cacheKey = HealthKitCache.createAuthKey(identifiers);
// Check cache first
const cached = this.cache.get<AuthorizationStatus[]>(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: Array<QuantityTypeIdentifier | CategoryTypeIdentifier | CharacteristicTypeIdentifier>
): Promise<{
success: boolean;
authorizedTypes: string[];
deniedTypes: string[];
notDeterminedTypes: string[];
message: string;
}> {
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 as any, []);
if (authSuccess) {
// 6. Check again after authorization
const newStatus = await this.getAuthorizationStatus(notDeterminedTypes as any);
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: any) {
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: QuantityTypeIdentifier | CategoryTypeIdentifier | CharacteristicTypeIdentifier
): Promise<{
isAuthorized: boolean;
wasRequested: boolean;
message: string;
}> {
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 = newStatus[0]?.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: any) {
return {
isAuthorized: false,
wasRequested: false,
message: `Error: ${error.message}`
};
}
}
// Type methods with specific types
async getAvailableTypes(): Promise<HealthKitTypeInfo[]> {
return this.provider.getAvailableTypes();
}
async getTypeInfo(
identifier: QuantityTypeIdentifier | CategoryTypeIdentifier | CharacteristicTypeIdentifier
): Promise<HealthKitTypeInfo> {
return this.provider.getTypeInfo(identifier);
}
// Day-based methods with specific types
async getQuantitySamplesForDays<T extends QuantityTypeIdentifier>(
identifier: T,
unit: string,
days: number
): Promise<HealthKitSample[]> {
return this.provider.getQuantitySamplesForDays(identifier, unit, days);
}
/**
* 📊 Get quantity samples by hours
* Fetches data from the last X hours
*/
async getQuantitySamplesForHours<T extends QuantityTypeIdentifier>(
identifier: T,
unit: string,
hours: number
): Promise<HealthKitSample[]> {
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<T extends QuantityTypeIdentifier>(
identifier: T,
unit: string
): Promise<HealthKitSample[]> {
return this.provider.getQuantitySamplesForLastDayWithData(identifier, unit);
}
async getCategorySamplesForDays(
identifier: CategoryTypeIdentifier,
days: number
): Promise<HealthKitSample[]> {
const data = await this.provider.getCategorySamplesForDays(identifier, days);
return data.map((item: any) => ({
value: item.value,
startDate: new Date(item.startDate).toISOString(),
endDate: new Date(item.endDate).toISOString()
}));
}
async getWorkoutsForDays(days: number): Promise<HealthKitWorkout[]> {
const data = await this.provider.getWorkoutsForDays(days);
return data.map((item: any) => ({
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<T extends QuantityTypeIdentifier>(
identifier: T,
unit: string,
startDate: string,
endDate: string
): Promise<HealthKitSample[]> {
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: number, index: number) => ({
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: CategoryTypeIdentifier,
startDate: string,
endDate: string
): Promise<HealthKitSample[]> {
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: any) => ({
value: item.value,
startDate: new Date(item.startDate).toISOString(),
endDate: new Date(item.endDate).toISOString()
}));
}
// Legacy methods maintained for compatibility
async getQuantitySamplesWithRange(identifier: string, unit: string, startDate: number, endDate: number): Promise<number[]> {
return this.provider.getQuantitySamplesWithRange(identifier, unit, startDate, endDate);
}
async getCategorySamplesWithRange(identifier: string, startDate: number, endDate: number): Promise<any[]> {
return this.provider.getCategorySamplesWithRange(identifier, startDate, endDate);
}
async getWorkoutsWithRange(startDate: number, endDate: number): Promise<any[]> {
return this.provider.getWorkoutsWithRange(startDate, endDate);
}
async getAudiogramsWithRange(startDate: number, endDate: number): Promise<any[]> {
return this.provider.getAudiogramsWithRange(startDate, endDate);
}
async getECGWithRange(startDate: number, endDate: number): Promise<any[]> {
return this.provider.getECGWithRange(startDate, endDate);
}
async getAudiogramsForDays(days: number): Promise<any[]> {
return this.provider.getAudiogramsForDays(days);
}
async getECGForDays(days: number): Promise<any[]> {
return this.provider.getECGForDays(days);
}
async getQuantitySamples(identifier: string, unit: string): Promise<HealthKitSample[]> {
return this.provider.getQuantitySamples(identifier, unit);
}
async getCategorySamples(identifier: string): Promise<any[]> {
return this.provider.getCategorySamples(identifier);
}
async getWorkoutsGeneric(): Promise<any[]> {
return this.provider.getWorkoutsGeneric();
}
async getAudiograms(): Promise<any[]> {
return this.provider.getAudiograms();
}
async getECG(): Promise<any[]> {
return this.provider.getECG();
}
// Advanced methods
async queryQuantitySamples(identifier: string, unit: string, options: any): Promise<any> {
return this.provider.queryQuantitySamples(identifier, unit, options);
}
async queryCategorySamples(identifier: string, options: any): Promise<any> {
return this.provider.queryCategorySamples(identifier, options);
}
async queryWorkouts(options: any): Promise<any> {
return this.provider.queryWorkouts(options);
}
// Utilities
isAvailable(): boolean {
return this.provider.isAvailable();
}
getProviderName(): string {
return this.provider.getProviderName();
}
getProviderVersion(): string {
return this.provider.getProviderVersion();
}
// Configuration methods
getAvailableProviders(): string[] {
return this.factory.getAvailableProviders();
}
getProviderInfo(): { name: string; version: string; available: boolean } {
return {
name: this.getProviderName(),
version: this.getProviderVersion(),
available: this.isAvailable()
};
}
getAllProvidersInfo(): Array<{ type: string; name: string; version: string; available: boolean }> {
return this.factory.getAllProvidersInfo();
}
/**
* 🔍 Check if specific types are available
*/
async isTypeAvailable(identifiers: string[]): Promise<Record<string, boolean>> {
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: any) {
console.error('❌ [Bridge] Erro em isTypeAvailable:', e);
return {};
}
}
/**
* 📖 Check if can read specific types
*/
async canReadType(identifiers: string[]): Promise<Record<string, boolean>> {
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: any) {
console.error('❌ [Bridge] Erro em canReadType:', e);
return {};
}
}
/**
* ✏️ Check if can write specific types
*/
async canWriteType(identifiers: string[]): Promise<Record<string, boolean>> {
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: any) {
console.error('❌ [Bridge] Erro em canWriteType:', e);
return {};
}
}
/**
* 🔍 Get detailed authorization status for specific types
*/
async getDetailedAuthorizationStatus(identifiers: string[]): Promise<Array<{
identifier: string;
status: string;
canRead: boolean;
canWrite: boolean;
isAvailable: boolean;
}>> {
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: any) {
console.error('❌ [Bridge] Erro em getDetailedAuthorizationStatus:', e);
throw e;
}
}
}