Skip to content

Add permission_handler_watchos: permissions on the watch - #11

Open
iamoguzhanarslantas wants to merge 6 commits into
flutterwatch:mainfrom
iamoguzhanarslantas:permission-handler-watchos
Open

iamoguzhanarslantas wants to merge 6 commits into
flutterwatch:mainfrom
iamoguzhanarslantas:permission-handler-watchos

Conversation

@iamoguzhanarslantas

Copy link
Copy Markdown
Contributor

Summary

Adds permission_handler_watchos 0.0.1, the watchOS implementation of permission_handler, over dart:ffi.

Not verified on a physical Apple Watch. Everything below was checked on the watchOS 27.0 Simulator and on the host only. I could not get a watch to show up in Xcode, so no device run has been done.

  • Supported, with the iOS implementation's statuses and error codes: notification (provisional included), location / locationWhenInUse / locationAlways (with the service status; "always" needs "when in use" first, as on iOS), sensors (Motion & Fitness), microphone (watchOS 10+), contacts (limited from watchOS 11), calendarFullAccess / calendarWriteOnly / reminders, and bluetooth. One strategy per permission group, as in the iOS implementation.
  • The permissions watchOS does not have answer as iOS answers for a permission it does not know: denied, a request permanentlyDenied, service disabled. criticalAlerts and assistant need an entitlement each and are not implemented yet. openAppSettings returns false: watchOS has no settings page an app can open.
  • The iOS implementation keeps an app from referencing privacy APIs it never asks for with build macros, which a watch app has no Podfile to set. Here each framework except UserNotifications is loaded at runtime (dlopen, classes looked up by name) the first time a permission of its group is asked about, and only messaged. nm -u on the compiled objects shows no class of CoreLocation, CoreMotion, AVFAudio, Contacts, EventKit or CoreBluetooth.
  • watchOS terminates an app that asks for a permission without its usage description. Before each prompt the package checks the Info.plist key and fails with a PlatformException whose code is MISSING_USAGE_DESCRIPTION instead.
  • An "always" location request kept at "while using" changes nothing, so CoreLocation sends no delegate call. As in the iOS implementation, the app becoming active again after the prompt is the answer then.
  • The root README gets the table row; tool/upstream_versions.yaml gets permission_handler. The license is upstream's MIT, as for the other Baseflow ports, so the word allow-list gets its "free of charge" line, plus the package's C free(.

Verified

On the watchOS 27.0 Simulator (Apple Watch Series 12, 46 mm):

  • example/integration_test/permission_handler_test.dart (4 tests) passes. Baseflow's example is a manual demo with no integration test, so this one is the package's own.
  • Every group's system prompt asked for and answered by hand, each giving what iOS gives: notification allowed → granted; location when in use allowed → granted; location always kept at "while using" → permanentlyDenied; sensors allowed → granted; microphone allowed → granted; contacts refused → permanentlyDenied; reminders allowed → granted; calendar write only allowed → granted. Bluetooth reads granted on the Simulator, which has no Bluetooth.
  • The example builds with every ffiSymbols entry in the export table.

Off the watch:

  • 6 host tests against a fake backend.
  • Every tool/ check, dart format as CI runs it, and pub publish --dry-run are clean.

Not verified

  • A physical Apple Watch. No prompt has been seen on a device.
  • Bluetooth's real state and service status: the Simulator has no Bluetooth.
  • Location and sensors with their services turned off in the watch's Settings.
  • That App Store Connect reports no missing usage descriptions for an app that asks for only some groups. The nm -u check above is the evidence; no build has been uploaded.

Notes for review

  • Dart asks for the permissions of requestPermissions one at a time, as the watch shows one prompt at a time.
  • Each call returns a request id at once; native keeps the answer as JSON and wakes Dart through one NativeCallable.listener that carries only a kind, never a payload.
  • A follow-up PR, after this one, makes network_info_plus_watchos's example upstream's again: it asks permission_handler for location behind Platform.isIOS, and with this package that call has an answer.
  • Like my other open PRs here, this one edits the root README's table and tool/upstream_versions.yaml, so whichever merges first leaves the others a conflict there; I will rebase each one after a merge.

🤖 Generated with Claude Code

permission_handler had no watchOS implementation; network_info_plus's
example is one app that started with that error. This package answers it
over dart:ffi for the permissions watchOS has: location (when in use and
always), notifications, sensors, the microphone, contacts, the calendar
(full access and write only), reminders and Bluetooth, with the iOS
implementation's statuses and error codes. The others answer as iOS
answers for a permission it does not know.

The iOS implementation keeps an app from referencing privacy APIs it never
asks for with build macros, which a watch app cannot set. Here each
framework but UserNotifications is loaded at runtime and only messaged;
nm shows no class of the others in the objects. A request whose usage
description is missing fails with MISSING_USAGE_DESCRIPTION instead of the
app being terminated.

Checked on the watchOS 27.0 Simulator: 6 host tests, a 4-case watchOS
integration test, and every group's system prompt answered by hand, the
"always" location kept at "while using" included. Not yet on a watch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mehmet-nettle

This comment was marked as duplicate.

@MAUstaoglu MAUstaoglu left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, the code is clean and follows AUTHORING's push rules. What blocks it is App Review safety, plus a few permissions answered with an invented status.

Must-fix

  1. App Review linkage: every permission group's code is compiled into every app, with no opt-out.

    • The Xcode 27 linker drops the unused frameworks, but class names, delegate metadata and request selectors stay in the binary.
    • Apple doesn't document whether its ITMS-90683 scan reads those.
    • Upstream leaves unused groups out at compile time.

    Please don't claim otherwise in README.md:39-42, CHANGELOG.md:11-13 and PORTING_REPORT.md:24-30 until it's proven. Today the CLI compiles plugin sources with fixed flags, so we'll look at a CLI feature for compile-time opt-in. Before merging, we'd like evidence: an App Store Connect upload of an app that uses only notifications, with no ITMS-90683.

  2. phw_location.m:19,120 uses NSUserDefaults, a required-reason API, and it lands in the app binary. Please document that apps must declare NSPrivacyAccessedAPICategoryUserDefaults (CA92.1). Upstream ships a privacy manifest for the same reason.

  3. storage falls through to the unknown strategy (permission_handler_watchos_ffi.m:144), so a request returns permanentlyDenied. iOS answers granted.

  4. criticalAlerts (UNNotificationSettings.criticalAlertSetting, watchOS 5+) and assistant (INPreferences, watchOS 6+) exist on watchOS, yet the plugin answers permanentlyDenied, which tells the app the user refused. Implement them (UserNotifications is already linked) or answer denied.

Also:

  • phw_location.m:166 ignores NotDetermined, so a dismissed prompt hangs.
  • phw_contacts.m:22 maps default: to limited, although the SDK marks Limited as iOS 18 only.

Nice-to-have

  • Add the ERROR_ALREADY_REQUESTING_PERMISSIONS guard from iOS: overlapping requests overwrite _asked and _store (lib/permission_handler_watchos.dart:83).
  • Add an example/test_driver, as all 20 other examples have.
  • Add a host test for an answer that arrives before the handler is registered.
  • Remove the dead pre-watchOS-10 branches.
  • LICENSE is MIT while the source headers say BSD.
  • Both allow-list entries are justified (the MIT text and free(). Please keep the list alphabetical and fix the reason text.

We'll start the CI run after your update. Thank you!

@MAUstaoglu
MAUstaoglu dismissed mehmet-nettle’s stale review October 23, 2026 20:47

Posted from the wrong account; superseded by the maintainer review.

From the review of PR flutterwatch#11:

* The README, CHANGELOG and PORTING_REPORT no longer say that apps carry
  no reference to the privacy APIs of groups they never ask for. Every
  group's code is in every app: the frameworks but UserNotifications are
  loaded at runtime instead of linked, but class names, selectors and
  delegate metadata are in the binary, and whether ITMS-90683 reads them
  is unchecked until an App Store Connect upload shows it. The README
  tells apps to expect to need every group's usage description until then.
* The README documents the privacy manifest entry the location strategy's
  NSUserDefaults flag needs: NSPrivacyAccessedAPICategoryUserDefaults,
  CA92.1.
* storage is granted, with its service not applicable, as on iOS.
* criticalAlerts is implemented on UserNotifications, as on iOS.
* assistant (Siri) is implemented on INPreferences, loaded at runtime.
  INPreferences throws in an app without the Siri entitlement (seen on the
  Simulator) and leaves its dispatch_once unfinished, so
  NSSiriUsageDescription is the opt-in that the iOS implementation's
  PERMISSION_ASSISTANT macro is: without it the status is denied.
* A when-in-use location prompt closed with the crown sends no delegate
  call on watchOS, so the request waited forever. The app becoming active
  again now answers it, with denied if the status is still not determined
  a second later.
* Contacts: the watchOS SDK has no CNAuthorizationStatusLimited, so an
  unknown status is denied, not limited.
* A request while another waits fails with
  ERROR_ALREADY_REQUESTING_PERMISSIONS, as on iOS.
* The pre-watchOS-10 branches are gone; Package.swift says watchOS 10.
* The source headers name the MIT license that LICENSE holds.
* example/test_driver, as the other examples have; a host test for an
  answer that lands before the handler is registered; the allow-list
  entry in alphabetical order, its reason naming the _release export.

Checked: 8 host tests (2 new); the watchOS integration test, 5 cases (1
new), on the watchOS 27.0 Simulator; there, a when-in-use prompt closed
with the crown answered denied, allowed answered granted, and "always"
kept at "while using" answered permanentlyDenied. The example builds with
all six exports and no class symbol of the runtime-loaded frameworks. The
critical alerts and Siri prompts need entitlements the example lacks and
were not asked for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@iamoguzhanarslantas

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Pushed 57942d2:

Must-fix

  1. App Review linkage: README, CHANGELOG and PORTING_REPORT no longer claim that unused groups leave no trace. They now say that every group's class names, selectors and delegate metadata stay in the binary, and that ITMS-90683 behaviour is unchecked. Until an upload proves otherwise, the README tells apps to expect to need every group's usage description. The App Store Connect upload you asked for is still to come (see below).
  2. The README documents the privacy manifest entry for the NSUserDefaults flag: NSPrivacyAccessedAPICategoryUserDefaults, reason CA92.1.
  3. storage answers granted, with service not applicable, as on iOS.
  4. criticalAlerts is implemented on UserNotifications, as in CriticalAlertsPermissionStrategy. assistant is implemented on INPreferences, loaded at runtime. On the Simulator, +siriAuthorizationStatus throws without the Siri entitlement, from inside a dispatch_once, so the exception can't be caught safely. So NSSiriUsageDescription in Info.plist is the opt-in, in place of the PERMISSION_ASSISTANT macro. Without it the status is denied, and a request fails with MISSING_USAGE_DESCRIPTION.

Also

  • Dismissed prompt: on the watchOS 27.0 Simulator, closing the when-in-use prompt with the crown sends no delegate call at all, not even the second NotDetermined that iOS's handler waits for. WKApplicationDidBecomeActiveNotification now answers. If the status is still not determined a second later, the answer is denied. On the Simulator I checked three cases:
    • crown → denied;
    • allow → granted;
    • always kept at while-using → permanentlyDenied.
  • Contacts: default: maps to denied, because watchOS has no Limited.

Nice-to-have

  • ERROR_ALREADY_REQUESTING_PERMISSIONS guard, with a test.
  • example/test_driver.
  • A host test for an answer that arrives before the handler is registered.
  • The pre-watchOS-10 branches are removed, and Package.swift says watchOS 10.
  • Source headers now say MIT.
  • Allow-list: alphabetical, and the reason names _release.

Checked:

  • 8 host tests.
  • The integration test, now 5 cases, on the watchOS 27.0 Simulator.
  • The example exports all 6 symbols, and nm -u shows no class symbol from the runtime-loaded frameworks.

Not yet done:

  • The App Store Connect upload.
  • The critical-alert and Siri prompts, which need entitlements the example doesn't have.
  • A physical watch.

🤖 Generated with Claude Code

@MAUstaoglu MAUstaoglu left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, everything from round one is addressed, and the App Review wording is honest now. Please fix two things in the new code:

  1. phw_location.m:166-172 never cancels the 1 s re-read timer, which exists for the case where the delegate answers first. In that case, a location request made within the second gets permanentlyDenied while its prompt is on screen, and the usual whenInUse-then-always pair does exactly this. Please cancel the timer in answer:, and arm it only after WillResignActive.
  2. README.md:52 tells apps to add every group's key, but NSSiriUsageDescription also switches on the INPreferences call, which terminates an app that lacks the Siri entitlement. Please give Siri a separate opt-in, or answer denied.

Nit: the root README row leaves out Intents, and the root README still says every package is on pub.dev.

The App Store Connect upload no longer blocks the merge, since the package is staged at 0.0.1 and unpublished. #19 tracks the upload and a compile-time opt-in before 0.1.0.

@MAUstaoglu MAUstaoglu left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more item, from our standards rather than the code.

Example (AUTHORING.md §3, README "Examples & tests"). A ported package ships upstream's own example, unchanged, ported with flutter-watchos plugin port --from-pub permission_handler_apple --include-example. This PR ships a hand-written watch demo instead. The report gives the reason as the template's links, but permission_handler_apple's example does run on the watch:

  • baseflow_plugin_template calls url_launcher only when a link is tapped (lib/src/info_page.dart).
  • url_launcher_watchos in this repo opens http and https links on the watch, in the system browser sheet.

So please:

  • Replace example/lib/ with upstream's, unchanged apart from the formatting our CI checks.
  • In the example pubspec, add what that code imports (baseflow_plugin_template, permission_handler_platform_interface), each with a caret constraint, plus url_launcher_watchos, so the template's links work.
  • Keep the Info.plist usage descriptions.
  • Keep integration_test/permission_handler_test.dart. Upstream has no integration test, so a watch test is right here, as in the Firebase packages. It imports only permission_handler, so it does not depend on the demo.
  • Update the report's Example section to match.

The root README's package table: main adds the sqflite_watchos row, this
branch the permission_handler_watchos one, which now also names Intents,
loaded at runtime for Siri.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MAUstaoglu

Copy link
Copy Markdown
Member

The example rule is now written down in AUTHORING.md §3 (#25).

For this PR: port upstream's example. Adding url_launcher_watchos to the example's pubspec is one of the listed watch deviations, so list it in the package README and in PORTING_REPORT.md. The rest of my last review stands.

iamoguzhanarslantas and others added 3 commits October 24, 2026 20:14
From the second review of PR flutterwatch#11:

* The location strategy's one-second re-read no longer answers a later
  request. It is armed only when the app resigned active since the
  request, as the prompt makes it, and an answer from the delegate stands
  it down, so "always" asked for right after "when in use" keeps its
  prompt. Checked on the watchOS 27.0 Simulator: a when-in-use prompt
  closed with the crown is answered and a second request prompts again;
  allowed answers granted; "always" asked for right after kept its prompt
  until answered, and "keep while using" answered permanentlyDenied.
* Siri has an opt-in of its own, the Info.plist Boolean
  PermissionHandlerWatchosSiri, which only an app with the Siri capability
  sets. NSSiriUsageDescription alone no longer switches INPreferences on,
  since the README tells apps to add every group's key and INPreferences
  ends an app without the capability. Without the opt-in the status is
  denied, and so is a request.
* example/lib/ is permission_handler_apple 9.6.3's own example, unchanged,
  with its res/images. Its pubspec adds baseflow_plugin_template and
  permission_handler_platform_interface, which it imports, and
  url_launcher_watchos for the template's links. On the Simulator it runs,
  and a calendar request from it prompted and answered granted; the
  template's link buttons sit below a scroll area too small to reach
  there and were not tapped. The Info.plist usage descriptions and the
  watchOS integration test stay.
* The root README row names Intents (in the merge before this commit).

Checked: 8 host tests; the watchOS integration test, 5 cases, on the
Simulator with upstream's example in place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Its content is permission_handler_apple's; only the layout changes, to
the package's language version, so that CI's dart format step passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AUTHORING.md §3 now names the deviations a ported example may have and
asks for each in the package README and PORTING_REPORT. This example has
one: its pubspec adds url_launcher_watchos, for the template's links. The
README gets an "Example deviations" table, as network_info_plus_watchos
has, and the report's verification table a row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@iamoguzhanarslantas

Copy link
Copy Markdown
Contributor Author

Thanks! Pushed f5b0a28, ddc7592 and b502b47 on top of a merge of main:

  1. Re-read timer. It is now armed only after WKApplicationWillResignActiveNotification has fired since the request. Every answer: bumps a counter, and the timer only acts if the counter is unchanged, so an answer from the delegate cancels it. Checked on the watchOS 27.0 Simulator:
    • When-in-use prompt closed with the crown: answered, and a second request prompts again.
    • Allowed: granted.
    • "Always" asked right after: its prompt stayed up until answered; "keep while using" → permanentlyDenied.
  2. Siri has its own opt-in: the Info.plist Boolean PermissionHandlerWatchosSiri, which only an app with the Siri capability sets. NSSiriUsageDescription no longer turns INPreferences on. Without the opt-in, the status is denied and a request also answers denied. permission_handler_watchos: App Review proof and compile-time opt-in before 0.1.0 #19 can replace the key with a compile-time opt-in.
  3. Example. example/lib/ is now permission_handler_apple's example, unchanged except for CI formatting (ddc7592), with its res/images. The pubspec adds baseflow_plugin_template ^2.2.1, permission_handler_platform_interface ^4.4.1 and url_launcher_watchos. The Info.plist and the integration test are unchanged. On the Simulator:
    • It runs, and a calendar request from it prompted and answered granted.
    • The template's info page opens, but its link buttons sit below a scroll area I couldn't reach in the Simulator, so I haven't tapped the links.
    • Per the new AUTHORING §3 rule (b502b47), the url_launcher_watchos addition is listed as the example's one watch deviation, in an "Example deviations" table in the README and a row in PORTING_REPORT.

Nit: the Intents mention in the root README row is in the merge. The pub.dev line had already been fixed on main.

Checked: 8 host tests, and the integration test (5 cases) with upstream's example in place.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants