From 071d63f31f633106ae0ed7bf014c7029d1d0cb75 Mon Sep 17 00:00:00 2001 From: bubulux Date: Tue, 27 Oct 2026 11:10:51 +0100 Subject: [PATCH] Play upload: drop the Bluetooth plugin's capped location permissions, fail on rejection Google Play rejected the v0.13.0 staging bundle: the ble-plx plugin with neverForLocation declares ACCESS_COARSE/FINE_LOCATION again as uses-permission-sdk-23 with maxSdkVersion 30, next to the app's own uncapped declarations. withUncappedLocation removes the capped copies. The rejection went unnoticed because the upload step piped into tee and took its exit code; set -o pipefail makes a rejected upload fail the run. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/build-phone-app.yml | 2 ++ apps/mobile/app.config.ts | 25 ++++++++++++++++++++++--- docs/known-issues.md | 4 ++++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-phone-app.yml b/.github/workflows/build-phone-app.yml index b920b31..04c4d39 100644 --- a/.github/workflows/build-phone-app.yml +++ b/.github/workflows/build-phone-app.yml @@ -248,6 +248,8 @@ jobs: - name: Upload to Google Play if: env.AAB_NAME != '' && env.TRACK != 'none' run: | + # The script's exit code, not tee's: a rejected upload must fail the run. + set -o pipefail tools=$(ls -d "$ANDROID_HOME"/build-tools/* | sort -V | tail -1) package=$("$tools/aapt2" dump badging "$RUNNER_TEMP/$APK_NAME" | sed -n "s/^package: name='\([^']*\)'.*/\1/p") node scripts/play-upload.mjs --bundle "$RUNNER_TEMP/$AAB_NAME" --package "$package" --track "$TRACK" \ diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index 416ccf8..9b255b9 100644 --- a/apps/mobile/app.config.ts +++ b/apps/mobile/app.config.ts @@ -1,5 +1,5 @@ import type { ConfigContext, ExpoConfig } from "expo/config"; -import { withAppBuildGradle } from "expo/config-plugins"; +import { withAndroidManifest, withAppBuildGradle } from "expo/config-plugins"; import { version } from "../../package.json"; /** @@ -59,6 +59,25 @@ const withReleaseSigning = (config: ExpoConfig): ExpoConfig => return c; }) as ExpoConfig; +/** + * The Bluetooth plugin (react-native-ble-plx, lights #208) with `neverForLocation` declares the location + * permissions again as `uses-permission-sdk-23` capped at Android 11 (maxSdkVersion 30). The app needs + * them on every version for GPS (app.json, expo-location), and Google Play rejects a permission declared + * twice with different maxSdkVersions. So the capped copies go; Bluetooth scanning keeps its + * `neverForLocation` flag. + */ +const LOCATION = new Set(["android.permission.ACCESS_COARSE_LOCATION", "android.permission.ACCESS_FINE_LOCATION"]); +const withUncappedLocation = (config: ExpoConfig): ExpoConfig => + withAndroidManifest(config, (c) => { + const m = c.modResults.manifest as { "uses-permission-sdk-23"?: { $: Record }[] }; + const sdk23 = m["uses-permission-sdk-23"]; + if (sdk23) m["uses-permission-sdk-23"] = sdk23.filter((p) => !(LOCATION.has(p.$["android:name"]) && p.$["android:maxSdkVersion"])); + return c; + }) as ExpoConfig; + +/** Every build's config plugins of our own. */ +const withOwnPlugins = (config: ExpoConfig): ExpoConfig => withUncappedLocation(withReleaseSigning(config)); + /** * The builds besides production (app.json), by APP_VARIANT. Each has its own package id, so it sits next * to the production app with its own storage instead of replacing it, and its own icon colour tells them @@ -88,12 +107,12 @@ export default ({ config }: ConfigContext): ExpoConfig => { extra: { ...base.extra, commit: process.env.GIT_COMMIT ?? "" }, }; const name = process.env.APP_VARIANT; - if (!name) return withReleaseSigning(app); + if (!name) return withOwnPlugins(app); const v = VARIANTS[name]; // A typo must not silently build the production app. if (!v) throw new Error(`APP_VARIANT=${name}: expected one of ${Object.keys(VARIANTS).join(", ")}, or unset for production`); const icons = `./assets/icons/${v.icons}`; - return withReleaseSigning({ + return withOwnPlugins({ ...app, name: v.name, scheme: `openclapper-${v.slug}`, diff --git a/docs/known-issues.md b/docs/known-issues.md index 7776045..6b47a6c 100644 --- a/docs/known-issues.md +++ b/docs/known-issues.md @@ -51,3 +51,7 @@ The task view draws each workspace's windows as outlined boxes with the app's ic ## Vignetting is a rough indication, and a native dependency (issue #266) A rig's vignetting (Library › Rigs) is three numbers per focal length, interpolated between them: it ignores the aperture and the real falloff curve of the lens, so the darkened corners in the viewfinder and on photos are a hint of what the rig will show, not a measurement. On the phone the layer is an SVG radial gradient (`react-native-svg`, a native module): an installed app needs a new dev build or APK before it shows. A step of a sequence shows the vignette of its framing (or of the frame its overlay was drawn for). + +## The Bluetooth plugin's location permissions are removed (lights #208) + +`@sfourdrinier/react-native-ble-plx` with `neverForLocation` declares the location permissions a second time, capped at Android 11. Google Play rejects a permission declared twice with different `maxSdkVersion`s, so `withUncappedLocation` in `apps/mobile/app.config.ts` removes the capped copies; the app keeps them uncapped for GPS. A plugin update that declares them another way would bring the rejection back: Build phone app now fails when an upload is rejected (it used to pass, because the step piped into `tee`).