[local_auth] Convert iOS/macOS to Swift#9459
Conversation
stuartmorgan-g
left a comment
There was a problem hiding this comment.
I did this conversion manually (as a way to practice Swift); no auto-conversion or AI tools involved. I deliberately did it in-place, pretty much line by line, to simplify side-by-side review. I've left notes about changes that aren't just direct language conversion below.
There was a problem hiding this comment.
This can mostly be compared side-by-side to FLALocalAuthPlugin_Test.h
There was a problem hiding this comment.
This should be renamed in a follow-up; I didn't rename it here because there are enough changes that git would probably not show it as a diff, making review harder.
| // Use of this source code is governed by a BSD-style license that can be | ||
| // found in the LICENSE file. | ||
|
|
||
| import LocalAuthentication |
There was a problem hiding this comment.
This should always have been here, but transitive includes in Obj-C masked the missing include.
|
|
||
| /// A context factory that returns preset contexts. | ||
| final class StubAuthContextFactory: NSObject, FLADAuthContextFactory { | ||
| final class StubAuthContextFactory: AuthContextFactory { |
There was a problem hiding this comment.
I removed NSObject from all the test implementations since the protocols aren't Obj-C now, so it was just cruft.
| var contexts: [FLADAuthContext] | ||
| init(contexts: [FLADAuthContext]) { | ||
| var contexts: [AuthContext] | ||
| init(contexts: [AuthContext]) { |
There was a problem hiding this comment.
All the Pigeon-generated type names changed because there's no more prefixing.
|
|
||
| /// A flutter plugin for local authentication. | ||
| // TODO(stuartmorgan): Remove the @unchecked Sendable, and convert to strict thread enforcement. | ||
| // This was added to minimize the changes while converting from Swift to Objective-C. |
There was a problem hiding this comment.
This was unfortunate, but it was the best I could come up with to fix all the warnings without significant restructuring. Without this, the places where we dispatch from a potentially-background-thread-callback back to a main thread call to a method on self triggers warnings about self not being Sendable.
There are ways to fix that with more static methods, passing more state around instead of using self, etc., but they would require refactoring that I didn't want to combine with the language conversion as it would have made side-by-side review nearly impossible.
There was a problem hiding this comment.
I think let's keep @unchecked Sendable for now. I plan to look closer into Swift concurrency later this year
| self.handleError(authError, options: options, strings: strings, completion: completion) | ||
| } else { | ||
| // This should not happen according to docs, but if it ever does the plugin should still | ||
| // fire the completion. |
There was a problem hiding this comment.
This branch is new. In Obj-C, we just assumed the docs weren't lying about the error being set if it returned false, but that's not type safe in Swift, and since docs have lied about nullability before I wanted a fallback instead of a crash from force-unwrapping.
| animated: true, | ||
| completion: nil) | ||
| } else { | ||
| // TODO(stuartmorgan): Create a new error code for failure to show UI, and return it here. |
There was a problem hiding this comment.
This branch is new. We were failing to handle the case where there is no view before, so I added minimal handling for now.
| self.handleResult(succeeded: false, completion: completion) | ||
| } | ||
| } else { | ||
| alert.runModal() |
There was a problem hiding this comment.
This branch is new. We were failing to handle the case where there is no view before.
We could fail like on iOS (see below), but since macOS has a simple way to handle showing an alert without a view I went with that option.
| } else { | ||
| // The Obj-C declaration of evaluatePolicy defines the callback type as NSError*, but the | ||
| // Swift version is (any Error)?, so provide a fallback in case somehow the type is not | ||
| // NSError. |
There was a problem hiding this comment.
This branch is new, for the reason described here.
This comment was marked as resolved.
This comment was marked as resolved.
| var localizedFallbackTitle: String? | ||
|
|
||
| func canEvaluatePolicy(_ policy: LAPolicy) throws { | ||
| func canEvaluatePolicy(_ policy: LAPolicy, error: NSErrorPointer) -> Bool { |
There was a problem hiding this comment.
curious if this also conforms to the protocol:
func canEvaluatePolicy(_ policy: LAPolicy, error: inout NSError?) -> Bool
There was a problem hiding this comment.
The goal is to match LAContext, and what I have here is the signature according to Apple:
https://developer.apple.com/documentation/localauthentication/lacontext/canevaluatepolicy(_:error:)
| XCTAssertNil(error) | ||
| switch resultDetails { | ||
| case .success(let successDetails): | ||
| XCTAssertEqual(successDetails.result, AuthResult.success) |
There was a problem hiding this comment.
nit: XCTAssertEqual(successDetails.result, .success)
There was a problem hiding this comment.
Done throughout.
| XCTAssertEqual(result!.count, 1) | ||
| XCTAssertEqual(result![0].value, FLADAuthBiometric.face) | ||
| XCTAssertNil(error) | ||
| do { |
There was a problem hiding this comment.
here and other tests - we can mark the function signature as throws, and remove this do/catch block
There was a problem hiding this comment.
Done throughout.
| return AuthStrings( | ||
| reason: "a reason", lockOut: "locked out", goToSettingsButton: "Go To Settings", | ||
| goToSettingsDescription: "Settings", cancelButton: "Cancel", | ||
| localizedFallbackTitle: localizedFallbackTitle) |
There was a problem hiding this comment.
nit: separate lines for each items
(https://google.github.io/swift/ -> Line-Wrapping -> "Comma-delimited lists are only laid out in one direction: horizontally or vertically. ")
There was a problem hiding this comment.
Done. I forgot swift-format annoyingly doesn't handle this.
| #endif | ||
|
|
||
| /// A default context factory that wraps standard LAContext allocation. | ||
| struct DefaultAuthContextFactory: AuthContextFactory { |
There was a problem hiding this comment.
nit: use class unless we need value semantics of struct (since struct comes with overhead)
There was a problem hiding this comment.
What's the overhead? I thought it was the opposite; https://developer.apple.com/documentation/swift/choosing-between-structures-and-classes#Overview says to prefer structs by default.
There was a problem hiding this comment.
What's the overhead?
Value types increase both build time and binary size. Though Apple has improved it over the years, so it shouldn't be our key consideration.
The key consideration is whether we want value or reference semantics (which should be the "control identity" in the link you provided)
For example, factories in general should be reference types, because they often share state, so we don't want multiple factories. This particular factory is stateless, so not the end of the world, but still, semantically we don't want multiple factories when passing it around.
The next example is more interesting:
struct DefaultAlertController {
var controller: UIAlertController
}
If we use value type like here, imagine this code:
let c1 = DefaultAlertController(...)
let c2 = c1
Then we have 2 separate values (c1 and c2), but both pointing to the same underlying UIAlertController, which is conceptually misleading (it looks like you have created 2 distinct controllers, but you haven't).
There was a problem hiding this comment.
Makes sense! Changed all of them.
| ) { | ||
| if success { | ||
| handleResult(succeeded: true, completion: completion) | ||
| } else { |
There was a problem hiding this comment.
nit: return in success to reduce indentation?
if success {
handle...
return
}
...
| LAError.biometryLockout, | ||
| LAError.userFallback, | ||
| LAError.passcodeNotSet, | ||
| LAError.authenticationFailed: |
There was a problem hiding this comment.
nit: can infer these (e.g. case .biometryNotAvailable)
| completion( | ||
| .success( | ||
| AuthResultDetails( | ||
| result: succeeded ? AuthResult.success : AuthResult.failure, |
| for sheetWindow: NSWindow, | ||
| completionHandler handler: ((NSApplication.ModalResponse) -> Void)? | ||
| ) | ||
| @MainActor |
There was a problem hiding this comment.
we can mark the whole protocol @MainActor so no need each function. (same below)
There was a problem hiding this comment.
This is problematic until we annotate both Pigeon-generated methods and the FlutterPlugin registration methods as @MainActor, because it cascades through other code that ends up in places like the creation of the default implementations in register(with:), which aren't annotated @MainActor even though in practice they are.
It's much easier for now to annotate the specific methods.
| /// instances in unit tests. | ||
| protocol AuthAlertController { | ||
| @MainActor | ||
| func addAction(_ action: UIAlertAction) |
There was a problem hiding this comment.
nit: add a blank line between each members (same below)
stuartmorgan-g
left a comment
There was a problem hiding this comment.
Thanks for the Swift tips!
| var localizedFallbackTitle: String? | ||
|
|
||
| func canEvaluatePolicy(_ policy: LAPolicy) throws { | ||
| func canEvaluatePolicy(_ policy: LAPolicy, error: NSErrorPointer) -> Bool { |
There was a problem hiding this comment.
The goal is to match LAContext, and what I have here is the signature according to Apple:
https://developer.apple.com/documentation/localauthentication/lacontext/canevaluatepolicy(_:error:)
| XCTAssertNil(error) | ||
| switch resultDetails { | ||
| case .success(let successDetails): | ||
| XCTAssertEqual(successDetails.result, AuthResult.success) |
There was a problem hiding this comment.
Done throughout.
| XCTAssertEqual(result!.count, 1) | ||
| XCTAssertEqual(result![0].value, FLADAuthBiometric.face) | ||
| XCTAssertNil(error) | ||
| do { |
There was a problem hiding this comment.
Done throughout.
| return AuthStrings( | ||
| reason: "a reason", lockOut: "locked out", goToSettingsButton: "Go To Settings", | ||
| goToSettingsDescription: "Settings", cancelButton: "Cancel", | ||
| localizedFallbackTitle: localizedFallbackTitle) |
There was a problem hiding this comment.
Done. I forgot swift-format annoyingly doesn't handle this.
| #endif | ||
|
|
||
| /// A default context factory that wraps standard LAContext allocation. | ||
| struct DefaultAuthContextFactory: AuthContextFactory { |
There was a problem hiding this comment.
Makes sense! Changed all of them.
| completion( | ||
| .success( | ||
| AuthResultDetails( | ||
| result: succeeded ? AuthResult.success : AuthResult.failure, |
| for sheetWindow: NSWindow, | ||
| completionHandler handler: ((NSApplication.ModalResponse) -> Void)? | ||
| ) | ||
| @MainActor |
There was a problem hiding this comment.
This is problematic until we annotate both Pigeon-generated methods and the FlutterPlugin registration methods as @MainActor, because it cascades through other code that ends up in places like the creation of the default implementations in register(with:), which aren't annotated @MainActor even though in practice they are.
It's much easier for now to annotate the specific methods.
| /// instances in unit tests. | ||
| protocol AuthAlertController { | ||
| @MainActor | ||
| func addAction(_ action: UIAlertAction) |
|
|
||
| #if os(iOS) | ||
| /// A default alert controller that wraps UIAlertController. | ||
| struct DefaultAlertController: AuthAlertController { |
| /// A default alert controller that wraps UIAlertController. | ||
| struct DefaultAlertController: AuthAlertController { | ||
| /// The wrapped alert controller. | ||
| var controller: UIAlertController |
| #endif | ||
|
|
||
| /// A default context factory that wraps standard LAContext allocation. | ||
| class DefaultAuthContextFactory: AuthContextFactory { |
There was a problem hiding this comment.
nit: these classes can all be final
| // MARK: - | ||
|
|
||
| /// A data container for sticky auth state. | ||
| struct StickyAuthState { |
There was a problem hiding this comment.
thanks. not straightforward but interesting name
| policy, | ||
| localizedReason: strings.reason | ||
| ) { (success: Bool, error: Error?) in | ||
| DispatchQueue.main.async { [weak self] in |
There was a problem hiding this comment.
I think you have to put [weak self] in outer closure context.evaluatePolicy, otherwise it would remain strong when outer closure is still alive.
There was a problem hiding this comment.
Makes sense; done.
| #elseif os(iOS) | ||
| // TODO(stuartmorgan): Get the view controller from the view provider once it's possible. | ||
| // See https://github.com/flutter/flutter/issues/104117. | ||
| if let controller = UIApplication.shared.delegate?.window??.rootViewController { |
There was a problem hiding this comment.
nit: this is a good use case for guard let
There was a problem hiding this comment.
Converted to guard.
| strings: AuthStrings, | ||
| completion: @escaping (Result<AuthResultDetails, Error>) -> Void | ||
| ) { | ||
| var result = AuthResult.errorNotAvailable |
There was a problem hiding this comment.
nit: can be let result: AuthResult (prefer immutability). This will fail to compile if any of the case below forget to set the result.
There was a problem hiding this comment.
Done. I didn't know Swift could do this to; I'm a big fan of the pattern in Dart.
| /// Protocol for interacting with LAContext instances, abstracted to allow using mock/fake instances | ||
| /// in unit tests. | ||
| protocol AuthContext { | ||
| var localizedFallbackTitle: String? { get set } |
There was a problem hiding this comment.
nit: blank line between each members. Also wanna add doc comments?
|
autosubmit label was removed for flutter/packages/9459, because - The status or check suite Linux_android android_build_all_packages stable has failed. Please fix the issues identified (or deflake) before re-applying this label. |
flutter/packages@205c50f...e4fd6c0 2025-07-03 [email protected] [go_router] Add TODOs for meta migration (flutter/packages#9535) 2025-07-02 [email protected] [local_auth] Convert iOS/macOS to Swift (flutter/packages#9459) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/flutter-packages-flutter-autoroll Please CC [email protected] on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
flutter/packages@205c50f...e4fd6c0 2025-07-03 [email protected] [go_router] Add TODOs for meta migration (flutter/packages#9535) 2025-07-02 [email protected] [local_auth] Convert iOS/macOS to Swift (flutter/packages#9459) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/flutter-packages-flutter-autoroll Please CC [email protected] on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
flutter/packages@205c50f...e4fd6c0 2025-07-03 [email protected] [go_router] Add TODOs for meta migration (flutter/packages#9535) 2025-07-02 [email protected] [local_auth] Convert iOS/macOS to Swift (flutter/packages#9459) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/flutter-packages-flutter-autoroll Please CC [email protected] on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
Rewrites the iOS/macOS implementation in Swift. This is an in-place rewrite with minimal changes, to minimize the chances of breakage, and to simplify review. Future refactorings can improve the Swift structure (e.g., fully adopting thread enforcement). Fixes flutter/flutter#119104 ## Pre-Review Checklist [^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling.
flutter/packages@205c50f...e4fd6c0 2025-07-03 [email protected] [go_router] Add TODOs for meta migration (flutter/packages#9535) 2025-07-02 [email protected] [local_auth] Convert iOS/macOS to Swift (flutter/packages#9459) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/flutter-packages-flutter-autoroll Please CC [email protected] on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
Rewrites the iOS/macOS implementation in Swift. This is an in-place rewrite with minimal changes, to minimize the chances of breakage, and to simplify review. Future refactorings can improve the Swift structure (e.g., fully adopting thread enforcement).
Fixes flutter/flutter#119104
Pre-Review Checklist
[shared_preferences]pubspec.yamlwith an appropriate new version according to the pub versioning philosophy, or I have commented below to indicate which version change exemption this PR falls under1.CHANGELOG.mdto add a description of the change, following repository CHANGELOG style, or I have commented below to indicate which CHANGELOG exemption this PR falls under1.///).Footnotes
Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling. ↩ ↩2 ↩3