google-fit
Version:
A React Native bridge module for interacting with Google Fit
744 lines (681 loc) • 22.4 kB
JavaScript
import { DeviceEventEmitter, NativeModules } from 'react-native'
import { buildDailySteps, isNil, KgToLbs, lbsAndOzToK, prepareResponse } from './src/utils'
import PossibleScopes from './src/scopes'
const googleFit = NativeModules.RNGoogleFit
class RNGoogleFit {
eventListeners = []
isAuthorized = false
authorize = async (options = {}) => {
const successResponse = { success: true }
try {
await this.checkIsAuthorized()
if (this.isAuthorized) {
return successResponse
}
const authResult = await new Promise((resolve, reject) => {
this.onAuthorize(() => {
this.isAuthorized = true
resolve(successResponse)
})
this.onAuthorizeFailure((error) => {
this.isAuthorized = false
reject({ success: false, message: error.message })
})
const defaultScopes = [
Scopes.FITNESS_ACTIVITY_READ,
Scopes.FITNESS_BODY_READ_WRITE,
Scopes.FITNESS_LOCATION_READ,
]
googleFit.authorize({
scopes: (options && options.scopes) || defaultScopes,
})
})
return authResult
} catch (error) {
return { success: false, message: error.message }
}
}
checkIsAuthorized = async () => {
const {isAuthorized} = await googleFit.isAuthorized()
this.isAuthorized = isAuthorized
}
disconnect = () => {
this.isAuthorized = false
googleFit.disconnect()
this.removeListeners()
}
removeListeners = () => {
this.eventListeners.forEach((eventListener) => eventListener.remove())
this.eventListeners = []
}
/**
* Start recording fitness data
*
* You could specify data by array dataTypes. Possible values - step, distance, activity which corresponds
* DataTypes.TYPE_STEP_CUMULATIVE, DataType.TYPE_DISTANCE_DELTA and DataType.TYPE_ACTIVITIES_SAMPLES
*
* Default value for dataTypes is steps and distance data
*
* This function relies on sending events to signal the RecordingAPI status
* Simply create an event listener for the {DATA_TYPE}_RECORDING (ex. STEP_RECORDING)
* and check for {recording: true} as the event data
*/
startRecording = (callback, dataTypes = ['step', 'distance']) => {
googleFit.startFitnessRecording(dataTypes)
const eventListeners = dataTypes.map((dataTypeName) => {
const eventName = `${dataTypeName.toUpperCase()}_RECORDING`
return DeviceEventEmitter.addListener(eventName, (event) => callback(event))
})
this.eventListeners.push(...eventListeners)
}
// Will be deprecated in future releases
getSteps(dayStart, dayEnd) {
googleFit.getDailySteps(Date.parse(dayStart), Date.parse(dayEnd))
}
// Will be deprecated in future releases
getWeeklySteps(startDate) {
googleFit.getWeeklySteps(Date.parse(startDate), Date.now())
}
_retrieveDailyStepCountSamples = (startDate, endDate, callback) => {
googleFit.getDailyStepCountSamples(startDate, endDate,
(msg) => callback(msg, false),
(res) => {
if (res.length > 0) {
callback(false, res.map(function (dev) {
const obj = {}
obj.source = dev.source.appPackage + ((dev.source.stream) ? ':' + dev.source.stream : '')
obj.steps = buildDailySteps(dev.steps)
return obj
}, this))
} else {
callback('There is no any steps data for this period', false)
}
}
)
}
_retrieveDailySleepDataSamples = (startDate, endDate, currentDate, callback) => {
googleFit.getDailySleepDataSamples(startDate, endDate,
(msg) => callback(msg, false),
(res) => {
if (res.length > 0) {
let response = []
res.map((dev) => {
let cd = new Date(currentDate).toLocaleDateString()
let ed = new Date(dev.endDate).toLocaleDateString()
if (dev.activity === 'sleep' && cd === ed) {
const obj = {}
obj.activity = dev.activity
obj.startDate = dev.startDate
obj.endDate = dev.endDate
obj.difference = dev.endDate - dev.startDate
obj.appPackageName = dev.appPackageName
obj.identifier = dev.identifier
return response.push(obj)
}
}, this)
callback(false, response)
} else {
callback('There is no any sleep data for this period', false)
}
}
)
}
_retrieveSleepDataPerWeek = (startDate, endDate, callback) => {
googleFit.getDailySleepDataSamples(startDate, endDate,
(msg) => callback(msg, false),
(res) => {
if (res.length > 0) {
const response = []
for (let i = 1; i <= 7; i++) {
const currentDate = new Date(startDate)
currentDate.setDate(currentDate.getDate() + i)
const sleepDataDateList = []
res.map(dev => {
const endDate = new Date(dev.endDate)
if (dev.activity === 'sleep' && currentDate.toLocaleDateString() === endDate.toLocaleDateString()){
const obj = {}
obj.activity = dev.activity
obj.startDate = dev.startDate
obj.endDate = dev.endDate
obj.difference = dev.endDate - dev.startDate
obj.appPackageName = dev.appPackageName
obj.identifier = dev.identifier
sleepDataDateList.push(obj)
}
})
response.push(sleepDataDateList)
}
callback(false, response)
} else {
callback('There is no any sleep data for this period', false)
}
}
)
}
_retrieveBloodPressureDataSamples = (startDate, endDate, callback) => {
googleFit.getBloodPressureSamples(startDate, endDate,
(msg) => callback(msg, false),
(res) => {
if (res.length > 0) {
callback(false, prepareResponse(res, 'value'))
} else {
callback('There is no any heart rate data for this period', false)
}
}
)
}
_saveSleepData = (startDate, endDate, callback) => {
googleFit.saveSleepData(startDate, endDate,
(msg) => callback(msg, false),
(res) => {
if (res.isSuccess) {
callback(false, res);
} else {
callback('There is no any sleep data for this period', false)
}
}
)
}
_saveBloodPressure = (startDate, endDate, callback) => {
googleFit.saveBloodPressure(startDate, endDate,
(msg) => callback(msg, false),
(res) => {
if (res.isSuccess) {
callback(false, res);
} else {
callback('Blood pressure is failure', false);
}
}
)
}
/**
* Get the total steps per day over a specified date range.
* @param {Object} options getDailyStepCountSamples accepts an options object containing required startDate: ISO8601Timestamp and endDate: ISO8601Timestamp.
* @param {Function} callback The function will be called with an array of elements.
*/
getDailyStepCountSamples = (options, callback) => {
const startDate = !isNil(options.startDate) ? Date.parse(options.startDate) : (new Date()).setHours(0, 0, 0, 0)
const endDate = !isNil(options.endDate) ? Date.parse(options.endDate) : (new Date()).valueOf()
if (!callback || typeof callback !== 'function') {
return new Promise((resolve, reject) => {
this._retrieveDailyStepCountSamples(startDate, endDate, (error, result) => {
if (!error) {
resolve(result)
} else {
reject(error)
}
})
})
}
this._retrieveDailyStepCountSamples(startDate, endDate, callback)
}
getDailySleepDataSamples = (options, callback) => {
const startDate = !isNil(options.startDate) ? Date.parse(options.startDate) : (new Date()).setHours(0, 0, 0, 0)
const endDate = !isNil(options.endDate) ? Date.parse(options.endDate) : (new Date()).valueOf()
const currentDate = !isNil(options.currentDate) ? Date.parse(options.currentDate) : (new Date()).setHours(0, 0, 0, 0)
if (!callback || typeof callback !== 'function') {
return new Promise((resolve, reject) => {
this._retrieveDailySleepDataSamples(startDate, endDate, currentDate, (error, result) => {
if (!error) {
resolve(result)
} else {
reject(error)
}
})
})
}
this._retrieveDailySleepDataSamples(startDate, endDate, currentDate, callback)
}
getSleepPerWeek = (options, callback) => {
const startDate = !isNil(options.startDate) ? Date.parse(options.startDate) : (new Date()).setHours(0, 0, 0, 0)
const endDate = !isNil(options.endDate) ? Date.parse(options.endDate) : (new Date()).valueOf()
if (!callback || typeof callback !== 'function') {
return new Promise((resolve, reject) => {
this._retrieveSleepDataPerWeek(startDate, endDate, (error, result) => {
if (!error) {
resolve(result)
} else {
reject(error)
}
})
})
}
this._retrieveSleepDataPerWeek(startDate, endDate, callback)
}
saveSleepData = (options, callback) => {
const startDate = !isNil(options.startDate) ? Date.parse(options.startDate) : (new Date()).setHours(0, 0, 0, 0)
const endDate = !isNil(options.endDate) ? Date.parse(options.endDate) : (new Date()).valueOf()
if (!callback || typeof callback !== 'function') {
return new Promise((resolve, reject) => {
this._saveSleepData(startDate, endDate, (error, result) => {
if (!error) {
resolve(result)
} else {
reject(error)
}
})
})
}
this._saveSleepData(startDate, endDate, callback)
}
/**
* Get the total distance per day over a specified date range.
* @param {Object} options getDailyDistanceSamples accepts an options object containing required startDate: ISO8601Timestamp and endDate: ISO8601Timestamp.
* @param {function} callback The function will be called with an array of elements.
*/
getDailyDistanceSamples(options, callback) {
const startDate = !isNil(options.startDate) ? Date.parse(options.startDate) : (new Date()).setHours(0, 0, 0, 0)
const endDate = !isNil(options.endDate) ? Date.parse(options.endDate) : (new Date()).valueOf()
googleFit.getDailyDistanceSamples(startDate,
endDate,
(msg) => {
callback(msg, false)
},
(res) => {
if (res.length > 0) {
callback(false, prepareResponse(res, 'distance'))
} else {
callback('There is no any distance data for this period', false)
}
})
}
getActivitySamples(options, callback) {
googleFit.getActivitySamples(
options.startDate,
options.endDate,
(error) => {
callback(error, false)
},
(res) => {
if (res.length > 0) {
callback(false, res)
} else {
callback('There is no any distance data for this period', false)
}
})
}
/**
* Get the total calories per day over a specified date range.
* @param {Object} options getDailyCalorieSamples accepts an options object containing:
* required startDate: ISO8601Timestamp and endDate: ISO8601Timestamp
* optional basalCalculation - {true || false} should we substract the basal metabolic rate averaged over a week
* @param {Function} callback The function will be called with an array of elements.
*/
getDailyCalorieSamples(options, callback) {
const basalCalculation = options.basalCalculation !== false
const startDate = Date.parse(options.startDate)
const endDate = Date.parse(options.endDate)
googleFit.getDailyCalorieSamples(
startDate,
endDate,
basalCalculation,
(msg) => {
callback(msg, false)
},
(res) => {
if (res.length > 0) {
callback(false, prepareResponse(res, 'calorie'))
} else {
callback('There is no any calorie data for this period', false)
}
})
}
saveFood(options, callback) {
options.date = Date.parse(options.date)
googleFit.saveFood(options,
(msg) => {
callback(msg, false)
},
(res) => {
callback(false, res)
})
}
/**
* Query for weight samples. the options object is used to setup a query to retrieve relevant samples.
* @param {Object} options getDailyStepCountSamples accepts an options object containing unit: "pound"/"kg",
* startDate: ISO8601Timestamp and endDate: ISO8601Timestamp.
* @callback callback The function will be called with an array of elements.
*/
getWeightSamples = (options, callback) => {
const startDate = !isNil(options.startDate) ? Date.parse(options.startDate) : (new Date()).setHours(0, 0, 0, 0)
const endDate = !isNil(options.endDate) ? Date.parse(options.endDate) : (new Date()).valueOf()
googleFit.getWeightSamples(startDate,
endDate,
(msg) => {
callback(msg, false)
},
(res) => {
if (res.length > 0) {
res = res.map((el) => {
if (el.value) {
if (options.unit === 'pound') {
el.value = KgToLbs(el.value) //convert back to pounds
}
el.startDate = new Date(el.startDate).toISOString()
el.endDate = new Date(el.endDate).toISOString()
return el
}
})
callback(false, res.filter((day) => !isNil(day)))
} else {
callback('There is no any weight data for this period', false)
}
})
}
getHeightSamples(options, callback) {
const startDate = Date.parse(options.startDate)
const endDate = Date.parse(options.endDate)
googleFit.getHeightSamples(startDate,
endDate,
(msg) => {
callback(msg, false)
},
(res) => {
if (res.length > 0) {
callback(false, prepareResponse(res, 'value'))
} else {
callback('There is no any height data for this period', false)
}
})
}
saveHeight(options, callback) {
options.date = Date.parse(options.date)
googleFit.saveHeight(options,
(msg) => {
callback(msg, false)
},
(res) => {
callback(false, res)
})
}
saveWeight(options, callback) {
if (options.unit == 'pound') {
options.value = lbsAndOzToK({pounds: options.value, ounces: 0}) //convert pounds and ounces to kg
}
options.date = Date.parse(options.date)
googleFit.saveWeight(options,
(msg) => {
callback(msg, false)
},
(res) => {
callback(false, res)
})
}
deleteWeight = (options, callback) => {
if (options.unit === 'pound') {
options.value = lbsAndOzToK({pounds: options.value, ounces: 0}) //convert pounds and ounces to kg
}
options.date = Date.parse(options.date)
googleFit.deleteWeight(options,
(msg) => {
callback(msg, false)
},
(res) => {
callback(false, res)
})
}
deleteHeight = (options, callback) => {
options.date = Date.parse(options.date)
googleFit.deleteWeight(options,
(msg) => {
callback(msg, false)
},
(res) => {
callback(false, res)
})
}
isAvailable(callback) { // true if GoogleFit installed
googleFit.isAvailable(
(msg) => {
callback(msg, false)
},
(res) => {
callback(false, res)
})
}
isEnabled(callback) { // true if permission granted
googleFit.isEnabled(
(msg) => {
callback(msg, false)
},
(res) => {
callback(false, res)
})
}
openFit() {
googleFit.openFit()
}
observeSteps = (callback) => {
const stepsObserver = DeviceEventEmitter.addListener(
'StepChangedEvent',
(steps) => callback(steps)
)
googleFit.observeSteps()
this.eventListeners.push(stepsObserver)
}
observeHistory = (callback) => {
const historyObserver = DeviceEventEmitter.addListener(
'StepHistoryChangedEvent',
(steps) => callback(steps)
)
this.eventListeners.push(historyObserver)
}
onAuthorize = (callback) => {
const authObserver = DeviceEventEmitter.addListener(
'GoogleFitAuthorizeSuccess',
(authorized) => callback(authorized)
)
this.eventListeners.push(authObserver)
}
onAuthorizeFailure = (callback) => {
const authFailedObserver = DeviceEventEmitter.addListener(
'GoogleFitAuthorizeFailure',
(authorized) => callback(authorized)
)
this.eventListeners.push(authFailedObserver)
}
unsubscribeListeners = () => {
this.removeListeners()
}
getHeartRateSamples(options, callback) {
const startDate = Date.parse(options.startDate)
const endDate = Date.parse(options.endDate)
googleFit.getHeartRateSamples(
startDate,
endDate,
(msg) => {
callback(msg, false)
},
(res) => {
if (res.length > 0) {
callback(false, prepareResponse(res, 'value'))
} else {
callback('There is no any heart rate data for this period', false)
}
})
}
getBloodPressureSamples(options, callback) {
const startDate = !isNil(options.startDate) ? Date.parse(options.startDate) : (new Date()).setHours(0, 0, 0, 0)
const endDate = !isNil(options.endDate) ? Date.parse(options.endDate) : (new Date()).valueOf()
if (!callback || typeof callback !== 'function') {
return new Promise((resolve, reject) => {
this._retrieveBloodPressureDataSamples(startDate, endDate, (error, result) => {
if (!error) {
resolve(result)
} else {
reject(error)
}
})
})
}
this._retrieveBloodPressureDataSamples(startDate, endDate, callback)
// googleFit.getBloodPressureSamples(
// startDate,
// endDate,
// (msg) => {
// callback(msg, false)
// },
// (res) => {
// if (res.length > 0) {
// callback(false, prepareResponse(res, 'value'))
// } else {
// callback('There is no any heart rate data for this period', false)
// }
// })
}
saveBloodPressure(options, callback) {
const startDate = !isNil(options.startDate) ? Date.parse(options.startDate) : (new Date()).setHours(0, 0, 0, 0)
const endDate = !isNil(options.endDate) ? Date.parse(options.endDate) : (new Date()).valueOf()
//const currentDate = !isNil(options.currentDate) ? Date.parse(options.currentDate) : (new Date()).setHours(0, 0, 0, 0)
if (!callback || typeof callback !== 'function') {
return new Promise((resolve, reject) => {
this._saveBloodPressure(startDate, endDate, (error, result) => {
if (!error) {
resolve(result)
} else {
reject(error)
}
})
})
}
this._saveBloodPressure(startDate, endDate, callback)
}
}
export default new RNGoogleFit()
// Possible Scopes
export const Scopes = Object.freeze(PossibleScopes)
//Data types for food addition
export const MealType = Object.freeze({
UNKNOWN: 0,
BREAKFAST: 1,
LUNCH: 2,
DINNER: 3,
SNACK: 4,
})
export const Nutrient = Object.freeze({
/**
* Calories in kcal
* @type {string}
*/
CALORIES: 'calories',
/**
* Total fat in grams.
* @type {string}
*/
TOTAL_FAT: 'fat.total',
/**
* Saturated fat in grams.
* @type {string}
*/
SATURATED_FAT: 'fat.saturated',
/**
* Unsaturated fat in grams.
* @type {string}
*/
UNSATURATED_FAT: 'fat.unsaturated',
/**
* Polyunsaturated fat in grams.
* @type {string}
*/
POLYUNSATURATED_FAT: 'fat.polyunsaturated',
/**
* Monounsaturated fat in grams.
* @type {string}
*/
MONOUNSATURATED_FAT: 'fat.monounsaturated',
/**
* Trans fat in grams.
* @type {string}
*/
TRANS_FAT: 'fat.trans',
/**
* Cholesterol in milligrams.
* @type {string}
*/
CHOLESTEROL: 'cholesterol',
/**
* Sodium in milligrams.
* @type {string}
*/
SODIUM: 'sodium',
/**
* Potassium in milligrams.
* @type {string}
*/
POTASSIUM: 'potassium',
/**
* Total carbohydrates in grams.
* @type {string}
*/
TOTAL_CARBS: 'carbs.total',
/**
* Dietary fiber in grams
* @type {string}
*/
DIETARY_FIBER: 'dietary_fiber',
/**
* Sugar amount in grams.
* @type {string}
*/
SUGAR: 'sugar',
/**
* Protein amount in grams.
* @type {string}
*/
PROTEIN: 'protein',
/**
* Vitamin A amount in International Units (IU).
* @type {string}
*/
VITAMIN_A: 'vitamin_a',
/**
* Vitamin C amount in milligrams.
* @type {string}
*/
VITAMIN_C: 'vitamin_c',
/**
* Calcium amount in milligrams.
* @type {string}
*/
CALCIUM: 'calcium',
/**
* Iron amount in milligrams
* @type {string}
*/
IRON: 'iron',
})
/*
TODO: Add food example to readme
same as here: https://developers.google.com/fit/scenarios/add-nutrition-data
import GoogleFit, {Nutrient, MealType, FoodIntake, WeightSample} from "react-native-google-fit";
...
addFoodExample(): Promise<void> {
return new Promise<void>((resolve, reject): void => {
const options = {
mealType: MealType.BREAKFAST,
foodName: "banana",
date: moment().format(), //equals to new Date().toISOString()
nutrients: {
[Nutrient.TOTAL_FAT]: 0.4,
[Nutrient.SODIUM]: 1,
[Nutrient.SATURATED_FAT]: 0.1,
[Nutrient.PROTEIN]: 1.3,
[Nutrient.TOTAL_CARBS]: 27.0,
[Nutrient.CHOLESTEROL]: 0,
[Nutrient.CALORIES]: 105,
[Nutrient.SUGAR]: 14,
[Nutrient.DIETARY_FIBER]: 3.1,
[Nutrient.POTASSIUM]: 422,
}
} as FoodIntake;
GoogleFit.saveFood(options, (err: boolean) => {
if (!err) {
resolve();
} else {
reject();
}
});
});
}
*/