Skip to content

Commit

Permalink
[local_auth_darwin] macOS Support (flutter#6267)
Browse files Browse the repository at this point in the history
Adds macOS support for local_auth_darwin

![Screenshot 2024-03-05 at 8 30 35�AM](https://github.com/flutter/packages/assets/160153899/89bcfa78-b998-401e-869c-28b9d82a9229)

![Screenshot 2024-03-05 at 8 30 56�AM](https://github.com/flutter/packages/assets/160153899/69f0e215-1a7c-45eb-99a6-264458b0e771)

## Cancelled Example:

![Screenshot 2024-03-05 at 8 31 12�AM](https://github.com/flutter/packages/assets/160153899/1196b4e9-c010-4e96-994b-7467d1561ad1)

## Success Example

![Screenshot 2024-03-05 at 8 31 32�AM](https://github.com/flutter/packages/assets/160153899/acd0d550-3be2-46cf-957c-fbbe445abfa4)

## Error Example
<img width="912" alt="Screenshot 2024-03-05 at 4 01 58�PM" src="https://github.com/flutter/packages/assets/160153899/3a16eed5-d8b1-42a2-b6ab-ca82ade101ce">

*List which issues are fixed by this PR. You must list at least one issue.*

flutter#140685

*If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].*
  • Loading branch information
alexrabin-sentracam authored Aug 1, 2024
1 parent 79fc248 commit 27896d1
Show file tree
Hide file tree
Showing 45 changed files with 2,279 additions and 70 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,6 @@ Amir Panahandeh <[email protected]>
Daniele Cambi <[email protected]>
Michele Benedetti <[email protected]>
Taskulu LDA <[email protected]>
Alexander Rabin <[email protected]>
LinXunFeng <[email protected]>
Hashir Shoaib <[email protected]>
4 changes: 4 additions & 0 deletions packages/local_auth/local_auth_darwin/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 1.4.0

* Adds macOS support.

## 1.3.1

* Adjusts implementation for improved testability, and removes use of OCMock.
Expand Down
2 changes: 1 addition & 1 deletion packages/local_auth/local_auth_darwin/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# local_auth_darwin

The iOS implementation of [`local_auth`][1].
The iOS and macOS implementation of [`local_auth`][1].

## Usage

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,22 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import Flutter
import XCTest

@testable import local_auth_darwin

#if os(iOS)
import Flutter
#else
import FlutterMacOS
#endif

// Set a long timeout to avoid flake due to slow CI.
private let timeout: TimeInterval = 30.0

/// A context factory that returns preset contexts.
final class StubAuthContextFactory: NSObject, FLADAuthContextFactory {

var contexts: [FLADAuthContext]
init(contexts: [FLADAuthContext]) {
self.contexts = contexts
Expand All @@ -23,6 +29,74 @@ final class StubAuthContextFactory: NSObject, FLADAuthContextFactory {
}
}

final class StubViewProvider: NSObject, FLAViewProvider {
#if os(macOS)
var view: NSView
var window: NSWindow
override init() {
self.window = NSWindow()
self.view = NSView()
self.window.contentView = self.view
}
#endif
}

#if os(macOS)
final class TestAlert: NSObject, FLANSAlert {
var messageText: String = ""
var buttons: [String] = []
var presentingWindow: NSWindow?

func addButton(withTitle title: String) -> NSButton {
buttons.append(title)
return NSButton() // The return value is not used by the plugin.
}

func beginSheetModal(for sheetWindow: NSWindow) async -> NSApplication.ModalResponse {
presentingWindow = sheetWindow
return NSApplication.ModalResponse.OK
}
}
#else
final class TestAlertController: NSObject, FLAUIAlertController {
var actions: [UIAlertAction] = []
var presented = false
var presentingViewController: UIViewController?

func add(_ action: UIAlertAction) {
actions.append(action)
}

func present(
on presentingViewController: UIViewController, animated flag: Bool,
completion: (() -> Void)? = nil
) {
presented = true
self.presentingViewController = presentingViewController
}
}
#endif

final class StubAlertFactory: NSObject, FLADAlertFactory {
#if os(macOS)
var alert: TestAlert = TestAlert()
#else
var alertController: TestAlertController = TestAlertController()
#endif

#if os(macOS)
func createAlert() -> FLANSAlert {
return self.alert
}
#else
func createAlertController(
withTitle title: String?, message: String?, preferredStyle: UIAlertController.Style
) -> FLAUIAlertController {
return self.alertController
}
#endif
}

final class StubAuthContext: NSObject, FLADAuthContext {
/// Whether calls to this stub are expected to be for biometric authentication.
///
Expand Down Expand Up @@ -75,8 +149,12 @@ class FLALocalAuthPluginTests: XCTestCase {

func testSuccessfullAuthWithBiometrics() throws {
let stubAuthContext = StubAuthContext()
let alertFactory = StubAlertFactory()
let viewProvider = StubViewProvider()
let plugin = FLALocalAuthPlugin(
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]),
alertFactory: alertFactory, viewProvider: viewProvider
)

let strings = createAuthStrings()
stubAuthContext.expectBiometrics = true
Expand All @@ -99,8 +177,12 @@ class FLALocalAuthPluginTests: XCTestCase {

func testSuccessfullAuthWithoutBiometrics() {
let stubAuthContext = StubAuthContext()
let alertFactory = StubAlertFactory()
let viewProvider = StubViewProvider()

let plugin = FLALocalAuthPlugin(
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]),
alertFactory: alertFactory, viewProvider: viewProvider)

let strings = createAuthStrings()
stubAuthContext.evaluateResponse = true
Expand All @@ -123,8 +205,11 @@ class FLALocalAuthPluginTests: XCTestCase {

func testFailedAuthWithBiometrics() {
let stubAuthContext = StubAuthContext()
let alertFactory = StubAlertFactory()
let viewProvider = StubViewProvider()
let plugin = FLALocalAuthPlugin(
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]),
alertFactory: alertFactory, viewProvider: viewProvider)

let strings = createAuthStrings()
stubAuthContext.expectBiometrics = true
Expand Down Expand Up @@ -153,8 +238,11 @@ class FLALocalAuthPluginTests: XCTestCase {

func testFailedWithUnknownErrorCode() {
let stubAuthContext = StubAuthContext()
let alertFactory = StubAlertFactory()
let viewProvider = StubViewProvider()
let plugin = FLALocalAuthPlugin(
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]),
alertFactory: alertFactory, viewProvider: viewProvider)

let strings = createAuthStrings()
stubAuthContext.evaluateError = NSError(domain: "error", code: 99)
Expand All @@ -177,8 +265,11 @@ class FLALocalAuthPluginTests: XCTestCase {

func testSystemCancelledWithoutStickyAuth() {
let stubAuthContext = StubAuthContext()
let alertFactory = StubAlertFactory()
let viewProvider = StubViewProvider()
let plugin = FLALocalAuthPlugin(
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]),
alertFactory: alertFactory, viewProvider: viewProvider)

let strings = createAuthStrings()
stubAuthContext.evaluateError = NSError(domain: "error", code: LAError.systemCancel.rawValue)
Expand All @@ -201,8 +292,11 @@ class FLALocalAuthPluginTests: XCTestCase {

func testFailedAuthWithoutBiometrics() {
let stubAuthContext = StubAuthContext()
let alertFactory = StubAlertFactory()
let viewProvider = StubViewProvider()
let plugin = FLALocalAuthPlugin(
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]),
alertFactory: alertFactory, viewProvider: viewProvider)

let strings = createAuthStrings()
stubAuthContext.evaluateError = NSError(
Expand All @@ -228,10 +322,50 @@ class FLALocalAuthPluginTests: XCTestCase {
self.waitForExpectations(timeout: timeout)
}

func testFailedAuthShowsAlert() {
let stubAuthContext = StubAuthContext()
let alertFactory = StubAlertFactory()
let viewProvider = StubViewProvider()
let plugin = FLALocalAuthPlugin(
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]),
alertFactory: alertFactory, viewProvider: viewProvider)

let strings = createAuthStrings()
stubAuthContext.canEvaluateError = NSError(
domain: "error", code: LAError.biometryNotEnrolled.rawValue)

#if os(macOS)
let expectation = expectation(description: "Result is called")
#endif
plugin.authenticate(
with: FLADAuthOptions.make(
withBiometricOnly: false,
sticky: false,
useErrorDialogs: true),
strings: strings
) { resultDetails, error in
// TODO(stuartmorgan): Add a wrapper around UIAction to allow accessing the handler, so
// that the test can trigger the callback on iOS as well, and then unfork this.
#if os(macOS)
expectation.fulfill()
#endif
}
#if os(macOS)
self.waitForExpectations(timeout: timeout)
XCTAssertEqual(alertFactory.alert.presentingWindow, viewProvider.view.window)
#else
XCTAssertTrue(alertFactory.alertController.presented)
XCTAssertEqual(alertFactory.alertController.actions.count, 2)
#endif
}

func testLocalizedFallbackTitle() {
let stubAuthContext = StubAuthContext()
let alertFactory = StubAlertFactory()
let viewProvider = StubViewProvider()
let plugin = FLALocalAuthPlugin(
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]),
alertFactory: alertFactory, viewProvider: viewProvider)

let strings = createAuthStrings()
strings.localizedFallbackTitle = "a title"
Expand All @@ -255,8 +389,11 @@ class FLALocalAuthPluginTests: XCTestCase {

func testSkippedLocalizedFallbackTitle() {
let stubAuthContext = StubAuthContext()
let alertFactory = StubAlertFactory()
let viewProvider = StubViewProvider()
let plugin = FLALocalAuthPlugin(
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]),
alertFactory: alertFactory, viewProvider: viewProvider)

let strings = createAuthStrings()
strings.localizedFallbackTitle = nil
Expand All @@ -278,8 +415,11 @@ class FLALocalAuthPluginTests: XCTestCase {

func testDeviceSupportsBiometrics_withEnrolledHardware() {
let stubAuthContext = StubAuthContext()
let alertFactory = StubAlertFactory()
let viewProvider = StubViewProvider()
let plugin = FLALocalAuthPlugin(
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]),
alertFactory: alertFactory, viewProvider: viewProvider)

stubAuthContext.expectBiometrics = true

Expand All @@ -291,8 +431,11 @@ class FLALocalAuthPluginTests: XCTestCase {

func testDeviceSupportsBiometrics_withNonEnrolledHardware() {
let stubAuthContext = StubAuthContext()
let alertFactory = StubAlertFactory()
let viewProvider = StubViewProvider()
let plugin = FLALocalAuthPlugin(
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]),
alertFactory: alertFactory, viewProvider: viewProvider)

stubAuthContext.expectBiometrics = true
stubAuthContext.canEvaluateError = NSError(
Expand All @@ -306,8 +449,11 @@ class FLALocalAuthPluginTests: XCTestCase {

func testDeviceSupportsBiometrics_withNoBiometricHardware() {
let stubAuthContext = StubAuthContext()
let alertFactory = StubAlertFactory()
let viewProvider = StubViewProvider()
let plugin = FLALocalAuthPlugin(
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]),
alertFactory: alertFactory, viewProvider: viewProvider)

stubAuthContext.expectBiometrics = true
stubAuthContext.canEvaluateError = NSError(domain: "error", code: 0)
Expand All @@ -320,11 +466,17 @@ class FLALocalAuthPluginTests: XCTestCase {

func testGetEnrolledBiometricsWithFaceID() {
let stubAuthContext = StubAuthContext()
let alertFactory = StubAlertFactory()
let viewProvider = StubViewProvider()
let plugin = FLALocalAuthPlugin(
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]),
alertFactory: alertFactory, viewProvider: viewProvider)

stubAuthContext.expectBiometrics = true
stubAuthContext.biometryType = .faceID
if #available(iOS 11, macOS 10.15, *) {
stubAuthContext.biometryType = .faceID

}

var error: FlutterError?
let result = plugin.getEnrolledBiometricsWithError(&error)
Expand All @@ -335,8 +487,11 @@ class FLALocalAuthPluginTests: XCTestCase {

func testGetEnrolledBiometricsWithTouchID() {
let stubAuthContext = StubAuthContext()
let alertFactory = StubAlertFactory()
let viewProvider = StubViewProvider()
let plugin = FLALocalAuthPlugin(
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]),
alertFactory: alertFactory, viewProvider: viewProvider)

stubAuthContext.expectBiometrics = true
stubAuthContext.biometryType = .touchID
Expand All @@ -350,8 +505,11 @@ class FLALocalAuthPluginTests: XCTestCase {

func testGetEnrolledBiometricsWithoutEnrolledHardware() {
let stubAuthContext = StubAuthContext()
let alertFactory = StubAlertFactory()
let viewProvider = StubViewProvider()
let plugin = FLALocalAuthPlugin(
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]),
alertFactory: alertFactory, viewProvider: viewProvider)

stubAuthContext.expectBiometrics = true
stubAuthContext.canEvaluateError = NSError(
Expand All @@ -365,8 +523,11 @@ class FLALocalAuthPluginTests: XCTestCase {

func testIsDeviceSupportedHandlesSupported() {
let stubAuthContext = StubAuthContext()
let alertFactory = StubAlertFactory()
let viewProvider = StubViewProvider()
let plugin = FLALocalAuthPlugin(
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]),
alertFactory: alertFactory, viewProvider: viewProvider)

var error: FlutterError?
let result = plugin.isDeviceSupportedWithError(&error)
Expand All @@ -378,8 +539,11 @@ class FLALocalAuthPluginTests: XCTestCase {
let stubAuthContext = StubAuthContext()
// An arbitrary error to cause canEvaluatePolicy to return false.
stubAuthContext.canEvaluateError = NSError(domain: "error", code: 1)
let alertFactory = StubAlertFactory()
let viewProvider = StubViewProvider()
let plugin = FLALocalAuthPlugin(
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]),
alertFactory: alertFactory, viewProvider: viewProvider)

var error: FlutterError?
let result = plugin.isDeviceSupportedWithError(&error)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ Downloaded by pub (not CocoaPods).
s.documentation_url = 'https://pub.dev/packages/local_auth_darwin'
s.source_files = 'local_auth_darwin/Sources/local_auth_darwin/**/*.{h,m}'
s.public_header_files = 'local_auth_darwin/Sources/local_auth_darwin/include/**/*.h'
s.dependency 'Flutter'
s.platform = :ios, '12.0'
s.ios.dependency 'Flutter'
s.osx.dependency 'FlutterMacOS'
s.ios.deployment_target = '12.0'
s.osx.deployment_target = '10.14'

s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
s.resource_bundles = {'local_auth_darwin_privacy' => ['local_auth_darwin/Sources/local_auth_darwin/Resources/PrivacyInfo.xcprivacy']}
end
Loading

0 comments on commit 27896d1

Please sign in to comment.