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