-
-
Notifications
You must be signed in to change notification settings - Fork 43
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
lateinit property requestPermission has not been initialized #71
Comments
+1 |
I confirm the bug too |
This is not a bug. You have to do additional configuration in your MainActivity file which is not available with Expo. https://github.com/matinzd/react-native-health-connect/releases/tag/v2.0.0 Try to rebuild your Android folder with dev client and add these configurations manually to MainActivity. |
Hey @matinzd , indeed I created my own custom expo config plugin for that inspired by #70: import { ConfigPlugin, withAndroidManifest, withDangerousMod, withMainActivity } from "@expo/config-plugins";
import path from "node:path";
import { promises as fs } from "node:fs";
type InsertProps = {
/** The content to look at */
content: string;
/** The string to find within `content` */
toFind: string;
/** What to insert in the `content`, be it a single string, or an array of string that would be separated by a `\n` */
toInsert: string | string[];
/** A tag that will be used to keep track of which `expo-plugin-config` introduced the modification */
tag: string;
/** The symbol(s) to be used to begin a comment in the given `content`. If an array, the first item will be used to start the comment, the second to end it */
commentSymbol: string | [string, string];
};
const createCommentSymbols = (commentSymbol: InsertProps["commentSymbol"]) => {
return {
startCommentSymbol: Array.isArray(commentSymbol) ? commentSymbol[0] : commentSymbol,
endCommentSymbol: Array.isArray(commentSymbol) ? ` ${commentSymbol[1]}` : "",
};
};
const createStartTag = (
commentSymbol: InsertProps["commentSymbol"],
tag: InsertProps["tag"],
toInsert: InsertProps["toInsert"],
) => {
const { startCommentSymbol, endCommentSymbol } = createCommentSymbols(commentSymbol);
return `${startCommentSymbol} @generated begin ${tag} - expo prebuild (DO NOT MODIFY)${endCommentSymbol}`;
};
const createEndTag = (commentSymbol: InsertProps["commentSymbol"], tag: InsertProps["tag"]) => {
const { startCommentSymbol, endCommentSymbol } = createCommentSymbols(commentSymbol);
return `${startCommentSymbol} @generated end ${tag}${endCommentSymbol}`;
};
const createContentToInsert = (
commentSymbol: InsertProps["commentSymbol"],
tag: InsertProps["tag"],
toInsert: InsertProps["toInsert"],
) => {
const startTag = createStartTag(commentSymbol, tag, toInsert);
const endTag = createEndTag(commentSymbol, tag);
return `${startTag}\n${Array.isArray(toInsert) ? toInsert.join("\n") : toInsert}\n${endTag}`;
};
const insert = ({
content,
toFind,
toInsert,
tag,
commentSymbol,
where,
}: InsertProps & {
where: "before" | "after" | "replace";
}): string => {
const toInsertWithComments = createContentToInsert(commentSymbol, tag, toInsert);
if (!content.includes(toFind)) {
throw new Error(`Couldn't find ${toFind} in the given props.content`);
}
if (!content.includes(toInsertWithComments)) {
switch (where) {
case "before":
content = content.replace(toFind, `${toInsertWithComments}\n${toFind}`);
break;
case "after":
content = content.replace(toFind, `${toFind}\n${toInsertWithComments}`);
break;
case "replace":
content = content.replace(toFind, `${toInsertWithComments}`);
break;
}
}
return content;
};
/**
* Insert `props.toInsert` into `props.content` the line after `props.toFind`
* @returns the modified `props.content`
*/
export const insertAfter = (props: InsertProps) => {
return insert({ ...props, where: "after" });
};
/**
* Insert `props.toInsert` into `props.content` the line before `props.toFind`
* @returns the modified `props.content`
*/
export const insertBefore = (props: InsertProps) => {
return insert({ ...props, where: "before" });
};
/**
* Replace `props.toFind` by `props.toInsert` into `props.content`
* @returns the modified `props.content`
*/
export const replace = (props: InsertProps) => {
return insert({ ...props, where: "replace" });
};
/** Copies `srcFile` to `destFolder` with an optional `destFileName` or its initial name if not provided
* @returns the path of the created file
*/
const copyFile = async (srcFile: string, destFolder: string, destFileName?: string) => {
const fileName = destFileName ?? path.basename(srcFile);
await fs.mkdir(destFolder, { recursive: true });
const destFile = path.resolve(destFolder, fileName);
await fs.copyFile(srcFile, destFile);
return destFile;
};
const withReactNativeHealthConnect: ConfigPlugin<{
permissionsRationaleActivityPath: string;
}> = (config, { permissionsRationaleActivityPath }) => {
config = withAndroidManifest(config, async (config) => {
const androidManifest = config.modResults.manifest;
if (!androidManifest?.application?.[0]) {
throw new Error("AndroidManifest.xml is not valid!");
}
if (!androidManifest.application[0]["activity"]) {
throw new Error("AndroidManifest.xml is missing application activity");
}
androidManifest.application[0]["activity"].push({
$: {
"android:name": ".PermissionsRationaleActivity",
"android:exported": "true",
},
"intent-filter": [
{
action: [{ $: { "android:name": "androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" } }],
},
],
});
// @ts-expect-error activity-alias is not defined in the type
if (!androidManifest.application[0]["activity-alias"]) {
// @ts-expect-error activity-alias is not defined in the type
androidManifest.application[0]["activity-alias"] = [];
}
// @ts-expect-error activity-alias is not defined in the type
androidManifest.application[0]["activity-alias"].push({
$: {
"android:name": "ViewPermissionUsageActivity",
"android:exported": "true",
"android:targetActivity": ".PermissionsRationaleActivity",
"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;
});
config = withMainActivity(config, async (config) => {
config.modResults.contents = insertAfter({
content: config.modResults.contents,
toFind: "import com.facebook.react.defaults.DefaultReactActivityDelegate;",
toInsert: "import dev.matinzd.healthconnect.permissions.HealthConnectPermissionDelegate;",
commentSymbol: "//",
tag: "withReactNativeHealthConnect",
});
config.modResults.contents = insertAfter({
content: config.modResults.contents,
toFind: "super.onCreate(null);",
toInsert: [
"HealthConnectPermissionDelegate hcpd = HealthConnectPermissionDelegate.INSTANCE;",
'hcpd.setPermissionDelegate(this, "com.google.android.apps.healthdata");',
],
commentSymbol: "//",
tag: "withReactNativeHealthConnect",
});
return config;
});
config = withDangerousMod(config, [
"android",
async (config) => {
const projectRoot = config.modRequest.projectRoot;
const destPath = path.resolve(projectRoot, "android/app/src/main/java/com/alanmobile");
await copyFile(permissionsRationaleActivityPath, destPath, "PermissionsRationaleActivity.kt");
return config;
},
]);
return config;
};
export default withReactNativeHealthConnect; Should be straightforward to update the expo config plugin in this repo from this one |
Does this plugin still fix the issue ? However, when it comes to build the app, I caught this error : Cannot read properties of undefined (reading 'permissionsRationaleActivityPath') |
My MainActivity is written in Kotlin but not Java, so I've customized the plugin. // https://github.com/matinzd/react-native-health-connect/issues/71#issuecomment-1986791229
import { promises as fs } from 'node:fs';
import * as path from 'node:path';
import { ConfigPlugin, withAndroidManifest, withDangerousMod, withMainActivity } from '@expo/config-plugins';
type InsertProps = {
/** The content to look at */
content: string;
/** The string to find within `content` */
toFind: string;
/** What to insert in the `content`, be it a single string, or an array of string that would be separated by a `\n` */
toInsert: string | string[];
/** A tag that will be used to keep track of which `expo-plugin-config` introduced the modification */
tag: string;
/** The symbol(s) to be used to begin a comment in the given `content`. If an array, the first item will be used to start the comment, the second to end it */
commentSymbol: string | [string, string];
};
const createCommentSymbols = (commentSymbol: InsertProps['commentSymbol']) => {
return {
startCommentSymbol: Array.isArray(commentSymbol) ? commentSymbol[0] : commentSymbol,
endCommentSymbol: Array.isArray(commentSymbol) ? ` ${commentSymbol[1]}` : '',
};
};
const createStartTag = (
commentSymbol: InsertProps['commentSymbol'],
tag: InsertProps['tag'],
_: InsertProps['toInsert'],
) => {
const { startCommentSymbol, endCommentSymbol } = createCommentSymbols(commentSymbol);
return `${startCommentSymbol} @generated begin ${tag} - expo prebuild (DO NOT MODIFY)${endCommentSymbol}`;
};
const createEndTag = (commentSymbol: InsertProps['commentSymbol'], tag: InsertProps['tag']) => {
const { startCommentSymbol, endCommentSymbol } = createCommentSymbols(commentSymbol);
return `${startCommentSymbol} @generated end ${tag}${endCommentSymbol}`;
};
const createContentToInsert = (
commentSymbol: InsertProps['commentSymbol'],
tag: InsertProps['tag'],
toInsert: InsertProps['toInsert'],
) => {
const startTag = createStartTag(commentSymbol, tag, toInsert);
const endTag = createEndTag(commentSymbol, tag);
return `${startTag}\n${Array.isArray(toInsert) ? toInsert.join('\n') : toInsert}\n${endTag}`;
};
const insert = ({
content,
toFind,
toInsert,
tag,
commentSymbol,
where,
}: InsertProps & {
where: 'before' | 'after' | 'replace';
}): string => {
const toInsertWithComments = createContentToInsert(commentSymbol, tag, toInsert);
if (!content.includes(toFind)) {
throw new Error(`Couldn't find ${toFind} in the given props.content`);
}
if (!content.includes(toInsertWithComments)) {
switch (where) {
case 'before':
content = content.replace(toFind, `${toInsertWithComments}\n${toFind}`);
break;
case 'after':
content = content.replace(toFind, `${toFind}\n${toInsertWithComments}`);
break;
case 'replace':
content = content.replace(toFind, `${toInsertWithComments}`);
break;
}
}
return content;
};
/**
* Insert `props.toInsert` into `props.content` the line after `props.toFind`
* @returns the modified `props.content`
*/
export const insertAfter = (props: InsertProps) => {
return insert({ ...props, where: 'after' });
};
/**
* Insert `props.toInsert` into `props.content` the line before `props.toFind`
* @returns the modified `props.content`
*/
export const insertBefore = (props: InsertProps) => {
return insert({ ...props, where: 'before' });
};
/**
* Replace `props.toFind` by `props.toInsert` into `props.content`
* @returns the modified `props.content`
*/
export const replace = (props: InsertProps) => {
return insert({ ...props, where: 'replace' });
};
/** Copies `srcFile` to `destFolder` with an optional `destFileName` or its initial name if not provided
* @returns the path of the created file
*/
const copyFile = async (srcFile: string, destFolder: string, packageName: string) => {
const fileName = path.basename(srcFile);
await fs.mkdir(destFolder, { recursive: true });
const destFile = path.resolve(destFolder, fileName);
const buf = await fs.readFile(srcFile);
const content = buf.toString().replace('{pkg}', packageName);
await fs.writeFile(destFile, content);
return destFile;
};
const withReactNativeHealthConnect: ConfigPlugin = (config) => {
config = withAndroidManifest(config, async (config) => {
const androidManifest = config.modResults.manifest;
if (!androidManifest?.application?.[0]) {
throw new Error('AndroidManifest.xml is not valid!');
}
if (!androidManifest.application[0]['activity']) {
throw new Error('AndroidManifest.xml is missing application activity');
}
// for Android 13
androidManifest.application[0]['activity'].push({
$: {
'android:name': '.PermissionsRationaleActivity',
'android:exported': 'true',
},
'intent-filter': [
{
action: [{ $: { 'android:name': 'androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE' } }],
},
],
});
// for Android 14
// @ts-expect-error activity-alias is not defined in the type
if (!androidManifest.application[0]['activity-alias']) {
// @ts-expect-error activity-alias is not defined in the type
androidManifest.application[0]['activity-alias'] = [];
}
// @ts-expect-error activity-alias is not defined in the type
androidManifest.application[0]['activity-alias'].push({
$: {
'android:name': 'ViewPermissionUsageActivity',
'android:exported': 'true',
'android:targetActivity': '.PermissionsRationaleActivity',
'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;
});
config = withMainActivity(config, async (config) => {
config.modResults.contents = insertAfter({
content: config.modResults.contents,
toFind: 'import com.facebook.react.defaults.DefaultReactActivityDelegate',
toInsert: 'import dev.matinzd.healthconnect.permissions.HealthConnectPermissionDelegate',
commentSymbol: '//',
tag: 'withReactNativeHealthConnect',
});
config.modResults.contents = replace({
content: config.modResults.contents,
toFind: 'super.onCreate(null)',
toInsert: ['super.onCreate(savedInstanceState)', 'HealthConnectPermissionDelegate.setPermissionDelegate(this)'],
commentSymbol: '//',
tag: 'withReactNativeHealthConnect',
});
return config;
});
config = withDangerousMod(config, [
'android',
async (config) => {
const pkg = config.android?.package;
if (!pkg) {
throw new Error('no package id');
}
const projectRoot = config.modRequest.projectRoot;
const destPath = path.resolve(projectRoot, `android/app/src/main/java/${pkg.split('.').join('/')}`);
await copyFile(__dirname + '/PermissionRationaleActivity.kt', destPath, pkg);
return config;
},
]);
return config;
};
// eslint-disable-next-line import/no-default-export
export default withReactNativeHealthConnect; Put package {pkg}
import android.os.Bundle
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AppCompatActivity
class PermissionsRationaleActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val webView = WebView(this)
webView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
return false
}
}
webView.loadUrl("https://developer.android.com/health-and-fitness/guides/health-connect/develop/get-started")
setContentView(webView)
}
} |
Side note here: Make sure to customise the rationale activity based on your need. That file is just an example webview and you need to create your own native or js view. |
Can someone try this new expo adapter and let me know if it works properly for you? |
@matinzd I am still encountering the same issue with expo-health-connect. |
Can you specify what kind of issue do you have and provide a log or something? |
@matinzd Thank you for your quick reply! I am getting the same error as mentioned in the initial post on Android 10 (I haven't tried it on a different device yet). Here are the dependencies: Error:
Let me know if you need further information on how to reproduce this. |
Did you rebuild the project with |
@matinzd After closely comparing your example with mine, I discovered that |
Describe the bug
I've followed the exact installation in your documentation on how to use it with expo.
I'm using an expo dev build with config plugin.
Tried to run the app on Android 13 and 10, both encountered the same error.
Downgraded to version 1.2.3 and it is now working.
However, I'd like to report the issue so I can upgrade to the latest version.
Here's the error log:
Your app just crashed. See the error below. kotlin.UninitializedPropertyAccessException: lateinit property requestPermission has not been initialized dev.matinzd.healthconnect.permissions.HealthConnectPermissionDelegate.launch(HealthConnectPermissionDelegate.kt:32) dev.matinzd.healthconnect.HealthConnectManager$requestPermission$1$1.invokeSuspend(HealthConnectManager.kt:64) kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:108) kotlinx.coroutines.internal.LimitedDispatcher$Worker.run(LimitedDispatcher.kt:115) kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:103) kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:584) kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:793) kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:697) kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:684)
Environment:
The text was updated successfully, but these errors were encountered: