win32 6, Swift Package Manager, and a toolchain refresh (6.0.0-dev.1) - #150
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>
Two findings from review. authenticationContext(reason:) only assigned localizedReason when the reason was non-empty, so a call passing an empty saveTitle/accessTitle inherited the previous call's wording — a read showing the save prompt's text. Only reachable through a context reused via darwinTouchIDAuthenticationForceReuseContextDuration, since a fresh context has nothing to inherit, and only when an app passes an explicitly empty string. The key this replaced was per-query and could not carry over. Now assigned unconditionally, which also restores the old behaviour exactly: `kSecUseOperationPrompt` was set to the empty string in that case rather than removed. The win32 bindings had compile coverage on every host but no runtime coverage anywhere, which is how a use-after-free survived in every 5.x: `read()` decoded the `CredentialBlob.asTypedList()` view *after* `CredFree` released it. The rewrite already copies the bytes out first, so this commit adds the test that would have caught it, against the real credential store on the Windows CI job. It imports `src/` rather than the barrel deliberately — the barrel reaches the class through a conditional export whose default branch is an empty stub, and the analyzer resolves to the stub. Also records two ways the Gradle Kotlin hook can go quiet later: AGP's built-in Kotlin does not register the `org.jetbrains.kotlin.android` id, so the block dies when `android.builtInKotlin=true` becomes usable; and an app with no KGP visible anywhere leaves our sources uncompiled with only a logged warning. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
jvmToolchain(17) requires a JDK 17 to be discoverable, which is the constraint #117 and #140 were both about. The plugin stopped calling it in this branch, but the example app still did, so "builds under JDK 25" was only true where a JDK 17 also happened to be installed. jvmTarget emits 17 bytecode from whatever JDK runs Gradle, and is what `flutter create` emits today. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Second review round found the one mutation the new win32 tests could not catch: changing `namePrefix` orphans every existing user's stored credential while all four tests stay green, because write and read both go through it. One assertion pins it. `.gitattributes` forces LF on the files CI formats and both platforms build. The Windows leg of the format check passes today only because the runner's git config happens to leave line endings alone; this makes it a guarantee, and stops contributors on Windows with core.autocrlf=true seeing a tree that never looks formatted. Also corrects the age of the use-after-free. It did not ship with the initial Windows support: 3aef1c3 decoded the blob with no CredFree in that path at all. a9e3944 moved the CredFree call in between the asTypedList view and the decode, and the next release was 1.1.0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
It is maintainer-facing instructions and was shipping to every consumer. The trap: .pubignore is consulted INSTEAD OF .gitignore, and not only for its own directory — adding a one-line version stopped every nested .gitignore being honoured as well, which pulled seven example/build/web artifacts into the archive. So it restates the exclusions rather than only adding one. Measured rather than reasoned: `flutter pub publish --dry-run` goes from 151 to 150 files, and diffing the two file lists shows CLAUDE.md as the only change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`lib/src/biometric_storage.dart` imported `dart:io` unconditionally, for `Platform` alone. That library is reachable from the web implementation, so the import marked the whole package wasm-incompatible and cost it pana's `is:wasm-ready` tag and ten points — the only deduction it had. The host OS now arrives through a conditional import. The io branch is still `dart:io`'s own `Platform.operatingSystem`, so nothing changes on any platform that has one. Deliberately not `defaultTargetPlatform`, which needs no new files and would have been the shorter diff. It answers a different question — the platform Flutter is *emulating* — so an app setting `debugDefaultTargetPlatformOverride`, routine for exercising Cupertino widgets on desktop, would have made us send iOS arguments to an Android plugin. Not a trade worth making here. The web branch returns 'web' rather than throwing. Every caller sits inside MethodChannelBiometricStorage, which the web build never instantiates, so it should be unreachable — and if some path does reach it, 'web' degrades the way the callers already expect instead of turning a dead branch into a crash. pana now reports every section at full marks with is:wasm-ready granted. Verified compiling: wasm (main.dart.wasm emitted), JS, and Android for the io branch. #145 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
@copilot review this PR |
CI is green across all jobs (Ubuntu, Windows, iOS build with no-Podfile assertion, Android, Linux, web). Here is a full review. SummaryThe three motivating changes are each well-executed. The win32 rewrite is correct; the SwiftPM migration is complete; the dependency and toolchain updates are conservative in the right places. A few things are worth noting.
|
This branch untracked example/{ios,macos}/Podfile and their lockfiles when it
moved to Swift Package Manager, but taught no .gitignore about them. They are
still generated the moment anyone verifies the CocoaPods path still works —
which this PR's own description tells them to do — and from there they went
straight into `pub publish`, along with the r8 config the root .gitignore covers
but .pubignore did not restate, and dartdoc output nothing has ever covered.
Also corrects what the .pubignore comment claims. Nested .gitignore files ARE
still honoured; .pubignore only replaces the one in its own directory. The
artifacts the one-line version leaked were root .dart_tool output, not
example/build/web as the comment said, so it would have misled the next person
to edit it. And `/pubspec.lock` was never needed: pub does not package a
lockfile at any path.
Verified against a deliberately dirty tree — seeded Podfiles, lockfiles, a Pods
manifest, the r8 config and doc/api output — rather than the clean checkout the
first attempt measured. The archive gains none of them.
README: wincreds.h is wincred.h.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.Two more bugs fell out of the rewrite. Writing an empty value reached
Uint8List.toNative(), which rejects empty lists. Andread()decoded theCredentialBlob.asTypedList()view afterCredFreehad released it — a use-after-free present in every 5.x, now fixed by copying the bytes out first.Because that survived for years, the bindings now have runtime coverage rather than only compile coverage:
test/biometric_storage_win32_test.dartexercises write/read/delete against the real credential store and runs on the Windows CI job.The
fileName:key underwindows:was also 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
dart:htmlwent in 5.1.0-rc.3, butlib/src/biometric_storage.dartstill importeddart:iounconditionally forPlatform, which marked the whole package wasm-incompatible. The host OS now comes through a conditional import, withdart:iostill doing the work wherever it exists, so no platform changes behaviour.panagrantsis:wasm-readyand every section is at full marks; 5.0.1 being the last stable, this is also the first release to carry thedart:htmlremoval to users.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. The plugin no longer calls it at all, so building it no longer requires a JDK 17 toolchain to be present. The example app's ownjvmToolchain(17)is replaced byjvmTarget, which is whatflutter createemits today.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.Pre-existing bugs found while reviewing this PR
Deliberately not fixed here, to keep this reviewable. All six were verified against the code; a follow-up branched off this PR will pick them up. Ranked by severity:
Activityafter a configuration change.onReattachedToActivityForConfigChangesandonDetachedFromActivityForConfigChangesare both empty, so after a rotationattachedActivitystill points at the destroyed Activity andBiometricPromptgets a deadFragmentActivity. May explain ANR on Android 12 Device #142 and [Crash App]: Crash app on Samsung Note 10 Plus #143.result.errorarguments reversed atBiometricStoragePlugin.kt:152— an English sentence is delivered asPlatformException.code.initreplies twice when an argument is missing:requiredArgreplies with the error, then the unconditionalresult(true)runs anyway, so the caller sees success.canAuthenticatereports unknownLAErrors asunsupported, i.e. "plugin does not support platform", where Android now reportsstatusUnknown. Changing it is not cosmetic — callers treatstatusUnknownas usable.forceInitis ignored on Windows and Darwin although the Dart doc promises it throws; only Android implements it.read()collapses every failure tonull, so a caller cannot tell "nothing stored" from "the credential store failed". Non-not-found errors are logged at warning level, so not silent.Not published to pub.dev.
🤖 Generated with Claude Code