diff --git a/src/helpers/results.ts b/src/helpers/results.ts new file mode 100644 index 0000000..24d8499 --- /dev/null +++ b/src/helpers/results.ts @@ -0,0 +1,190 @@ +import { Platform } from 'react-native'; +import { + type ReadRecordsResult, + type RecordResult, +} from 'react-native-health-connect'; + +import type { HealthValue } from 'react-native-health'; +import { HealthLinkDataType, type ReadOptions } from '../types/dataTypes'; +import { + androidHeightUnitMap, + androidWeightUnitMap, + BloodGlucoseUnit, +} from '../types/units'; +import type { HealthLinkDataValue } from '../types/results'; + +const AppleHealthKit = require('react-native-health'); + +export const dataValueDeserializer = ( + dataType: T, + options: ReadOptions, + dataValue: HealthValue | RecordResult +): HealthLinkDataValue | null => { + if (Platform.OS === 'ios') { + let iosDataValue = dataValue as HealthValue; + let result = { + value: iosDataValue.value, + id: iosDataValue.id, + time: iosDataValue.startDate, + metadata: { + ...iosDataValue.metadata, + source: (iosDataValue as any).sourceId, + }, + }; + + switch (dataType) { + case HealthLinkDataType.BloodPressure: + return { + ...result, + value: { + systolic: (iosDataValue as any).bloodPressureSystolicValue, + diastolic: (iosDataValue as any).bloodPressureDiastolicValue, + }, + } as HealthLinkDataValue; + case HealthLinkDataType.OxygenSaturation: + return { + ...result, + value: iosDataValue.value * 100, + } as HealthLinkDataValue; + default: + return result as HealthLinkDataValue; + } + } else if (Platform.OS === 'android') { + let androidDataValue = dataValue as RecordResult; + let result = { + id: androidDataValue.metadata?.id, + metadata: { + ...androidDataValue.metadata, + source: androidDataValue.metadata?.dataOrigin as string, + }, + time: (androidDataValue as any).time, + }; + + switch (dataType) { + case HealthLinkDataType.BloodGlucose: { + const bloodGlucoseDataValue = + androidDataValue as RecordResult<'BloodGlucose'>; + return { + ...result, + metadata: { + ...result.metadata, + relationToMeal: bloodGlucoseDataValue.relationToMeal, + mealType: bloodGlucoseDataValue.mealType, + specimenSource: bloodGlucoseDataValue.specimenSource, + }, + value: + options?.unit === BloodGlucoseUnit.MgPerdL + ? bloodGlucoseDataValue.level?.inMilligramsPerDeciliter + : bloodGlucoseDataValue.level?.inMillimolesPerLiter, + } as unknown as HealthLinkDataValue; + } + case HealthLinkDataType.BloodPressure: { + const bloodPressureDataValue = + androidDataValue as RecordResult<'BloodPressure'>; + return { + ...result, + value: { + systolic: bloodPressureDataValue.systolic.inMillimetersOfMercury, + diastolic: bloodPressureDataValue.diastolic.inMillimetersOfMercury, + }, + } as HealthLinkDataValue; + } + case HealthLinkDataType.Height: { + const heightDataValue = androidDataValue as RecordResult<'Height'>; + return { + ...result, + value: androidHeightUnitMap(heightDataValue, options.unit), + } as HealthLinkDataValue; + } + case HealthLinkDataType.Weight: { + const weightDataValue = androidDataValue as RecordResult<'Weight'>; + return { + ...result, + value: androidWeightUnitMap(weightDataValue, options.unit), + } as HealthLinkDataValue; + } + case HealthLinkDataType.HeartRate: { + const heartRateDataValue = + androidDataValue as RecordResult<'HeartRate'>; + return { + ...result, + value: heartRateDataValue.samples[0]?.beatsPerMinute, + time: heartRateDataValue.samples[0]?.time, + } as HealthLinkDataValue; + } + case HealthLinkDataType.RestingHeartRate: { + const restingHeartRateDataValue = + androidDataValue as RecordResult<'RestingHeartRate'>; + return { + ...result, + value: restingHeartRateDataValue.beatsPerMinute, + } as HealthLinkDataValue; + } + case HealthLinkDataType.OxygenSaturation: { + const oxygenSaturationDataValue = + androidDataValue as RecordResult<'OxygenSaturation'>; + return { + ...result, + value: oxygenSaturationDataValue.percentage, + } as HealthLinkDataValue; + } + case HealthLinkDataType.Steps: { + const stepsDataValue = androidDataValue as RecordResult<'Steps'>; + return { + ...result, + value: stepsDataValue.count, + } as HealthLinkDataValue; + } + default: + return result as HealthLinkDataValue; + } + } + return null; +}; + +export const readDataResultDeserializer = ( + dataType: T, + options: ReadOptions, + data: ReadRecordsResult | HealthValue[] +): HealthLinkDataValue[] => { + let dataValueArray: RecordResult[] | HealthValue[] = + Platform.OS === 'ios' + ? (data as HealthValue[]) + : (data as ReadRecordsResult).records; + return dataValueArray.reduce((acc, d) => { + const deserializedValue = dataValueDeserializer(dataType, options, d); + if (deserializedValue !== null) { + acc.push(deserializedValue); + } + return acc; + }, [] as HealthLinkDataValue[]); +}; + +export const readIosCallback = ( + dataType: HealthLinkDataType, + options: ReadOptions +) => { + return new Promise((resolve, reject) => { + dataValueToIosReadFunction(dataType)(options, (err: any, results: any) => { + if (err) { + console.error(err); + reject(err); + } + resolve(results); + }); + }); +}; + +export const dataValueToIosReadFunction = (dataType: HealthLinkDataType) => { + const dataTypeMap: { [key in HealthLinkDataType]?: any } = { + BloodGlucose: AppleHealthKit.getBloodGlucoseSamples, + Height: AppleHealthKit.getHeightSamples, + Weight: AppleHealthKit.getWeightSamples, + HeartRate: AppleHealthKit.getHeartRateSamples, + RestingHeartRate: AppleHealthKit.getRestingHeartRateSamples, + OxygenSaturation: AppleHealthKit.getOxygenSaturationSamples, + BloodPressure: AppleHealthKit.getBloodPressureSamples, + Steps: AppleHealthKit.getDailyStepCountSamples, + }; + return dataTypeMap[dataType] || (() => {}); +}; diff --git a/src/helpers/save.ts b/src/helpers/save.ts new file mode 100644 index 0000000..845db8e --- /dev/null +++ b/src/helpers/save.ts @@ -0,0 +1,142 @@ +import { type HealthValueOptions } from 'react-native-health'; +import { HealthLinkDataType } from '../types/dataTypes'; +import { Platform } from 'react-native'; +import type { HealthConnectRecord } from 'react-native-health-connect'; +import { + BloodGlucoseUnit, + HeighUnit, + unitToIosUnitMap, + WeightUnit, +} from '../types/units'; +import type { WriteDataType, WriteOptions } from '../types/save'; + +const AppleHealthKit = require('react-native-health'); + +export const serializeWriteOptions = ( + dataType: T, + options: WriteOptions +): HealthValueOptions | HealthConnectRecord | null => { + if (Platform.OS === 'ios') { + if (!options.unit) return null; + const value = options.value ?? 0; + const iosOptions: HealthValueOptions = { + unit: unitToIosUnitMap[options.unit], + value: + options.unit === WeightUnit.Kg + ? value * 1000 + : options.unit === HeighUnit.Cm + ? value / 100 + : value, + }; + return iosOptions; + } + + if (Platform.OS === 'android') { + const androidOptions = { + startTime: options.startDate ?? new Date().toISOString(), + endTime: options.endDate ?? new Date().toISOString(), + }; + + switch (dataType) { + case HealthLinkDataType.BloodGlucose: { + const { + relationToMeal = 0, + mealType = 0, + specimenSource = 0, + } = options.metadata || {}; + const unit = + options.unit === BloodGlucoseUnit.MgPerdL + ? 'milligramsPerDeciliter' + : 'millimolesPerLiter'; + return { + ...androidOptions, + recordType: 'BloodGlucose', + relationToMeal, + mealType, + specimenSource, + time: options.time ?? androidOptions.startTime, + level: { unit, value: options.value as number }, + }; + } + case HealthLinkDataType.Steps: + return { + ...androidOptions, + recordType: 'Steps', + count: options.value ?? 0, + }; + case HealthLinkDataType.Weight: { + const weightUnit = + options.unit === WeightUnit.Kg + ? 'kilograms' + : options.unit === WeightUnit.Gram + ? 'grams' + : 'pounds'; + return { + ...androidOptions, + recordType: 'Weight', + time: options.time ?? androidOptions.startTime, + weight: { unit: weightUnit, value: options.value as number }, + }; + } + case HealthLinkDataType.Height: { + const heightUnit = + options.unit === HeighUnit.Cm || options.unit === HeighUnit.Meter + ? 'meters' + : options.unit === HeighUnit.Foot + ? 'feet' + : 'inches'; + const heightValue = + options.unit === HeighUnit.Cm + ? (options.value as number) / 100 + : (options.value as number); + return { + ...androidOptions, + recordType: 'Height', + time: options.time ?? androidOptions.startTime, + height: { unit: heightUnit, value: heightValue }, + }; + } + case HealthLinkDataType.HeartRate: + return { + ...androidOptions, + recordType: 'HeartRate', + samples: [ + { + time: options.time ?? androidOptions.startTime, + beatsPerMinute: options.value as number, + }, + ], + }; + default: + return null; + } + } + + return null; +}; + +export const writeIosCallback = ( + dataType: WriteDataType, + options: WriteOptions +) => { + return new Promise((resolve, reject) => { + dataValueToIosWriteFunction(dataType)(options, (err: any, results: any) => { + if (err) { + console.error(err); + reject(err); + } + resolve(results); + }); + }); +}; + +export const dataValueToIosWriteFunction = (dataType: WriteDataType) => { + const dataTypeMap: { [key in WriteDataType]?: any } = { + BloodGlucose: AppleHealthKit.saveBloodGlucoseSample, + Height: AppleHealthKit.saveHeight, + Weight: AppleHealthKit.saveWeight, + HeartRate: AppleHealthKit.saveHeartRateSample, + Steps: AppleHealthKit.saveSteps, + }; + return dataTypeMap[dataType] || (() => {}); +}; diff --git a/src/index.tsx b/src/index.tsx index f189126..eff7703 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -19,17 +19,10 @@ import { type ReadOptions, } from './types/dataTypes'; import type { HealthValue } from 'react-native-health'; -import { - readDataResultDeserializer, - readIosCallback, - type HealthLinkDataValue, -} from './types/results'; -import { - serializeWriteOptions, - writeIosCallback, - type WriteDataType, - type WriteOptions, -} from './types/save'; +import { type HealthLinkDataValue } from './types/results'; +import { type WriteDataType, type WriteOptions } from './types/save'; +import { serializeWriteOptions, writeIosCallback } from './helpers/save'; +import { readDataResultDeserializer, readIosCallback } from './helpers/results'; const AppleHealthKit = require('react-native-health'); @@ -63,17 +56,13 @@ export const write = async ( dataType: T, data: WriteOptions ): Promise => { + const serializedData = serializeWriteOptions(dataType, data); + if (serializedData === null) { + return; + } if (Platform.OS === 'ios') { - const serializedData = serializeWriteOptions(dataType, data); - if (serializedData === null) { - return; - } await writeIosCallback(dataType, serializedData as WriteOptions); } else if (Platform.OS === 'android') { - const serializedData = serializeWriteOptions(dataType, data); - if (serializedData === null) { - return; - } await insertRecords([serializedData as HealthConnectRecord]).catch((e) => { console.error(e); }); diff --git a/src/types/results.ts b/src/types/results.ts index 0d7c758..11e3643 100644 --- a/src/types/results.ts +++ b/src/types/results.ts @@ -1,18 +1,4 @@ -import { Platform } from 'react-native'; -import { - type ReadRecordsResult, - type RecordResult, -} from 'react-native-health-connect'; - -import type { HealthValue } from 'react-native-health'; -import { HealthLinkDataType, type ReadOptions } from './dataTypes'; -import { - androidHeightUnitMap, - androidWeightUnitMap, - BloodGlucoseUnit, -} from './units'; - -const AppleHealthKit = require('react-native-health'); +import { HealthLinkDataType } from './dataTypes'; export type HealthLinkDataValueMap = { [HealthLinkDataType.BloodGlucose]: number; @@ -33,176 +19,3 @@ export interface HealthLinkDataValue { source?: string; } & Record; } -export const dataValueDeserializer = ( - dataType: T, - options: ReadOptions, - dataValue: HealthValue | RecordResult -): HealthLinkDataValue | null => { - if (Platform.OS === 'ios') { - let iosDataValue = dataValue as HealthValue; - let result = { - value: iosDataValue.value, - id: iosDataValue.id, - time: iosDataValue.startDate, - metadata: { - ...iosDataValue.metadata, - source: (iosDataValue as any).sourceId, - }, - }; - - switch (dataType) { - case HealthLinkDataType.BloodPressure: - return { - ...result, - value: { - systolic: (iosDataValue as any).bloodPressureSystolicValue, - diastolic: (iosDataValue as any).bloodPressureDiastolicValue, - }, - } as HealthLinkDataValue; - case HealthLinkDataType.OxygenSaturation: - return { - ...result, - value: iosDataValue.value * 100, - } as HealthLinkDataValue; - default: - return result as HealthLinkDataValue; - } - } else if (Platform.OS === 'android') { - let androidDataValue = dataValue as RecordResult; - let result = { - id: androidDataValue.metadata?.id, - metadata: { - ...androidDataValue.metadata, - source: androidDataValue.metadata?.dataOrigin as string, - }, - time: (androidDataValue as any).time, - }; - - switch (dataType) { - case HealthLinkDataType.BloodGlucose: { - const bloodGlucoseDataValue = - androidDataValue as RecordResult<'BloodGlucose'>; - return { - ...result, - metadata: { - ...result.metadata, - relationToMeal: bloodGlucoseDataValue.relationToMeal, - mealType: bloodGlucoseDataValue.mealType, - specimenSource: bloodGlucoseDataValue.specimenSource, - }, - value: - options?.unit === BloodGlucoseUnit.MgPerdL - ? bloodGlucoseDataValue.level?.inMilligramsPerDeciliter - : bloodGlucoseDataValue.level?.inMillimolesPerLiter, - } as unknown as HealthLinkDataValue; - } - case HealthLinkDataType.BloodPressure: { - const bloodPressureDataValue = - androidDataValue as RecordResult<'BloodPressure'>; - return { - ...result, - value: { - systolic: bloodPressureDataValue.systolic.inMillimetersOfMercury, - diastolic: bloodPressureDataValue.diastolic.inMillimetersOfMercury, - }, - } as HealthLinkDataValue; - } - case HealthLinkDataType.Height: { - const heightDataValue = androidDataValue as RecordResult<'Height'>; - return { - ...result, - value: androidHeightUnitMap(heightDataValue, options.unit), - } as HealthLinkDataValue; - } - case HealthLinkDataType.Weight: { - const weightDataValue = androidDataValue as RecordResult<'Weight'>; - return { - ...result, - value: androidWeightUnitMap(weightDataValue, options.unit), - } as HealthLinkDataValue; - } - case HealthLinkDataType.HeartRate: { - const heartRateDataValue = - androidDataValue as RecordResult<'HeartRate'>; - return { - ...result, - value: heartRateDataValue.samples[0]?.beatsPerMinute, - time: heartRateDataValue.samples[0]?.time, - } as HealthLinkDataValue; - } - case HealthLinkDataType.RestingHeartRate: { - const restingHeartRateDataValue = - androidDataValue as RecordResult<'RestingHeartRate'>; - return { - ...result, - value: restingHeartRateDataValue.beatsPerMinute, - } as HealthLinkDataValue; - } - case HealthLinkDataType.OxygenSaturation: { - const oxygenSaturationDataValue = - androidDataValue as RecordResult<'OxygenSaturation'>; - return { - ...result, - value: oxygenSaturationDataValue.percentage, - } as HealthLinkDataValue; - } - case HealthLinkDataType.Steps: { - const stepsDataValue = androidDataValue as RecordResult<'Steps'>; - return { - ...result, - value: stepsDataValue.count, - } as HealthLinkDataValue; - } - default: - return result as HealthLinkDataValue; - } - } - return null; -}; - -export const readDataResultDeserializer = ( - dataType: T, - options: ReadOptions, - data: ReadRecordsResult | HealthValue[] -): HealthLinkDataValue[] => { - let dataValueArray: RecordResult[] | HealthValue[] = - Platform.OS === 'ios' - ? (data as HealthValue[]) - : (data as ReadRecordsResult).records; - return dataValueArray.reduce((acc, d) => { - const deserializedValue = dataValueDeserializer(dataType, options, d); - if (deserializedValue !== null) { - acc.push(deserializedValue); - } - return acc; - }, [] as HealthLinkDataValue[]); -}; - -export const readIosCallback = ( - dataType: HealthLinkDataType, - options: ReadOptions -) => { - return new Promise((resolve, reject) => { - dataValueToIosReadFunction(dataType)(options, (err: any, results: any) => { - if (err) { - console.error(err); - reject(err); - } - resolve(results); - }); - }); -}; - -export const dataValueToIosReadFunction = (dataType: HealthLinkDataType) => { - const dataTypeMap: { [key in HealthLinkDataType]?: any } = { - BloodGlucose: AppleHealthKit.getBloodGlucoseSamples, - Height: AppleHealthKit.getHeightSamples, - Weight: AppleHealthKit.getWeightSamples, - HeartRate: AppleHealthKit.getHeartRateSamples, - RestingHeartRate: AppleHealthKit.getRestingHeartRateSamples, - OxygenSaturation: AppleHealthKit.getOxygenSaturationSamples, - BloodPressure: AppleHealthKit.getBloodPressureSamples, - Steps: AppleHealthKit.getDailyStepCountSamples, - }; - return dataTypeMap[dataType] || (() => {}); -}; diff --git a/src/types/save.ts b/src/types/save.ts index 3f5624f..4ec6276 100644 --- a/src/types/save.ts +++ b/src/types/save.ts @@ -1,18 +1,12 @@ -import { type HealthValueOptions } from 'react-native-health'; import { HealthLinkDataType } from './dataTypes'; -import { Platform } from 'react-native'; -import type { HealthConnectRecord } from 'react-native-health-connect'; import { BloodGlucoseUnit, HeartRateUnit, HeighUnit, StepsUnit, - unitToIosUnitMap, WeightUnit, } from './units'; -const AppleHealthKit = require('react-native-health'); - export interface WriteOptionsBase { value?: T extends HealthLinkDataType.BloodPressure ? { diastolic: number; systolic: number } @@ -42,133 +36,3 @@ export type WriteDataType = | HealthLinkDataType.Weight | HealthLinkDataType.HeartRate | HealthLinkDataType.Steps; - -export const serializeWriteOptions = ( - dataType: T, - options: WriteOptions -): HealthValueOptions | HealthConnectRecord | null => { - if (Platform.OS === 'ios') { - switch (dataType) { - default: - let iosOptions: HealthValueOptions = { - unit: options.unit && unitToIosUnitMap[options.unit], - value: - options.unit === WeightUnit.Kg - ? (options.value ?? 0) * 1000 - : options.unit === HeighUnit.Cm - ? (options.value ?? 0) / 100 - : (options.value ?? 0), - }; - return iosOptions; - } - } else if (Platform.OS === 'android') { - let androidOptions = { - startTime: options.startDate, - endTime: options.endDate, - }; - switch (dataType) { - case HealthLinkDataType.BloodGlucose: - return { - ...androidOptions, - recordType: 'BloodGlucose', - relationToMeal: options.metadata?.relationToMeal ?? 0, - mealType: options.metadata?.mealType ?? 0, - specimenSource: options.metadata?.specimenSource ?? 0, - time: options.time ?? options.startDate ?? new Date().toISOString(), - level: { - unit: - options.unit === BloodGlucoseUnit.MgPerdL - ? 'milligramsPerDeciliter' - : 'millimolesPerLiter', - value: options.value as number, - }, - }; - case HealthLinkDataType.Steps: - return { - ...androidOptions, - recordType: 'Steps', - count: options.value ?? 0, - startTime: options.startDate ?? new Date().toISOString(), - endTime: options.endDate ?? new Date().toISOString(), - }; - case HealthLinkDataType.Weight: - return { - ...androidOptions, - recordType: 'Weight', - time: options.time ?? options.startDate ?? new Date().toISOString(), - weight: { - unit: - (options.unit as unknown as WeightUnit) === WeightUnit.Kg - ? 'kilograms' - : (options.unit as unknown as WeightUnit) === WeightUnit.Gram - ? 'grams' - : 'pounds', - value: options.value as number, - }, - }; - case HealthLinkDataType.Height: - return { - ...androidOptions, - recordType: 'Height', - time: options.time ?? options.startDate ?? new Date().toISOString(), - height: { - unit: - (options.unit as unknown as HeighUnit) === HeighUnit.Cm || - (options.unit as unknown as HeighUnit) === HeighUnit.Meter - ? 'meters' - : (options.unit as unknown as HeighUnit) === HeighUnit.Foot - ? 'feet' - : 'inches', - value: - (options.unit as unknown as HeighUnit) === HeighUnit.Cm - ? (options.value as number) / 100 - : (options.value as number), - }, - }; - case HealthLinkDataType.HeartRate: - return { - ...androidOptions, - recordType: 'HeartRate', - startTime: options.startDate ?? new Date().toISOString(), - endTime: options.endDate ?? new Date().toISOString(), - samples: [ - { - time: - options.time ?? options.startDate ?? new Date().toISOString(), - beatsPerMinute: options.value as number, - }, - ], - }; - default: - return null; - } - } - //@ts-ignore - return options; -}; - -export const writeIosCallback = ( - dataType: WriteDataType, - options: WriteOptions -) => { - return new Promise((resolve, reject) => { - dataValueToIosWriteFunction(dataType)(options, (err: any, results: any) => { - if (err) { - console.error(err); - reject(err); - } - resolve(results); - }); - }); -}; - -export const dataValueToIosWriteFunction = (dataType: WriteDataType) => { - const dataTypeMap: { [key in WriteDataType]?: any } = { - BloodGlucose: AppleHealthKit.saveBloodGlucoseSample, - Height: AppleHealthKit.saveHeight, - Weight: AppleHealthKit.saveWeight, - HeartRate: AppleHealthKit.saveHeartRateSample, - Steps: AppleHealthKit.saveSteps, - }; - return dataTypeMap[dataType] || (() => {}); -};