win32 6, Swift Package Manager, and a toolchain refresh (6.0.0-dev.1) - #149
Closed
hpoul wants to merge 18 commits into
Closed
win32 6, Swift Package Manager, and a toolchain refresh (6.0.0-dev.1)#149hpoul wants to merge 18 commits into
hpoul wants to merge 18 commits into
Conversation
Dart 3.7 changed the default style and nothing here had been run through it since, so `dart format --set-exit-if-changed` could not be added to CI without a reformat first. This commit is that reformat and nothing else. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every published 5.x pinned `win32 >=2.0.0 <6.0.0`, so the plugin could not be resolved alongside `package_info_plus >=10.1.0`, which needs win32 ^6.0.1. Lifting the pin means porting to the 6.0 API: `TEXT()` is gone in favour of `String.toPcwstr()`/`toPwstr()`, the Cred* calls return `Win32Result<bool>` carrying the last-error code rather than a BOOL plus `GetLastError()`, and the enum constants are top-level extension types. Allocations move into an `Arena`, which is what removes the hand-rolled free of every pointer. This is not a Windows-only fix. `lib/biometric_storage.dart` re-exports the Windows implementation under `if (dart.library.io)`, and Flutter generates `dart_plugin_registrant.dart` for every platform at once — an iOS release build of a consuming app imports the barrel and calls `Win32BiometricStoragePlugin.registerWith()` behind a `Platform.isWindows` check. So a win32 API break fails an iOS build. The test now imports the public barrel instead of `src/`, which makes `flutter test` on any host compile the bindings; reintroducing a `TEXT()` call reproduces the original error there. Also: writing an empty value used to reach `Uint8List.toNative()`, which rejects an empty list. A zero-length blob now gets a valid one-byte pointer. The `fileName:` key under `windows:` is a web-only key and was never read by the tool; dropped rather than corrected, since the barrel import is what the registrant needs. Requires Dart 3.10 / Flutter 3.44, which is win32 6.0.1's own floor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Flutter 3.44 makes SwiftPM the default for iOS and macOS, and falls back to CocoaPods for any plugin without a Package.swift — so adding this plugin to an app that had finished the migration silently regenerated its Podfiles. The CocoaPods registry also goes read-only on 2 December 2026, and Flutter's advice until then is to ship both, which is what this does: a Package.swift alongside the podspec, with the FlutterFramework dependency the tool now expects. SwiftPM wants the sources under `<platform>/<plugin_name>/Sources/<plugin_name>`, which the old layout could not satisfy twice over: `ios/Classes` reached the shared implementation through a symlink into `macos/Classes`, and mixing the Objective-C shim with Swift in one target is not something a SwiftPM target does. Both go away. The sources now live once in `darwin/`, shared through `sharedDarwinSource: true`, and the registrar's messenger — a method on iOS, a property on macOS — is the only thing behind an `#if os(...)`. Consequences worth knowing about: the macOS plugin class is `BiometricStoragePlugin` rather than `BiometricStorageMacOSPlugin`, and iOS no longer vends `BiometricStoragePlugin.h`. Flutter generates the registrant for both, so this is only visible to someone registering the plugin by hand. The example's Xcode projects were regenerated from the current template rather than unpicked: they carried CocoaPods build phases, an iOS 12 deployment target and a macOS 10.14 one, all below what Flutter now supports. Its own customisations — bundle ids, signing team, NSFaceIDUsageDescription, the keychain-access-groups entitlements — are carried over. Verified by building an app that also depends on package_info_plus ^10.2.1 for iOS and macOS: both succeed, no Podfile is generated, and the plugin's Swift symbols are in the linked binaries. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Kotlin 2.0.21 to 2.2.20, AGP 8.1.4 to 8.13.2, compileSdk 35 to 36,
androidx.biometric 1.4.0-alpha02 to alpha07, core-ktx 1.10.1 to 1.18.0,
fragment-ktx 1.6.1 to 1.9.0, slf4j 2.0.7 to 2.0.18, kotlin-logging 5 to 8.
core-ktx stops at 1.18.0 deliberately: 1.19.0 declares minCompileSdk 37, above
what AGP 9.1 supports.
Two of these are not version churn. `lintOptions` was removed in AGP 9, so the
block had to become `lint`. And the plugin no longer applies the Kotlin Gradle
Plugin itself: AGP 9 brings its own Kotlin support and Flutter warns that
plugins applying KGP will stop building, while on AGP 8 Flutter's own Gradle
plugin applies `kotlin-android` for us. Either way it arrives after this file is
evaluated, so `kotlin { }` is not available at the top level — which is also
what made `jvmToolchain` unresolvable in some consumer builds (#107). The JVM
target is configured from inside `pluginManager.withPlugin(...)` instead.
`canAuthenticate()` used to throw on any BiometricManager status code it did not
recognise, and androidx.biometric keeps adding them — Android 16 introduced
BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS (21) and the call started blowing up
(#148). Unmapped codes now report as ErrorStatusUnknown, which is what that
value is for, and log the code.
The example moves to Gradle 9.3.1 and AGP 9.1.0, which is what `flutter create`
emits today and what Java 25 requires, so the plugin is exercised against the
newest toolchain rather than the one it declares. Its `android.builtInKotlin`
stays false: AGP 9.1.0 ships KGP 2.2.10 and Flutter 3.47 requires 2.2.20.
Also drops two dead files: settings_aar.gradle, from the pre-1.0 aar workflow,
and an empty app/gradle.properties.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The README presented `MainActivity extends FlutterFragmentActivity` and a `Theme.AppCompat` theme as flat requirements of using the plugin at all, which makes adoption look far more invasive than it is — and is the reason this pass exists, since a consumer wanting `authenticationRequired: false` needs neither. Both are conditions on showing a prompt. `withAuth` returns straight to its callback when `authenticationRequired` is false, so `attachedActivity` is never read and `BiometricPrompt` is never constructed; a plain `FlutterActivity` costs one logged error and only authenticated reads and writes. The theme is narrower still. androidx.biometric only builds its own dialog with `androidx.appcompat.app.AlertDialog` when `isUsingFingerprintDialog()` holds: SDK_INT < 28, SDK_INT == 28 without a fingerprint sensor, or a device on the `shouldUseFingerprintForCrypto` list. The README said "Android < 29"; API 29 and up use the system BiometricPrompt and the theme does not matter. Also: the getStorage snippet was missing its `await` and then read from a variable it never assigned, the kotlin version to "make sure to use" was 1.4.31, and the iOS/macOS deployment targets were three Flutter releases stale. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The workflow had drifted: checkout@v1, flutter-action@v1, a Java 12 toolchain, an ubuntu-22.04 matrix whose only entry excluded itself, and a `web` job that set up Flutter on Windows and then did nothing at all. Windows now runs the test suite rather than nothing, which is the platform whose bindings just broke everyone else's builds. Formatting and `--fatal-infos` analysis run alongside the tests. The iOS build asserts that no Podfile appeared, so a regression in the Swift Package Manager support fails rather than passing quietly with CocoaPods underneath. macOS stays out of the build matrix for the reason the old comment gave, now stated precisely: the example's keychain-access-groups entitlement resolves $(AppIdentifierPrefix) from the signing team, so its Runner cannot be built without a development certificate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Matches the plugin's own `^3.10.0` and flutter_lints 6. The analyzer excludes and the .metadata revisions are what `flutter pub get` and `flutter create` wrote; the web platform entry is put back by hand, since regenerating only ios and macos dropped it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The if/else chain over `Platform.isX` had the unsupported-platform case at the bottom of a five-branch ladder. Switching on `Platform.operatingSystem` puts each platform's payload on one line beside its name, and the fallthrough binds the value it reports. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mostly the things this pass cost time to work out and that the code does not say: that the Windows implementation is compiled on iOS and macOS too and why a federated split would not change that, that `fileName:` under `windows:` was never read, how the darwin sources are shared, and why the plugin must not apply the Kotlin Gradle Plugin. The working-practice half is taken from the conventions in the owner's other Flutter project, kept to what applies to a plugin: the commands, structured edits over shell text manipulation, backgrounding slow builds, and the habit that produced most of the above — an analyze is not a compile, and a plugin that failed to register still builds green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`isUsingFingerprintDialog()` is `SDK_INT < 28`, not `<= 28`; the API 28 case is the separate one where the device has no fingerprint sensor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Removing the barrel export, naming the file with dartFileName, and splitting Windows into a federated package each fail for a different reason, and stating only the last of them invites the first two to be tried. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Carried over from the template podspec, which said 'Your Company' and 'email@example.com'. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Xcode writes .swiftpm/xcode/xcuserdata beside Package.swift as soon as anyone opens the package, and Package.resolved has nothing to pin here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ionPrompt kSecUseOperationPrompt has been deprecated since iOS 14 / macOS 11, and warned twice on every build. Its replacement is LAContext.localizedReason, which means the reason travels on the context the query already carries rather than beside it in the query dictionary. Set on every call rather than once, because `context` may hand back a context reused across calls when darwinTouchIDAuthenticationForceReuseContextDuration is set. An empty or absent title is left alone: localizedReason is a non-optional String, where assigning nil to the old query key simply removed it. localizedReason is API_AVAILABLE(macos(10.13), ios(11.0)), below the package's declared iOS 13 / macOS 10.15, so it needs no availability guard. Verified by building an app for iOS and macOS: both succeed with no remaining deprecation warnings from this file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The unanchored rule also matched
example/{ios,macos}/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved,
which is an app-level lockfile and the one case where committing it is right.
Neither file exists today — every dependency in the generated package graph is
a local path dependency, which SwiftPM does not record — but the rule should
not be waiting to hide the wrong one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Checked on an iPhone Xr running iOS 18.7.9 with a diagnostic build that set the deprecated kSecUseOperationPrompt and the replacement LAContext.localizedReason at the same time, to different marker strings. A screen recording of the read shows neither: the Face ID panel is the glyph and the words "Face ID", and the alert after a failed scan offers only "Face ID erneut versuchen" and "Abbrechen". So the strings were never rendered here, before the migration or after it — worth recording, because someone customising IosPromptInfo and seeing nothing change would otherwise reasonably suspect the plugin of dropping them. They still reach the system, and Touch ID and macOS do draw them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
logging_appenders 1.1 to 2.0, which drops dio in favour of package:http and takes three transitive dependencies with it. The example only uses PrintAppender and the formatter types, so nothing in it changes. The widget test has never passed: it is the stock plugin-template test, asserting a Text starting with 'Running on:' that the example's UI has never contained — zero occurrences in main.dart at any commit. Replaced with a smoke test of what is actually on screen. The example is a separate package, so `flutter analyze` and `flutter test` at the repository root never reached it, which is how the test stayed broken. CI now runs both there too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Upgrading the example from AGP 9.1.0 to 9.3.2 (with Gradle 9.3.1 to 9.5.0 and KGP 2.4.0 to 2.4.10, both required by it) surfaced a regression this branch had already introduced without noticing. androidx.biometric 1.4.0-alpha06 added `minCompileMinorSdk=1` to its AAR metadata, so alpha06 and alpha07 require consumers to compile against compileSdk 36.1. AGP 9.1 does not read that field and builds happily; AGP 9.3 enforces it and fails `checkDebugAarMetadata`. Bumping alpha02 to alpha07 therefore moved every consumer's floor from compileSdk 35 to 36.1, invisibly, for as long as they stayed on AGP 9.1. alpha05 is the newest release that asks only for compileSdk 35. Nothing in the plugin needs what alpha06 added: the fix for the unmapped Android 16 status code falls back on any unrecognised value rather than naming the new constant, so it does not depend on the constant existing. Keeping the example ahead of the `flutter create` template is what caught this, which is the argument for keeping it there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Collaborator
Author
|
Closed as a side effect of renaming the branch Superseded by #150, same commits. |
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.
Makes the plugin installable and buildable again on the current toolchain, and adds Swift Package Manager support. Targets
6.0.0-dev.1.The motivating case: an app that wants
authenticationRequired: falseto hold an OAuth refresh token could not adopt this plugin at all. Three separate blockers, each verified with a build rather than an analyze.1.
win32pin made the plugin unresolvableEvery published 5.x pinned
win32 >=2.0.0 <6.0.0, so it could not resolve alongsidepackage_info_plus >=10.1.0, which needswin32 ^6.0.1:Lifting the pin means porting to the win32 6.0 API:
TEXT()is gone in favour ofString.toPcwstr()/toPwstr(), theCred*calls returnWin32Result<bool>carrying the last-error code instead of aBOOLplusGetLastError(), and the enum constants are top-level extension types. Allocations moved into anArena.This was never a Windows-only bug.
lib/biometric_storage.dartre-exports the Windows implementation underif (dart.library.io), and Flutter generatesdart_plugin_registrant.dartfor every platform at once. An iOS release build of a consuming app contains:So a
package:win32break fails an iOS build. A federatedbiometric_storage_windowssplit would not change that — the registrant would import that package instead, on every platform. The defence is a compile:test/biometric_storage_test.dartnow imports the public barrel rather thansrc/, soflutter teston any host compiles the win32 bindings, and CI runs the suite on Windows too. Verified by reintroducing aTEXT()call and watching the original error reappear as a test failure.Also fixed while in there: writing an empty value reached
Uint8List.toNative(), which rejects empty lists. And thefileName:key underwindows:was a web-only key the tool never read.2. Swift Package Manager
Flutter 3.44+ defaults to SwiftPM and falls back to CocoaPods for any plugin without a
Package.swift— so adding this plugin to a migrated app silently regenerated its Podfiles. The CocoaPods registry also goes read-only on 2 December 2026.Both are shipped: a
Package.swift(with theFlutterFrameworkdependency the tool now expects) alongside the podspec. The sources moved todarwin/undersharedDarwinSource: true, which removes the symlinkios/Classesused to reach intomacos/Classesand the Objective-C shim — neither survives a SwiftPM target.Verified by building an app that also depends on
package_info_plus ^10.2.1: iOS and macOS both build with no Podfile generated, and the plugin's Swift symbols are in the linked binaries. Then verified again with SwiftPM disabled, so the podspec path still works.Visible change: the macOS plugin class is
BiometricStoragePluginrather thanBiometricStorageMacOSPlugin, and iOS no longer vendsBiometricStoragePlugin.h. Flutter generates the registrant for both, so this only matters to someone registering the plugin by hand.3. Staleness
Last stable was 5.0.1, roughly two years old.
fragment-ktx1.9.0,core-ktx1.18.0, slf4j 2.0.18, kotlin-logging 8.lintOptionswas removed in AGP 9, so it becamelint. The plugin no longer applies the Kotlin Gradle Plugin itself — AGP 9 warns about that and future Flutter releases reject it.androidx.biometricis held at 1.4.0-alpha05 deliberately. alpha06 addedminCompileMinorSdk=1, which forces consumers to compileSdk 36.1. AGP 9.1 ignores that field, AGP 9.3 enforces it — so a bump to alpha07 raises every consumer's floor invisibly. alpha05 is the newest that asks only for compileSdk 35.flutter createtemplate on purpose, which is what caught the item above.actions/checkout@v1→ v4, awebjob that set up Flutter on Windows and then did nothing → a real Windows test run, plus formatting and--fatal-infosanalysis, and an assertion that no Podfile appeared. The example is a separate package, so analyze and test now run there too.logging_appenders1.1 → 2.0 in the example, and its widget test — which had never passed, asserting a string the UI never contained — replaced with a real smoke test.Fixes
canAuthenticate()threw on any status code it did not recognise. Android 16 addedBIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS(21) and every call blew up. Unmapped codes now report asCanAuthenticateResponse.statusUnknownand log, which is version-independent rather than a fix for code 21 specifically.Could not find method jvmToolchain(). The call is gone; the JVM target is set insidepluginManager.withPlugin('org.jetbrains.kotlin.android').jvmToolchain(17)that biometric_storage does not build with Java 21 #117 removed and a9717ef reinstated. Without it a JDK 17 toolchain is no longer required; the example builds under JDK 25.Supersedes
sharedDarwinSourcerather than duplicating them underios/andmacos/.Two more are already obsolete, though not because of this PR — worth closing separately:
iosBiometricOnly, shipped asdarwinBiometricOnlyin 5.0.1.promptInfoparameter onread/writein 5.0.0.Documentation corrections
The README presented
FlutterFragmentActivityand aTheme.AppCompattheme as unconditional requirements, which is what made adoption look invasive. Both are conditions on showing a prompt:withAuthreturns straight to its callback whenauthenticationRequiredis false, soattachedActivityis never read andBiometricPromptis never constructed.androidx.biometriconly draws its own AppCompat dialog whenisUsingFingerprintDialog()holds:SDK_INT < 28, API 28 without a fingerprint sensor, or a device on theshouldUseFingerprintForCryptolist. The README said "Android < 29".Also documented:
IosPromptInfo.saveTitle/accessTitleare not rendered on a Face ID device. Checked on an iPhone Xr running iOS 18.7.9 with a build that set the deprecatedkSecUseOperationPromptand its replacementLAContext.localizedReasonto different marker strings — a screen recording of a read shows neither. They still reach the system, and Touch ID and macOS do draw them.kSecUseOperationPrompt, deprecated since iOS 14 / macOS 11, is replaced bylocalizedReasonin this PR; the strings were invisible under Face ID before the change too.Breaking
Requires Dart 3.10 / Flutter 3.44, which is
win326.0.1's own floor and where SwiftPM became the default. The plugin still declares AGP 8.13.2 rather than 9.x, so it does not drag consumers onto Gradle 9.Not addressed
keychain-access-groupsentitlement resolves$(AppIdentifierPrefix)from the signing team, so its Runner cannot be built without a development certificate.android.builtInKotlincannot be turned on yet — every AGP from 9.1.0 to 9.3.2 bundles KGP 2.2.10, below the 2.2.20 Flutter treats as a hard error.dart:html) was already fixed onmainin 5.1.0-rc.3; 5.0.1 is the last stable, so this release is the first to carry it to users.Not published to pub.dev.
🤖 Generated with Claude Code