Skip to content

Commit

Permalink
fix(expo-config-plugin): rework expo-config-plugin to fix lateinit
Browse files Browse the repository at this point in the history
…error matinzd#71
  • Loading branch information
xseignard committed Mar 25, 2024
1 parent 3784cc3 commit ea0f5e6
Show file tree
Hide file tree
Showing 6 changed files with 444 additions and 36 deletions.
23 changes: 16 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ This library is a wrapper around Health Connect for react native. Health Connect

- [Health Connect](https://play.google.com/store/apps/details?id=com.google.android.apps.healthdata&hl=en&gl=US) needs to be installed on the user's device. Starting from Android 14 (Upside Down Cake), Health Connect is part of the Android Framework. Read more [here](https://developer.android.com/health-and-fitness/guides/health-connect/develop/get-started#step-1).
- Health Connect API requires `minSdkVersion=26` (Android Oreo / 8.0).
- If you are planning to release your app on Google Play, you will need to submit a [declaration form](https://docs.google.com/forms/d/1LFjbq1MOCZySpP5eIVkoyzXTanpcGTYQH26lKcrQUJo/viewform?edit_requested=true).
- If you are planning to release your app on Google Play, you will need to submit a [declaration form](https://docs.google.com/forms/d/1LFjbq1MOCZySpP5eIVkoyzXTanpcGTYQH26lKcrQUJo/viewform?edit_requested=true).

## Installation

Expand All @@ -31,7 +31,7 @@ To install react-native-health-connect, use the following command:
yarn add react-native-health-connect
```

For version 2 onwards, please add the following code into your `MainActivity.kt` within the `onCreate` method:
For version 2 onwards, please add the following code into your `MainActivity.kt` within the `onCreate` method (not needed for Expo projects, see below for Expo installation):

```diff
package com.healthconnectexample
Expand Down Expand Up @@ -66,7 +66,6 @@ class MainActivity : ReactActivity() {

```


## Expo installation

This package cannot be used in the [Expo Go](https://expo.io/client) app, but it can be used with custom managed apps.
Expand All @@ -80,11 +79,19 @@ expo install react-native-health-connect

Then add the prebuild [config plugin](https://docs.expo.io/guides/config-plugins/) to the [`plugins`](https://docs.expo.io/versions/latest/config/app/#plugins) array of your `app.json` or `app.config.js`:

```json
```json5
{
"expo": {
"plugins": ["react-native-health-connect"]
}
expo: {
plugins: [
[
'react-native-health-connect',
{
mainActivityLanguage: 'java', // or "kotlin"
permissionsRationaleActivityPath: 'PermissionRationaleActivity.kt', // path relative to your package.json
},
],
],
},
}
```

Expand All @@ -111,6 +118,8 @@ Then add the prebuild [config plugin](https://docs.expo.io/guides/config-plugins
}
```

- Add the needed permissions: see [app.json / app.config.js permissions](https://docs.expo.dev/versions/latest/config/app/#permissions)

Then rebuild the native app:

- Run `expo prebuild`
Expand Down
22 changes: 1 addition & 21 deletions app.plugin.js
Original file line number Diff line number Diff line change
@@ -1,21 +1 @@
const { withAndroidManifest } = require('@expo/config-plugins');

const withHealthConnect = function androidManifestPlugin(config) {
return withAndroidManifest(config, async (config) => {
let androidManifest = config.modResults.manifest;

androidManifest.application[0].activity[0]['intent-filter'].push({
action: [
{
$: {
'android:name': 'androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE',
},
},
],
});

return config;
});
};

module.exports = withHealthConnect;
module.exports = require('./lib/commonjs/expo-plugin/withHealthConnect');
17 changes: 13 additions & 4 deletions docs/docs/get-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ title: Get started

- [Health Connect](https://play.google.com/store/apps/details?id=com.google.android.apps.healthdata&hl=en&gl=US) needs to be installed on the user's device. Starting from Android 14 (Upside Down Cake), Health Connect is part of the Android Framework. Read more [here](https://developer.android.com/health-and-fitness/guides/health-connect/develop/get-started#step-1).
- Health Connect API requires `minSdkVersion=26` (Android Oreo / 8.0).
- If you are planning to release your app on Google Play, you will need to submit a [declaration form](https://docs.google.com/forms/d/1LFjbq1MOCZySpP5eIVkoyzXTanpcGTYQH26lKcrQUJo/viewform?edit_requested=true).
- If you are planning to release your app on Google Play, you will need to submit a [declaration form](https://docs.google.com/forms/d/1LFjbq1MOCZySpP5eIVkoyzXTanpcGTYQH26lKcrQUJo/viewform?edit_requested=true).

:::note
Health Connect does not appear on the Home screen by default. To open Health Connect, go to Settings > Apps > Health Connect, or add Health Connect to your Quick Settings menu.
Expand All @@ -25,7 +25,7 @@ To install react-native-health-connect, use the following command:
yarn add react-native-health-connect
```

For version 2 onwards, please add the following code into your `MainActivity.kt` within the `onCreate` method:
For version 2 onwards, please add the following code into your `MainActivity.kt` within the `onCreate` method (not needed for Expo projects, see below for Expo installation):

```diff
package com.healthconnectexample
Expand Down Expand Up @@ -60,7 +60,6 @@ class MainActivity : ReactActivity() {

```


## Expo installation

This package cannot be used in the [Expo Go](https://expo.io/client) app, but it can be used with custom managed apps.
Expand All @@ -77,7 +76,15 @@ Then add the prebuild [config plugin](https://docs.expo.io/guides/config-plugins
```json
{
"expo": {
"plugins": ["react-native-health-connect"]
"plugins": [
[
"react-native-health-connect",
{
"mainActivityLanguage": "java", // or "kotlin"
"permissionsRationaleActivityPath": "PermissionRationaleActivity.kt" // path relative to your package.json
}
]
]
}
}
```
Expand Down Expand Up @@ -105,6 +112,8 @@ Then add the prebuild [config plugin](https://docs.expo.io/guides/config-plugins
}
```

- Add the needed permissions: see [app.json / app.config.js permissions](https://docs.expo.dev/versions/latest/config/app/#permissions)

Then rebuild the native app:

- Run `expo prebuild`
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"devDependencies": {
"@commitlint/config-conventional": "^17.4.2",
"@evilmartians/lefthook": "^1.2.8",
"@expo/config-plugins": "7.8.4",
"@react-native-community/eslint-config": "^3.0.2",
"@release-it/conventional-changelog": "^5.0.0",
"@types/jest": "^28.1.2",
Expand Down
189 changes: 189 additions & 0 deletions src/expo-plugin/withHealthConnect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/* eslint-disable @typescript-eslint/no-shadow */
import {
AndroidConfig,
ConfigPlugin,
createRunOncePlugin,
withAndroidManifest,
withDangerousMod,
withMainActivity,
} from '@expo/config-plugins';
import { mergeContents } from '@expo/config-plugins/build/utils/generateCode';
import { promises as fs } from 'node:fs';
import path from 'node:path';

const pkg = require('../../../package.json');

const { getMainApplicationOrThrow } = AndroidConfig.Manifest;

type MainActivityLanguage = 'java' | 'kotlin';

type PluginProps = {
permissionsRationaleActivityPath: string;
providerPackageName?: string;
mainActivityLanguage: MainActivityLanguage;
};

const MAIN_ACTIVITY_CHANGES: Record<
MainActivityLanguage,
{ code: string[]; anchor: RegExp; offset: number; tag: string }[]
> = {
java: [
{
code: [
'import dev.matinzd.healthconnect.permissions.HealthConnectPermissionDelegate;',
],
anchor:
/import com\.facebook\.react\.defaults\.DefaultReactActivityDelegate;/,
offset: 1,
tag: 'react-native-health-connect import',
},
{
code: [
'HealthConnectPermissionDelegate hcpd = HealthConnectPermissionDelegate.INSTANCE;',
'hcpd.setPermissionDelegate(this, "{{providerPackageName}}");',
],
anchor: /super\.onCreate\(\w+\);/,
offset: 1,
tag: 'react-native-health-connect onCreate',
},
],
kotlin: [
{
code: [
'import dev.matinzd.healthconnect.permissions.HealthConnectPermissionDelegate',
],
anchor:
/import com\.facebook\.react\.defaults\.DefaultReactActivityDelegate/,
offset: 1,
tag: 'react-native-health-connect import',
},
{
code: [
'HealthConnectPermissionDelegate.setPermissionDelegate(this, "{{providerPackageName}}")',
],
anchor: /super\.onCreate\(\w+\)/,
offset: 1,
tag: 'react-native-health-connect onCreate',
},
],
};

const withHealthConnect: ConfigPlugin<PluginProps> = (
config,
{
mainActivityLanguage,
providerPackageName = 'com.google.android.apps.healthdata',
permissionsRationaleActivityPath,
}
) => {
// 1 - Add the permissions rationale activity to the AndroidManifest.xml
config = withAndroidManifest(config, async (config) => {
const application = getMainApplicationOrThrow(config.modResults);
// Ensure activities array exists
if (!application.activity) application.activity = [];
const activities = application.activity;

const permissionsRationaleActivityName = path
.basename(permissionsRationaleActivityPath)
.split('.')[0];

// For supported versions through Android 13, create an activity to show the rationale
// of Health Connect permissions once users click the privacy policy link.
activities.push({
'$': {
'android:name': `.${permissionsRationaleActivityName}`,
'android:exported': 'true',
},
'intent-filter': [
{
action: [
{
$: {
'android:name':
'androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE',
},
},
],
},
],
});

// For versions starting Android 14, create an activity alias to show the rationale
// of Health Connect permissions once users click the privacy policy link.
// @ts-expect-error activity-alias is not defined in the type
if (!application['activity-alias']) application['activity-alias'] = [];
// @ts-expect-error activity-alias is not defined in the type
const activityAliases = application['activity-alias'];
activityAliases.push({
'$': {
'android:name': 'ViewPermissionUsageActivity',
'android:exported': 'true',
'android:targetActivity': `.${permissionsRationaleActivityName}`,
'android:permission': 'android.permission.START_VIEW_PERMISSION_USAGE',
},
'intent-filter': [
{
action: [
{
$: {
'android:name': 'android.intent.action.VIEW_PERMISSION_USAGE',
},
},
],
category: [
{
$: {
'android:name': 'android.intent.category.HEALTH_PERMISSIONS',
},
},
],
},
],
});

return config;
});

// 2 - Add the HealthConnectPermissionDelegate to the MainActivity
config = withMainActivity(config, (config) => {
const changes = MAIN_ACTIVITY_CHANGES[mainActivityLanguage];
changes.forEach((change) => {
config.modResults.contents = mergeContents({
tag: change.tag,
src: config.modResults.contents,
newSrc: change.code
.map((code) => {
return code.replace('{{providerPackageName}}', providerPackageName);
})
.join('\n'),
anchor: change.anchor,
offset: change.offset,
comment: '//',
}).contents;
});
return config;
});

// 3 - Copy the PermissionsRationaleActivity.[java|kt] file to the Android project
config = withDangerousMod(config, [
'android',
async (config) => {
const packageName = config.android?.package;
if (!packageName) throw new Error('No package id found');
const projectRoot = config.modRequest.projectRoot;
const destPath = path.resolve(
projectRoot,
`android/app/src/main/java/${packageName.split('.').join('/')}`
);
const fileName = path.basename(permissionsRationaleActivityPath);
const buf = await fs.readFile(permissionsRationaleActivityPath);
const content = buf.toString().replace('{{pkg}}', packageName);
await fs.writeFile(path.resolve(destPath, fileName), content);
return config;
},
]);

return config;
};

export default createRunOncePlugin(withHealthConnect, pkg.name, pkg.version);
Loading

0 comments on commit ea0f5e6

Please sign in to comment.