From 516354125d2e8f2edb37d5382586d65277e34855 Mon Sep 17 00:00:00 2001 From: Nichlas Torgersen Date: Fri, 9 May 2025 14:55:48 +0200 Subject: [PATCH 1/2] feature(ConfigPlugin): adds possibility of adding 'uses-feature' flags to AndroidManifest --- package/src/expo-plugin/@types.ts | 22 +++++++++++++++++++ .../expo-plugin/withRequireFeatureAndroid.ts | 19 ++++++++++++++++ package/src/expo-plugin/withVisionCamera.ts | 9 ++++++++ 3 files changed, 50 insertions(+) create mode 100644 package/src/expo-plugin/withRequireFeatureAndroid.ts diff --git a/package/src/expo-plugin/@types.ts b/package/src/expo-plugin/@types.ts index 015901617c..939bb41793 100644 --- a/package/src/expo-plugin/@types.ts +++ b/package/src/expo-plugin/@types.ts @@ -47,4 +47,26 @@ export type ConfigProps = { * @default false */ enableCodeScanner?: boolean + /** + * Android Only + * Whether to require the camera through Androids uses-feature flag. + * + * When set to `true`, the app will only be available in Play Store for phones with a camera. + * When set to `false`, the feature will not be required, and the app will be discoverable for devices without a camera. + * If it is `undefined` or `null`, the uses-feature flag will not be added to the AndroidManifest. + * + * @default undefined + */ + requiresCamera?: boolean | null + /** + * Android Only + * Whether to require the microphone through Androids uses-feature flag. + * + * When set to `true`, the app will only be available in Play Store for phones with a microphone. + * When set to `false`, the feature will not be required, and the app will be discoverable for devices without a microphone. + * If it is `undefined` or `null`, the uses-feature flag will not be added to the AndroidManifest. + * + * @default undefined + */ + requiresMicrophone?: boolean | null } diff --git a/package/src/expo-plugin/withRequireFeatureAndroid.ts b/package/src/expo-plugin/withRequireFeatureAndroid.ts new file mode 100644 index 0000000000..a3b4ec2337 --- /dev/null +++ b/package/src/expo-plugin/withRequireFeatureAndroid.ts @@ -0,0 +1,19 @@ +import type { ConfigPlugin } from '@expo/config-plugins' +import { withAndroidManifest } from '@expo/config-plugins' + +export const withRequireFeatureAndroid: ConfigPlugin<[string, boolean]> = (c, [featureName, featureIsRequired]) => { + return withAndroidManifest(c, (config) => { + if (!Array.isArray(config.modResults.manifest['uses-feature'])) config.modResults.manifest['uses-feature'] = [] + + if (config.modResults.manifest['uses-feature'].find((item) => item.$['android:name'] === featureName) == null) { + config.modResults.manifest['uses-feature'].push({ + $: { + 'android:name': featureName, + 'android:required': featureIsRequired ? 'true' : 'false', + }, + }) + } + + return config + }) +} diff --git a/package/src/expo-plugin/withVisionCamera.ts b/package/src/expo-plugin/withVisionCamera.ts index 13d0034ee2..92ee356555 100644 --- a/package/src/expo-plugin/withVisionCamera.ts +++ b/package/src/expo-plugin/withVisionCamera.ts @@ -3,6 +3,7 @@ import { withPlugins, AndroidConfig, createRunOncePlugin } from '@expo/config-pl import { withEnableFrameProcessorsAndroid } from './withEnableFrameProcessorsAndroid' import { withEnableFrameProcessorsIOS } from './withEnableFrameProcessorsIOS' import { withAndroidMLKitVisionModel } from './withAndroidMLKitVisionModel' +import { withRequireFeatureAndroid } from './withRequireFeatureAndroid' import type { ConfigProps } from './@types' import { withEnableLocationIOS } from './withEnableLocationIOS' // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-assignment @@ -41,6 +42,14 @@ const withCamera: ConfigPlugin = (config, props = {}) => { config = withEnableFrameProcessorsAndroid(config, props.enableFrameProcessors) config = withEnableFrameProcessorsIOS(config, props.enableFrameProcessors) } + if (props.requiresCamera !== null && props.requiresCamera !== undefined) { + // add uses-feature element to AndroidManifest.xml for hiding app from Play store in case camera is not available on the device + config = withRequireFeatureAndroid(config, ['android.hardware.camera', props.requiresCamera]) + } + if (props.requiresMicrophone !== null && props.requiresMicrophone !== undefined) { + // add uses-feature element to AndroidManifest.xml for hiding app from Play store in case microphone is not available on the device + config = withRequireFeatureAndroid(config, ['android.hardware.microphone', props.requiresMicrophone]) + } if (props.enableCodeScanner) config = withAndroidMLKitVisionModel(config, props) From efdb29db2a542610fe68fc8a08ab7e1772899e4f Mon Sep 17 00:00:00 2001 From: Nichlas Torgersen Date: Fri, 9 May 2025 15:50:24 +0200 Subject: [PATCH 2/2] refactor(ConfigPlugin): renames uses-feature plugin impl --- ...thRequireFeatureAndroid.ts => withUsesFeatureAndroid.ts} | 2 +- package/src/expo-plugin/withVisionCamera.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) rename package/src/expo-plugin/{withRequireFeatureAndroid.ts => withUsesFeatureAndroid.ts} (84%) diff --git a/package/src/expo-plugin/withRequireFeatureAndroid.ts b/package/src/expo-plugin/withUsesFeatureAndroid.ts similarity index 84% rename from package/src/expo-plugin/withRequireFeatureAndroid.ts rename to package/src/expo-plugin/withUsesFeatureAndroid.ts index a3b4ec2337..8aebeb5bde 100644 --- a/package/src/expo-plugin/withRequireFeatureAndroid.ts +++ b/package/src/expo-plugin/withUsesFeatureAndroid.ts @@ -1,7 +1,7 @@ import type { ConfigPlugin } from '@expo/config-plugins' import { withAndroidManifest } from '@expo/config-plugins' -export const withRequireFeatureAndroid: ConfigPlugin<[string, boolean]> = (c, [featureName, featureIsRequired]) => { +export const withUsesFeatureAndroid: ConfigPlugin<[string, boolean]> = (c, [featureName, featureIsRequired]) => { return withAndroidManifest(c, (config) => { if (!Array.isArray(config.modResults.manifest['uses-feature'])) config.modResults.manifest['uses-feature'] = [] diff --git a/package/src/expo-plugin/withVisionCamera.ts b/package/src/expo-plugin/withVisionCamera.ts index 92ee356555..7d444c313e 100644 --- a/package/src/expo-plugin/withVisionCamera.ts +++ b/package/src/expo-plugin/withVisionCamera.ts @@ -3,7 +3,7 @@ import { withPlugins, AndroidConfig, createRunOncePlugin } from '@expo/config-pl import { withEnableFrameProcessorsAndroid } from './withEnableFrameProcessorsAndroid' import { withEnableFrameProcessorsIOS } from './withEnableFrameProcessorsIOS' import { withAndroidMLKitVisionModel } from './withAndroidMLKitVisionModel' -import { withRequireFeatureAndroid } from './withRequireFeatureAndroid' +import { withUsesFeatureAndroid } from './withUsesFeatureAndroid' import type { ConfigProps } from './@types' import { withEnableLocationIOS } from './withEnableLocationIOS' // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-assignment @@ -44,11 +44,11 @@ const withCamera: ConfigPlugin = (config, props = {}) => { } if (props.requiresCamera !== null && props.requiresCamera !== undefined) { // add uses-feature element to AndroidManifest.xml for hiding app from Play store in case camera is not available on the device - config = withRequireFeatureAndroid(config, ['android.hardware.camera', props.requiresCamera]) + config = withUsesFeatureAndroid(config, ['android.hardware.camera', props.requiresCamera]) } if (props.requiresMicrophone !== null && props.requiresMicrophone !== undefined) { // add uses-feature element to AndroidManifest.xml for hiding app from Play store in case microphone is not available on the device - config = withRequireFeatureAndroid(config, ['android.hardware.microphone', props.requiresMicrophone]) + config = withUsesFeatureAndroid(config, ['android.hardware.microphone', props.requiresMicrophone]) } if (props.enableCodeScanner) config = withAndroidMLKitVisionModel(config, props)