Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is Background Processing for Expo Possible? #187

Open
AhmadHoranieh opened this issue Dec 1, 2021 · 18 comments
Open

Is Background Processing for Expo Possible? #187

AhmadHoranieh opened this issue Dec 1, 2021 · 18 comments
Labels
bug Something isn't working

Comments

@AhmadHoranieh
Copy link

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?

@AhmadHoranieh AhmadHoranieh added the bug Something isn't working label Dec 1, 2021
@jhbarnett
Copy link

jhbarnett commented Mar 24, 2022

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
Copy link

Any luck @jhbarnett jhbarnett??

@jhbarnett
Copy link

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
Copy link

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
Copy link
Contributor

jclif commented Nov 17, 2022

@jhbarnett How did everything work out in the end?

@jclif
Copy link
Contributor

jclif commented Feb 6, 2023

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

@chunghn
Copy link

chunghn commented May 12, 2023

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
Copy link

chunghn commented May 15, 2023

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

@swellander
Copy link

Has anyone had success with this?

@jclif
Copy link
Contributor

jclif commented Oct 25, 2023

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
Copy link

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

@samuthekid
Copy link

samuthekid commented Jan 5, 2024

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
Copy link

@samuthekid any success with fetching HK data in background?

@samuthekid
Copy link

samuthekid commented Jan 22, 2024

@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
Copy link

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

@brandon-austin-lark
Copy link

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

@garygcchiu
Copy link

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

@samuthekid
Copy link

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

I'm using it in a prod app and it's working great! I build my app in EAS and I don't have any problems

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

There's no delay... you just get the values when the iOS triggers a broadcast of the new values. From my tests, it usually triggers when the user unlocks the phone a couple minutes after new values have been added to the Health app

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

10 participants