agencyenterprise / react-native-health

A React Native package to interact with Apple HealthKit
MIT License
796 stars 226 forks source link

Is Background Processing for Expo Possible? #187

Open AhmadHoranieh opened 2 years ago

AhmadHoranieh commented 2 years ago

I am interested in implementing a background listener for steps and active calories burnt as it provides a better user experience in my Expo application (preferably without ejection). Is it possible?

jhbarnett commented 2 years ago

Attempting to do the same with the config plugin below. Will report back whether I'm successful or not.

const {
  withAppDelegate,
  withEntitlementsPlist
} = require('@expo/config-plugins')
const {
  mergeContents
} = require('@expo/config-plugins/build/utils/generateCode')

const RN_HEALTH_IMPORT = '#import "RCTAppleHealthKit.h"'

const RN_HEALTH_INITIALIZE =
  '[[RCTAppleHealthKit new] initializeBackgroundObservers:bridge];'

function addImport(src) {
  const newSrc = [RN_HEALTH_IMPORT]
  return mergeContents({
    tag: 'healthkit-import',
    src,
    newSrc: newSrc.join('\n'),
    anchor: /#import "AppDelegate\.h"/,
    offset: 1,
    comment: '//'
  })
}

function addInit(src) {
  const newSrc = [RN_HEALTH_INITIALIZE]
  return mergeContents({
    tag: 'healthkit-init',
    src,
    newSrc: newSrc.join('\n'),
    anchor: /UIViewController/,
    offset: -1,
    comment: '//'
  })
}

const withHealthKitObservers = (config, options = {}) => {
  config = withAppDelegate(config, (config) => {
    if (config.modResults.language === 'objc') {
      config.modResults.contents = addImport(
        config.modResults.contents
      ).contents
      config.modResults.contents = addInit(config.modResults.contents).contents
    } else {
      WarningAggregator.addWarningIOS(
        'withHealthKitObservers',
        'Swift AppDelegate files are not supported yet.'
      )
    }
    return config
  })

  config = withEntitlementsPlist(config, (config) => {
    config.modResults[
      'com.apple.developer.healthkit.background-delivery'
    ] = true

    return config
  })

  return config
}

module.exports = withHealthKitObservers
brianfoody commented 2 years ago

Any luck @jhbarnett jhbarnett??

jhbarnett commented 2 years ago

Yeah @brianfoody it's working 90% great. We're still trying to pinpoint an issue with background delivery when the app is in a killed state, but near as we can tell this plugin is making all the right mods.

jhbarnett commented 2 years ago

For anyone else that stumbles upon this, here are the steps to integrate in your own project. I'll explore a PR here once our app has shipped.

jclif commented 1 year ago

@jhbarnett How did everything work out in the end?

jclif commented 1 year ago

@brianfoody @AhmadHoranieh Y'all had any success with this?

chunghn commented 1 year ago

Attempting to do the same with the config plugin below. Will report back whether I'm successful or not.

const {
  withAppDelegate,
  withEntitlementsPlist
} = require('@expo/config-plugins')
const {
  mergeContents
} = require('@expo/config-plugins/build/utils/generateCode')

const RN_HEALTH_IMPORT = '#import "RCTAppleHealthKit.h"'

const RN_HEALTH_INITIALIZE =
  '[[RCTAppleHealthKit new] initializeBackgroundObservers:bridge];'

function addImport(src) {
  const newSrc = [RN_HEALTH_IMPORT]
  return mergeContents({
    tag: 'healthkit-import',
    src,
    newSrc: newSrc.join('\n'),
    anchor: /#import "AppDelegate\.h"/,
    offset: 1,
    comment: '//'
  })
}

function addInit(src) {
  const newSrc = [RN_HEALTH_INITIALIZE]
  return mergeContents({
    tag: 'healthkit-init',
    src,
    newSrc: newSrc.join('\n'),
    anchor: /UIViewController/,
    offset: -1,
    comment: '//'
  })
}

const withHealthKitObservers = (config, options = {}) => {
  config = withAppDelegate(config, (config) => {
    if (config.modResults.language === 'objc') {
      config.modResults.contents = addImport(
        config.modResults.contents
      ).contents
      config.modResults.contents = addInit(config.modResults.contents).contents
    } else {
      WarningAggregator.addWarningIOS(
        'withHealthKitObservers',
        'Swift AppDelegate files are not supported yet.'
      )
    }
    return config
  })

  config = withEntitlementsPlist(config, (config) => {
    config.modResults[
      'com.apple.developer.healthkit.background-delivery'
    ] = true

    return config
  })

  return config
}

module.exports = withHealthKitObservers

For anyone who has build issue with eas build with the above config. WarningAggregator is not imported.

Just do

const {
  withAppDelegate,
  withEntitlementsPlist,
  WarningAggregator,
} = require("@expo/config-plugins")

You will be fine.

chunghn commented 1 year ago

Update: I cannot make the above code work, even I updated the AppDelegate.mm to be the same as it should be.

swellander commented 8 months ago

Has anyone had success with this?

jclif commented 8 months ago

I was able to query the health records in a location-based task, but the problem is that the health records are encrypted when the device is locked. An ideal solution would allow for a bridge to be set up within Expo that allows us to subscribe to new workouts, but I don't think there is a way to do this, currently.

samuthekid commented 6 months ago

@jclif @jhbarnett Any success with this? It would be great to be able to fetch the HK data in the background :)

samuthekid commented 5 months ago
const {
  withAppDelegate,
  withEntitlementsPlist,
  withInfoPlist,
} = require('@expo/config-plugins')
const {
  mergeContents
} = require('@expo/config-plugins/build/utils/generateCode')

const RN_HEALTH_IMPORT = '#import "RCTAppleHealthKit.h"'
const RN_HEALTH_BRIDGE = '  RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];'
const RN_HEALTH_INITIALIZE = '  [[RCTAppleHealthKit new] initializeBackgroundObservers:bridge];'

const HEALTH_SHARE = 'Allow $(PRODUCT_NAME) to check health info'
const HEALTH_UPDATE = 'Allow $(PRODUCT_NAME) to update health info'
const HEALTH_CLINIC_SHARE = 'Allow $(PRODUCT_NAME) to check health clinical info'

function addImport(src) {
  const newSrc = [RN_HEALTH_IMPORT]
  return mergeContents({
    tag: 'healthkit-import',
    src,
    newSrc: newSrc.join('\n'),
    anchor: /#import "AppDelegate\.h"/,
    offset: 1,
    comment: '//'
  })
}

function addInit(src) {
  const newSrc = [RN_HEALTH_BRIDGE, RN_HEALTH_INITIALIZE]
  return mergeContents({
    tag: 'healthkit-init',
    src,
    newSrc: newSrc.join('\n'),
    anchor: /self.initialProps = @{};/,
    offset: 1,
    comment: '  //'
  })
}

const withHealthKit = (
  config,
  { healthSharePermission, healthUpdatePermission, isClinicalDataEnabled, healthClinicalDescription } = {},
) => {
  // Add import
  config = withAppDelegate(config, (config) => {
    if (config.modResults.language === 'objcpp') {
      config.modResults.contents = addImport(config.modResults.contents).contents
      config.modResults.contents = addInit(config.modResults.contents).contents
    }
    return config
  })

  // Add permissions
  config = withInfoPlist(config, (config) => {
    config.modResults.NSHealthShareUsageDescription =
      healthSharePermission ||
      config.modResults.NSHealthShareUsageDescription ||
      HEALTH_SHARE
    config.modResults.NSHealthUpdateUsageDescription =
      healthUpdatePermission ||
      config.modResults.NSHealthUpdateUsageDescription ||
      HEALTH_UPDATE
    isClinicalDataEnabled ?
      config.modResults.NSHealthClinicalHealthRecordsShareUsageDescription =
        healthClinicalDescription ||
        config.modResults.NSHealthClinicalHealthRecordsShareUsageDescription ||
        HEALTH_CLINIC_SHARE :
      null

    return config
  })

  // Add entitlements. These are automatically synced when using EAS build for production apps.
  config = withEntitlementsPlist(config, (config) => {
    config.modResults['com.apple.developer.healthkit'] = true
    config.modResults['com.apple.developer.healthkit.background-delivery'] = true
    if (
      !Array.isArray(config.modResults['com.apple.developer.healthkit.access'])
    ) {
      config.modResults['com.apple.developer.healthkit.access'] = []
    }

    if (isClinicalDataEnabled) {
      config.modResults['com.apple.developer.healthkit.access'].push(
        'health-records',
      )

      // Remove duplicates
      config.modResults['com.apple.developer.healthkit.access'] = [
        ...new Set(config.modResults['com.apple.developer.healthkit.access']),
      ]
    }

    return config
  })

  return config
}
module.exports = withHealthKit

which modifies the AppDelegate.mm to the following:

#import "AppDelegate.h"
// @generated begin healthkit-import - expo prebuild (DO NOT MODIFY) sync-283e92dde8b719d45e241f992dc4e0cceb751e1e
#import "RCTAppleHealthKit.h"
// @generated end healthkit-import

#import <React/RCTBundleURLProvider.h>
#import <React/RCTLinkingManager.h>

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  self.moduleName = @"main";

  // You can add your custom initial props in the dictionary below.
  // They will be passed down to the ViewController used by React Native.
  self.initialProps = @{};
  // @generated begin healthkit-init - expo prebuild (DO NOT MODIFY) sync-f4def85acdf20e650ea4209ec70a05de7e0ebb70
  RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];
  [[RCTAppleHealthKit new] initializeBackgroundObservers:bridge];
  // @generated end healthkit-init

  return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

I'm still testing if this works, but I think I'm in the right path! PS: I'm using the latest version of this lib and Expo 49

rickyhonline commented 5 months ago

@samuthekid any success with fetching HK data in background?

samuthekid commented 5 months ago

@rickyhonline I didn't leave any feedback, but yeah! It works very well!

Right now, I'm using patch-package for this, so here's my patch:

diff --git a/node_modules/react-native-health/app.plugin.js b/node_modules/react-native-health/app.plugin.js
index 8dbb66a..b003319 100644
--- a/node_modules/react-native-health/app.plugin.js
+++ b/node_modules/react-native-health/app.plugin.js
@@ -1,13 +1,57 @@
-const { withEntitlementsPlist, withInfoPlist } = require('@expo/config-plugins')
+const {
+  withAppDelegate,
+  withEntitlementsPlist,
+  withInfoPlist,
+} = require('@expo/config-plugins')
+const {
+  mergeContents
+} = require('@expo/config-plugins/build/utils/generateCode')
+
+const RN_HEALTH_IMPORT = '#import "RCTAppleHealthKit.h"'
+const RN_HEALTH_BRIDGE = '  RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];'
+const RN_HEALTH_INITIALIZE = '  [[RCTAppleHealthKit new] initializeBackgroundObservers:bridge];'

 const HEALTH_SHARE = 'Allow $(PRODUCT_NAME) to check health info'
 const HEALTH_UPDATE = 'Allow $(PRODUCT_NAME) to update health info'
 const HEALTH_CLINIC_SHARE = 'Allow $(PRODUCT_NAME) to check health clinical info'

+function addImport(src) {
+  const newSrc = [RN_HEALTH_IMPORT]
+  return mergeContents({
+    tag: 'healthkit-import',
+    src,
+    newSrc: newSrc.join('\n'),
+    anchor: /#import "AppDelegate\.h"/,
+    offset: 1,
+    comment: '//'
+  })
+}
+
+function addInit(src) {
+  const newSrc = [RN_HEALTH_BRIDGE, RN_HEALTH_INITIALIZE]
+  return mergeContents({
+    tag: 'healthkit-init',
+    src,
+    newSrc: newSrc.join('\n'),
+    anchor: /self.initialProps = @{};/,
+    offset: 1,
+    comment: '  //'
+  })
+}
+
 const withHealthKit = (
   config,
   { healthSharePermission, healthUpdatePermission, isClinicalDataEnabled, healthClinicalDescription } = {},
 ) => {
+  // Add import
+  config = withAppDelegate(config, (config) => {
+    if (config.modResults.language === 'objcpp') {
+      config.modResults.contents = addImport(config.modResults.contents).contents
+      config.modResults.contents = addInit(config.modResults.contents).contents
+    }
+    return config
+  })
+
   // Add permissions
   config = withInfoPlist(config, (config) => {
     config.modResults.NSHealthShareUsageDescription =
@@ -31,6 +75,7 @@ const withHealthKit = (
   // Add entitlements. These are automatically synced when using EAS build for production apps.
   config = withEntitlementsPlist(config, (config) => {
     config.modResults['com.apple.developer.healthkit'] = true
+    config.modResults['com.apple.developer.healthkit.background-delivery'] = true
     if (
       !Array.isArray(config.modResults['com.apple.developer.healthkit.access'])
     ) {

which is located in the root folder like this: image

samuthekid commented 5 months ago

I think the owners could make a little update with this new version of the expo plugin ;)

brandon-austin-lark commented 1 month ago

Does anyone have experience with this implementation? Does it work for your managed expo app?

garygcchiu commented 2 weeks ago

What's the delay like for receiving data from the background listeners? I.e. how long it takes before the callback is called and data is received