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

fix(expo-config-plugin): rework expo-config-plugin to fix lateinit error #77

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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',
},
],
};
matinzd marked this conversation as resolved.
Show resolved Hide resolved

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) => {
matinzd marked this conversation as resolved.
Show resolved Hide resolved
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, [
matinzd marked this conversation as resolved.
Show resolved Hide resolved
'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
Loading