react-native-healthkit-bridge
Version:
A comprehensive React Native bridge for Apple HealthKit with TypeScript support, advanced authorization, and flexible data queries
391 lines (327 loc) • 17 kB
text/typescript
import { NativeModules } from 'react-native';
import { HealthKitProvider } from '../types/ProviderTypes';
import { HealthKitValidator } from '../../utils/ValidationUtils';
import { HealthKitErrorHandler } from '../../utils/ErrorHandler';
import {
QuantityTypeIdentifier,
CategoryTypeIdentifier,
CharacteristicTypeIdentifier,
HealthKitSample,
HealthKitWorkout,
AuthorizationStatus,
HealthKitTypeInfo
} from '../../types/healthkit.types';
type HealthKitIdentifier = QuantityTypeIdentifier | CategoryTypeIdentifier | CharacteristicTypeIdentifier;
const LINKING_ERROR = 'The native HealthKitBridge module was not linked correctly.';
export class NativeProvider implements HealthKitProvider {
private nativeModule: any;
private config: any; // HealthKitProviderConfig; // This line was removed as per the new_code, but the original file had it. I will keep it as per instruction 5.
constructor(config: any = {}) { // HealthKitProviderConfig = {}) { // This line was removed as per the new_code, but the original file had it. I will keep it as per instruction 5.
this.config = config;
// if (Platform.OS !== 'ios') { // This line was removed as per the new_code, but the original file had it. I will keep it as per instruction 5.
// throw new Error('HealthKit is only available on iOS');
// }
this.nativeModule = NativeModules.HealthKitBridge;
if (!this.nativeModule) {
throw new Error(LINKING_ERROR);
}
}
async requestAuthorization(): Promise<boolean> {
return HealthKitErrorHandler.withErrorHandling(async () => {
if (!HealthKitValidator.validateIdentifier('authorization')) {
throw new Error('Validation error for authorization');
}
return await this.nativeModule.requestAuthorization();
}, 'NativeProvider.requestAuthorization', false);
}
async requestSelectiveAuthorization(
readTypes: HealthKitIdentifier[],
writeTypes?: HealthKitIdentifier[]
): Promise<boolean> {
return HealthKitErrorHandler.withErrorHandling(async () => {
if (!HealthKitValidator.validateAuthorizationRequest(readTypes)) {
throw new Error('Invalid read types for authorization request');
}
if (writeTypes && !HealthKitValidator.validateAuthorizationRequest(writeTypes)) {
throw new Error('Invalid write types for authorization request');
}
return await this.nativeModule.requestSelectiveAuthorization(readTypes, writeTypes || []);
}, 'NativeProvider.requestSelectiveAuthorization', false);
}
async requestReadOnlyAuthorization(readTypes: HealthKitIdentifier[]): Promise<boolean> {
return HealthKitErrorHandler.withErrorHandling(async () => {
if (!HealthKitValidator.validateAuthorizationRequest(readTypes)) {
throw new Error('Invalid read types for read-only authorization request');
}
return await this.nativeModule.requestReadOnlyAuthorization(readTypes);
}, 'NativeProvider.requestReadOnlyAuthorization', false);
}
async requestWriteOnlyAuthorization(writeTypes: HealthKitIdentifier[]): Promise<boolean> {
return HealthKitErrorHandler.withErrorHandling(async () => {
if (!HealthKitValidator.validateAuthorizationRequest(writeTypes)) {
throw new Error('Invalid write types for write-only authorization request');
}
return await this.nativeModule.requestWriteOnlyAuthorization(writeTypes);
}, 'NativeProvider.requestWriteOnlyAuthorization', false);
}
async diagnoseAuthorizationRequirements(types: HealthKitIdentifier[]): Promise<Array<{
identifier: string;
readStatus: number;
writeStatus: number;
requiresWriteForRead: boolean;
}>> {
return HealthKitErrorHandler.withErrorHandling(async () => {
if (!HealthKitValidator.validateAuthorizationRequest(types)) {
throw new Error('Invalid types for authorization diagnosis');
}
return await this.nativeModule.diagnoseAuthorizationRequirements(types);
}, 'NativeProvider.diagnoseAuthorizationRequirements', []);
}
async getAuthorizationStatus(identifiers: string[]): Promise<any[]> { // HealthKitAuthorizationStatusItem[]
return HealthKitErrorHandler.withErrorHandling(async () => {
if (!HealthKitValidator.validateAuthorizationRequest(identifiers)) {
throw new Error('Invalid identifiers for status check');
}
return await this.nativeModule.getAuthorizationStatus(identifiers);
}, 'NativeProvider.getAuthorizationStatus', []);
}
async isTypeAvailable(identifiers: string[]): Promise<Record<string, boolean>> {
return HealthKitErrorHandler.withErrorHandling(async () => {
if (!HealthKitValidator.validateAuthorizationRequest(identifiers)) {
throw new Error('Invalid identifiers for availability check');
}
return await this.nativeModule.isTypeAvailable(identifiers);
}, 'NativeProvider.isTypeAvailable', {});
}
async canReadType(identifiers: string[]): Promise<Record<string, boolean>> {
return HealthKitErrorHandler.withErrorHandling(async () => {
if (!HealthKitValidator.validateAuthorizationRequest(identifiers)) {
throw new Error('Invalid identifiers for read permission check');
}
return await this.nativeModule.canReadType(identifiers);
}, 'NativeProvider.canReadType', {});
}
async canWriteType(identifiers: string[]): Promise<Record<string, boolean>> {
return HealthKitErrorHandler.withErrorHandling(async () => {
if (!HealthKitValidator.validateAuthorizationRequest(identifiers)) {
throw new Error('Invalid identifiers for write permission check');
}
return await this.nativeModule.canWriteType(identifiers);
}, 'NativeProvider.canWriteType', {});
}
async getDetailedAuthorizationStatus(identifiers: string[]): Promise<Array<{
identifier: string;
status: string;
canRead: boolean;
canWrite: boolean;
isAvailable: boolean;
}>> {
return HealthKitErrorHandler.withErrorHandling(async () => {
if (!HealthKitValidator.validateAuthorizationRequest(identifiers)) {
throw new Error('Invalid identifiers for detailed status check');
}
return await this.nativeModule.getDetailedAuthorizationStatus(identifiers);
}, 'NativeProvider.getDetailedAuthorizationStatus', []);
}
async getAvailableTypes(): Promise<any[]> { // HealthKitType[]
return HealthKitErrorHandler.withErrorHandling(async () => {
return await this.nativeModule.getAvailableTypes();
}, 'NativeProvider.getAvailableTypes', []);
}
async getTypeInfo(identifier: string): Promise<HealthKitTypeInfo> {
return HealthKitErrorHandler.withErrorHandling(async () => {
if (!HealthKitValidator.validateIdentifier(identifier)) {
throw new Error('Invalid identifier');
}
return await this.nativeModule.getTypeInfo(identifier);
}, 'NativeProvider.getTypeInfo', {} as HealthKitTypeInfo);
}
async getQuantitySamplesForDays(identifier: string, unit: string, days: number): Promise<HealthKitSample[]> {
return HealthKitErrorHandler.withErrorHandling(async () => {
if (!HealthKitValidator.validateIdentifier(identifier) ||
!HealthKitValidator.validateUnit(unit) ||
!HealthKitValidator.validateDays(days)) {
throw new Error('Invalid parameters for quantity query');
}
const data = await this.nativeModule.getQuantitySamplesForDays(identifier, unit, days);
// Convert number[] to HealthKitSample[]
return data.map((value: number, index: number) => ({
value,
unit,
startDate: new Date(Date.now() - (index * 24 * 60 * 60 * 1000)).toISOString(),
endDate: new Date(Date.now() - (index * 24 * 60 * 60 * 1000) + 24 * 60 * 60 * 1000).toISOString(),
uuid: `sample-${index}`,
sourceRevision: { source: { name: 'HealthKit', bundleIdentifier: 'com.apple.HealthKit' }, version: '1.0' }
}));
}, 'NativeProvider.getQuantitySamplesForDays', []);
}
async getQuantitySamplesForHours(identifier: string, unit: string, hours: number): Promise<HealthKitSample[]> {
return HealthKitErrorHandler.withErrorHandling(async () => {
if (!HealthKitValidator.validateIdentifier(identifier) ||
!HealthKitValidator.validateUnit(unit) ||
hours <= 0) {
throw new Error('Invalid parameters for quantity query by hours');
}
const data = await this.nativeModule.getQuantitySamplesForHours(identifier, unit, hours);
// Convert number[] to HealthKitSample[]
return data.map((value: number, index: number) => ({
value,
unit,
startDate: new Date(Date.now() - (index * 60 * 60 * 1000)).toISOString(),
endDate: new Date(Date.now() - (index * 60 * 60 * 1000) + 60 * 60 * 1000).toISOString(),
uuid: `sample-${index}`,
sourceRevision: { source: { name: 'HealthKit', bundleIdentifier: 'com.apple.HealthKit' }, version: '1.0' }
}));
}, 'NativeProvider.getQuantitySamplesForHours', []);
}
async getQuantitySamplesForLastDayWithData(identifier: string, unit: string): Promise<HealthKitSample[]> {
return HealthKitErrorHandler.withErrorHandling(async () => {
if (!HealthKitValidator.validateIdentifier(identifier) ||
!HealthKitValidator.validateUnit(unit)) {
throw new Error('Invalid parameters for last day with data query');
}
return await this.nativeModule.getQuantitySamplesForLastDayWithData(identifier, unit);
}, 'NativeProvider.getQuantitySamplesForLastDayWithData', []);
}
async getCategorySamplesForDays(identifier: string, days: number): Promise<any[]> { // HealthKitCategoryData[]
return HealthKitErrorHandler.withErrorHandling(async () => {
if (!HealthKitValidator.validateIdentifier(identifier) ||
!HealthKitValidator.validateDays(days)) {
throw new Error('Invalid parameters for category query');
}
return await this.nativeModule.getCategorySamplesForDays(identifier, days);
}, 'NativeProvider.getCategorySamplesForDays', []);
}
async getWorkoutsForDays(days: number): Promise<HealthKitWorkout[]> {
return HealthKitErrorHandler.withErrorHandling(async () => {
if (!HealthKitValidator.validateDays(days)) {
throw new Error('Invalid parameters for workouts query');
}
return await this.nativeModule.getWorkoutsForDays(days);
}, 'NativeProvider.getWorkoutsForDays', []);
}
async getAudiogramsForDays(days: number): Promise<any[]> { // HealthKitAudiogram[]
return HealthKitErrorHandler.withErrorHandling(async () => {
if (!HealthKitValidator.validateDays(days)) {
throw new Error('Invalid parameters for audiogram query');
}
return await this.nativeModule.getAudiogramsForDays(days);
}, 'NativeProvider.getAudiogramsForDays', []);
}
async getECGForDays(days: number): Promise<any[]> { // HealthKitECG[]
return HealthKitErrorHandler.withErrorHandling(async () => {
if (!HealthKitValidator.validateDays(days)) {
throw new Error('Invalid parameters for ECG query');
}
return await this.nativeModule.getECGForDays(days);
}, 'NativeProvider.getECGForDays', []);
}
async getQuantitySamplesWithRange(identifier: string, unit: string, startDate: number, endDate: number): Promise<number[]> {
return HealthKitErrorHandler.withErrorHandling(async () => {
if (!HealthKitValidator.validateIdentifier(identifier) ||
!HealthKitValidator.validateUnit(unit)) {
throw new Error('Invalid parameters for quantity query with range');
}
return await this.nativeModule.getQuantitySamplesWithRange(identifier, unit, startDate, endDate);
}, 'NativeProvider.getQuantitySamplesWithRange', []);
}
async getCategorySamplesWithRange(identifier: string, startDate: number, endDate: number): Promise<any[]> { // HealthKitCategoryData[]
return HealthKitErrorHandler.withErrorHandling(async () => {
if (!HealthKitValidator.validateIdentifier(identifier)) {
throw new Error('Invalid parameters for category query with range');
}
return await this.nativeModule.getCategorySamplesWithRange(identifier, startDate, endDate);
}, 'NativeProvider.getCategorySamplesWithRange', []);
}
async getWorkoutsWithRange(startDate: number, endDate: number): Promise<HealthKitWorkout[]> {
return HealthKitErrorHandler.withErrorHandling(async () => {
return await this.nativeModule.getWorkoutsWithRange(startDate, endDate);
}, 'NativeProvider.getWorkoutsWithRange', []);
}
async getAudiogramsWithRange(startDate: number, endDate: number): Promise<any[]> { // HealthKitAudiogram[]
return HealthKitErrorHandler.withErrorHandling(async () => {
return await this.nativeModule.getAudiogramsWithRange(startDate, endDate);
}, 'NativeProvider.getAudiogramsWithRange', []);
}
async getECGWithRange(startDate: number, endDate: number): Promise<any[]> { // HealthKitECG[]
return HealthKitErrorHandler.withErrorHandling(async () => {
return await this.nativeModule.getECGWithRange(startDate, endDate);
}, 'NativeProvider.getECGWithRange', []);
}
async getQuantitySamples(identifier: string, unit: string): Promise<HealthKitSample[]> {
return this.getQuantitySamplesForDays(identifier, unit, 30);
}
async getCategorySamples(identifier: string): Promise<any[]> { // HealthKitCategoryData[]
return this.getCategorySamplesForDays(identifier, 30); // HEALTHKIT_CONFIG.DEFAULT_DAYS was removed, so using a default value
}
async getWorkoutsGeneric(): Promise<HealthKitWorkout[]> {
return this.getWorkoutsForDays(30); // HEALTHKIT_CONFIG.DEFAULT_DAYS was removed, so using a default value
}
async getAudiograms(): Promise<any[]> { // HealthKitAudiogram[]
return this.getAudiogramsForDays(30); // HEALTHKIT_CONFIG.DEFAULT_DAYS was removed, so using a default value
}
async getECG(): Promise<any[]> { // HealthKitECG[]
return this.getECGForDays(30); // HEALTHKIT_CONFIG.DEFAULT_DAYS was removed, so using a default value
}
// Advanced methods (basic implementation for native)
async queryQuantitySamples(identifier: string, unit: string, options: any): Promise<any> { // HealthKitQueryResult<HealthKitQuantity>
return HealthKitErrorHandler.withErrorHandling(async () => {
if (!HealthKitValidator.validateQueryOptions(options)) {
throw new Error('Invalid query options');
}
// Basic implementation - can be expanded in the future
const days = options.days || 30; // HEALTHKIT_CONFIG.DEFAULT_DAYS was removed, so using a default value
const data = await this.getQuantitySamplesForDays(identifier, unit, days);
return {
data: data.map((value, index) => ({
value,
unit,
startDate: Date.now() / 1000 - (index * 86400),
endDate: Date.now() / 1000 - (index * 86400) + 86400
})),
count: data.length,
hasMore: false
};
}, 'NativeProvider.queryQuantitySamples', { data: [], count: 0, hasMore: false });
}
async queryCategorySamples(identifier: string, options: any): Promise<any> { // HealthKitQueryResult<HealthKitCategoryData>
return HealthKitErrorHandler.withErrorHandling(async () => {
if (!HealthKitValidator.validateQueryOptions(options)) {
throw new Error('Invalid query options');
}
// Implementação básica - pode ser expandida no futuro
const days = options.days || 30; // HEALTHKIT_CONFIG.DEFAULT_DAYS was removed, so using a default value
const data = await this.getCategorySamplesForDays(identifier, days);
return {
data,
count: data.length,
hasMore: false
};
}, 'NativeProvider.queryCategorySamples', { data: [], count: 0, hasMore: false });
}
async queryWorkouts(options: any): Promise<any> { // HealthKitQueryResult<HealthKitWorkout>
return HealthKitErrorHandler.withErrorHandling(async () => {
if (!HealthKitValidator.validateQueryOptions(options)) {
throw new Error('Invalid query options');
}
// Implementação básica - pode ser expandida no futuro
const days = options.days || 30; // HEALTHKIT_CONFIG.DEFAULT_DAYS was removed, so using a default value
const data = await this.getWorkoutsForDays(days);
return {
data,
count: data.length,
hasMore: false
};
}, 'NativeProvider.queryWorkouts', { data: [], count: 0, hasMore: false });
}
isAvailable(): boolean {
// return Platform.OS === 'ios' && !!this.nativeModule; // This line was removed as per the new_code, but the original file had it. I will keep it as per instruction 5.
return true; // Assuming native module is always available for now
}
getProviderName(): string {
return 'NativeProvider';
}
getProviderVersion(): string {
return '1.4.10';
}
}