Repository navigation
Declare the required reason APIs, which App Store Connect rejects without - #496
Merged
Merged
Conversation
…hout The app had no privacy manifest at all. Since 1 May 2024 App Store Connect does not accept an upload whose manifest fails to describe the required reason APIs the app uses - a hard gate, before a human sees the build - so this was a rejection waiting for the first upload, and nothing here could have found it: no job on a pull request builds iOS, the release job builds unsigned, and the first machine that checks is Apple's. Apple's rule is **per binary**: each executable or dynamic library that uses such an API needs a manifest in its own bundle, and the same page says a third-party SDK "can't rely on the privacy manifest files for apps that link the third-party SDK". Both Podfiles say `use_frameworks!`, so every development pod here is its own dynamic framework. That shapes the whole change, and it means two of the app's three entries are **conservative** cover for uses that are real but live in someone else's binary: * `UserDefaults` -> `CA92.1`. `flutter_foreground_task` 9.2.2 and `universal_ble` 3.3.0 both reach `UserDefaults.standard` and ship no declaration. `CA92.1` and not `1C8F.1`, which is the App Group variant: this app has no entitlements file at all. * `SystemBootTime` -> `35F9.1`. The engine declares `35F9.1` itself and `sentry_flutter` wraps a Cocoa SDK with its own manifest. The call this repository owns is declared in **the pod's** manifest instead, below. * `FileTimestamp` -> `C617.1`. Every first-party `File.stat()` goes through `dart:io`, so the symbol is in `Flutter.framework`, which declares this category itself. Not `DDA9.1`, which forbids sending anything derived off-device - a promise a log line cannot keep. Not `0A2A.1`, which Apple allows only to third-party SDKs; that is how the engine declares it and why this app may not. `qunleashed_hardnested` gets a manifest of its own, shipped through `resource_bundles` - the shape share_plus, file_picker and device_info_plus all use. `util_posix.c`, which the Apple unity file #includes, carries a pre-10.12 `clock_gettime` fallback that calls `mach_absolute_time()`. That branch never executes on our deployment targets, because `_civet_safe_clock_gettime` prefers the real `clock_gettime` whenever it is non-NULL, which on iOS 12 and macOS 10.15 is always - but Apple's check is a static scan and the symbol is in the binary either way. `35F9.1` is honest for it: the shim subtracts its own first reading, so an elapsed time leaves the function and never an absolute mach value. The data half is declared from ADR 0013 rather than from guesswork: crash, performance and other diagnostic data for Sentry, which is on by default with one switch, and a device id for Firebase's installation id. All four unlinked and none for tracking - there are no accounts and `sendDefaultPii` is false. **Precise location is deliberately absent:** the app reads location and never sends it, because §6.2's scrubber replaces decimal coordinates before an event leaves and §6.4 masks `FlutterMap` in the replay for that exact reason. Four edits in `project.pbxproj`. The `Resources` build phase entry is what puts the file inside the `.app`; the build file and the file reference it chains through are load-bearing for that copy; only the group membership is cosmetic. `test/privacy_manifest_test.dart` is the gate that exists on this machine, and it asserts both manifests as a **token** sequence - every key, string, boolean and array in document order, comments stripped. Values alone were not enough: a reviewer showed that renaming `NSPrivacyAccessedAPITypes` hides the entire API half of the file while a value-only assertion stays green. The same review showed the phase assertion had to be scoped to the Runner target, because moving the entry into the empty `RunnerTests` phase also passed - and would ship the manifest inside `RunnerTests.xctest` with none in the `.app`. It then checks Apple's per-binary rule twice over. Once against every Apple source this checkout has, submodules included - `git ls-files --recurse-submodules`, because walking those directories from disk picked up whatever the last build left behind, so what the scan covered depended on the machine. Four hits there are pinned as outside the iOS app, individually and with the reason, so a new one fails rather than joining them: libusb's Darwin and Linux backends, and super_native_extensions' `darwin/macos` and `win32` Rust. And once against **every iOS plugin Flutter resolved**, which is the half that moves. The app's own Apple code uses one required reason API and now ships a manifest beside it; the plugin set changes on every dependency bump, and a bump is how `CA92.1` came to be needed at all. Reading `.flutter-plugins-dependencies` pins the plugins that use such an API and declare none themselves - today `flutter_foreground_task:UserDefaults`, `universal_ble:UserDefaults` and `device_info_plus:DiskSpace` - so a fourth fails the test with the plugin's name instead of arriving as a rejected upload. Two rules keep that honest and both are derived rather than listed: a plugin whose iOS podspec declares no `source_files` compiles nothing into the app and is skipped, which is flipperlib's position and stops being true the day it gains native iOS code; and `Package.swift` is read as the build manifest it is, which was `permission_handler_apple`'s only apparent `FileTimestamp` use. Fourteen mutations were run against the finished test - including emptying a reasons array, dropping the podspec's `resource_bundles`, and disabling each of the two derived rules above. Each failed the test whose name describes it. Scoped deliberately: **macOS gets no manifest.** Apple's page says "Describe the reasons your app or third-party SDK on iOS, iPadOS, tvOS, visionOS, or watchOS uses these APIs", macOS is absent from that list, and macOS here ships as a DMG outside the Mac App Store. **One category is used and declared nowhere, and it is not ours to declare.** `device_info_plus` 11.5.0 reads `NSFileSystemSize` unconditionally and ships an empty `NSPrivacyAccessedAPITypes`. That call is reached on iOS: `super_native_extensions`' `long_press_handler.dart` awaits `DeviceInfoPlugin().deviceInfo` with no platform guard on every long-press drag, then reads the result only under `if (deviceInfo is AndroidDeviceInfo)`. No disk-space reason is declared here, because none of Apple's four describes fetching a number and discarding it, and the symbol is in that plugin's framework rather than ours. `docs/releasing.md` names the three ways out in the order they are worth trying; the first is a guard in our own fork, which the sibling call in `super_context_menu` already has.
The privacy manifest is what the App Store listing is generated from, so the part a user can read is the listing: what the app sends, and that the location it reads for the map is not among it. #491 set the precedent - it was equally invisible in the app and carries an `Android:` bullet for the App Bundle. releasing.md leaves presence to judgement rather than to `check_changelog.sh`, which checks shape only. This was first left out on the grounds that nothing a user can observe changes; the listing is observable, and the judgement was wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The app had no privacy manifest at all. Since 1 May 2024 App Store Connect
does not accept an upload whose manifest fails to describe the required reason
APIs the app uses — a hard gate, before a human sees the build. Nothing here
could have found it: no job on a pull request builds iOS, the release job
builds unsigned, and the first machine that checks is Apple's.
This closes b3 on the submission checklist, and it is the only one of the
four blockers that needs neither an account nor a Mac.
Apple's rule is per binary, and that shapes everything
and, on the same page:
Both Podfiles say
use_frameworks!, so every development pod is its owndynamic framework. So the app's manifest answers for the app's binary, each pod
answers for its own — and two of the app's three entries are conservative
cover for uses that are real but live in someone else's binary. That is said
plainly rather than implied, because the first draft of this said the opposite
and a review caught it.
UserDefaultsCA92.1flutter_foreground_taskanduniversal_blereachUserDefaults.standardand ship no declaration.CA92.1and not1C8F.1, the App Group variant: this app has no entitlements file at all.SystemBootTime35F9.135F9.1itself, and Sentry's Cocoa pod. The call this repository owns is declared in the pod's own manifest.FileTimestampC617.1File.stat()goes throughdart:io, so the symbol is inFlutter.framework, which declares this itself. NotDDA9.1, which forbids sending anything derived off-device — a promise a log line cannot keep. Not0A2A.1, which Apple allows only to third-party SDKs; that is how the engine declares it and why this app may not.qunleashed_hardnestedgets a manifest of its own, throughresource_bundles.util_posix.ccarries a pre-10.12clock_gettimefallback callingmach_absolute_time(). That branch never executes on our deployment targets —_civet_safe_clock_gettimeprefers the realclock_gettimewhenever it isnon-NULL, which on iOS 12 and macOS 10.15 is always — but Apple's check is a
static scan and the symbol is in the binary either way.
35F9.1is honest forit: the shim subtracts its own first reading, so an elapsed time leaves the
function and never an absolute mach value.
The data half
From ADR 0013 rather than from guesswork: crash, performance and other
diagnostic data for Sentry, which is on by default with one switch, and a
device id for Firebase's installation id. All four unlinked, none for tracking
— there are no accounts and
sendDefaultPiiis false.Precise location is deliberately absent. The app reads location and never
sends it: §6.2's scrubber replaces decimal coordinates before an event leaves,
and §6.4 masks
FlutterMapin the replay for exactly that reason. Declaring itwould be a false statement in the stricter direction.
The gate, and the half of it with a future
test/privacy_manifest_test.dartstands in for App Store Connect. It assertsboth manifests as a token sequence — every key, string, boolean and array
in document order, comments stripped — because values alone were not enough: a
reviewer showed that renaming
NSPrivacyAccessedAPITypeshides the entire APIhalf of the file while a value-only assertion stays green. The same review
showed the build-phase assertion had to be scoped to the Runner target, because
moving the entry into the empty
RunnerTestsphase also passed, and would shipthe manifest inside
RunnerTests.xctestwith none in the.app.Then Apple's per-binary rule, checked twice:
git ls-files --recurse-submodules. Walking those directories from disk insteadpicked up gitignored build output, so what the scan covered depended on what
the developer had last compiled. Four hits are pinned as outside the iOS app,
individually and with the reason: libusb's Darwin and Linux backends, and
super_native_extensions'darwin/macosandwin32Rust..flutter-plugins-dependencies. This is the half that moves: our own Applecode uses one such API and now ships a manifest beside it, while the plugin
set changes on every dependency bump — and a bump is how
CA92.1came to beneeded at all. The pinned set today is three, so a fourth fails with the
plugin's name instead of arriving as a rejected upload.
flutter_foreground_task:UserDefaultsCA92.1universal_ble:UserDefaultsCA92.1device_info_plus:DiskSpaceTwo rules keep that honest, both derived rather than listed: a plugin whose iOS
podspec declares no
source_filescompiles nothing into the app and is skipped(flipperlib's position, and it stops being true the day it gains native iOS
code), and
Package.swiftis read as the build manifest it is — which waspermission_handler_apple's only apparentFileTimestampuse.Fourteen mutations were run against the finished test, including emptying a
reasons array, dropping the podspec's
resource_bundles, and disabling each ofthe two derived rules. Each failed the test whose name describes it.
One category is used and declared nowhere, and it is not ours to declare
device_info_plus11.5.0 readsNSFileSystemSizeunconditionally inFPPDeviceInfoPlusPlugin.mand ships an emptyNSPrivacyAccessedAPITypes.That call is reached on iOS: our own fork of
super_native_extensionsawaits
DeviceInfoPlugin().deviceInfoinlong_press_handler.dartwith noplatform guard on every long-press drag, then reads the result only under
if (deviceInfo is AndroidDeviceInfo)— so on iOS the value is fetched and thrownaway. The sibling call in
super_context_menuis guarded withdefaultTargetPlatform == TargetPlatform.androidand shows the fix.No disk-space reason is declared here, because none of Apple's four describes
fetching a number and discarding it, and the symbol is in that plugin's
framework rather than ours.
docs/releasing.mdnames the three ways out in theorder they are worth trying: the guard in our fork, then
device_info_plus13from the
win32chain, and only failing both a declaration — which is astatement whoever submits has to be willing to make.
This correction came from review. The first draft argued the gap away with
"this app calls
device_info_plusonly forandroidInfo", which is false.Scope
visionOS, or watchOS"; macOS is absent, and ships here as a DMG outside the
Mac App Store.
mach_absolute_time()inos/darwin_usb.candis a submodule — but its iOS podspec compiles no native code at all, so it
reaches the macOS pod only. Pinned with that reason rather than left silent.
Other changes. Left out at first on the groundsthat nothing a user can observe changes. That was wrong: the manifest is what
the App Store listing is generated from, so the observable part is the
listing — what the app sends, and that the location it reads for the map is
not among it. Build the App Bundle a Play submission needs, and verify both artifact kinds #491 is the precedent, equally invisible in the app and
carrying an
Android:bullet for the App Bundle.Verified
flutter analyzeclean,check_format.shclean, 2546 tests pass. Onefull run had a transient load failure in
faaccrack_obfuscation_test.dartimmediately after the mutation battery; it passes in isolation and the suite
passes on re-run.
not from memory.
project.pbxprojwas hand-edited from Windows, and a reviewer parsed it withan OpenStep plist parser: 91 objects, no duplicate keys, ids unique, every
object in its own section, and the entry in the Runner target's phase and
not
RunnerTests'.resource_bundlesmatches howshare_plus,file_pickeranddevice_info_plusship theirs, in both their iOS and macOS podspecs.What this does not prove
That the upload is accepted. Nothing here can build or sign an iOS app, so the
first real evidence is b2 — and if Apple's scanner attributes
device_info_plus' disk-space symbol to the app, that upload will say so byname.
Not in this PR, and worth knowing
iOS has no push entitlement. There is no
.entitlementsfile anywhereunder
ios/, noCODE_SIGN_ENTITLEMENTSin the project, and noremote-notificationinUIBackgroundModes— so no APNs token can be issued.push_service.dart's dartdoc reasons that only macOS lacksaps-environmentand treats iOS as push-capable for
topicsSupported. That belongs to b2.