Skip to content

Commit 27896d1

Browse files
[local_auth_darwin] macOS Support (flutter#6267)
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].*
1 parent 79fc248 commit 27896d1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+2279
-70
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,5 +75,6 @@ Amir Panahandeh <[email protected]>
7575
Daniele Cambi <[email protected]>
7676
Michele Benedetti <[email protected]>
7777
Taskulu LDA <[email protected]>
78+
Alexander Rabin <[email protected]>
7879
LinXunFeng <[email protected]>
7980
Hashir Shoaib <[email protected]>

packages/local_auth/local_auth_darwin/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 1.4.0
2+
3+
* Adds macOS support.
4+
15
## 1.3.1
26

37
* Adjusts implementation for improved testability, and removes use of OCMock.

packages/local_auth/local_auth_darwin/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# local_auth_darwin
22

3-
The iOS implementation of [`local_auth`][1].
3+
The iOS and macOS implementation of [`local_auth`][1].
44

55
## Usage
66

packages/local_auth/local_auth_darwin/darwin/Tests/FLALocalAuthPluginTests.swift

Lines changed: 182 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,22 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
import Flutter
65
import XCTest
76

87
@testable import local_auth_darwin
98

9+
#if os(iOS)
10+
import Flutter
11+
#else
12+
import FlutterMacOS
13+
#endif
14+
1015
// Set a long timeout to avoid flake due to slow CI.
1116
private let timeout: TimeInterval = 30.0
1217

1318
/// A context factory that returns preset contexts.
1419
final class StubAuthContextFactory: NSObject, FLADAuthContextFactory {
20+
1521
var contexts: [FLADAuthContext]
1622
init(contexts: [FLADAuthContext]) {
1723
self.contexts = contexts
@@ -23,6 +29,74 @@ final class StubAuthContextFactory: NSObject, FLADAuthContextFactory {
2329
}
2430
}
2531

32+
final class StubViewProvider: NSObject, FLAViewProvider {
33+
#if os(macOS)
34+
var view: NSView
35+
var window: NSWindow
36+
override init() {
37+
self.window = NSWindow()
38+
self.view = NSView()
39+
self.window.contentView = self.view
40+
}
41+
#endif
42+
}
43+
44+
#if os(macOS)
45+
final class TestAlert: NSObject, FLANSAlert {
46+
var messageText: String = ""
47+
var buttons: [String] = []
48+
var presentingWindow: NSWindow?
49+
50+
func addButton(withTitle title: String) -> NSButton {
51+
buttons.append(title)
52+
return NSButton() // The return value is not used by the plugin.
53+
}
54+
55+
func beginSheetModal(for sheetWindow: NSWindow) async -> NSApplication.ModalResponse {
56+
presentingWindow = sheetWindow
57+
return NSApplication.ModalResponse.OK
58+
}
59+
}
60+
#else
61+
final class TestAlertController: NSObject, FLAUIAlertController {
62+
var actions: [UIAlertAction] = []
63+
var presented = false
64+
var presentingViewController: UIViewController?
65+
66+
func add(_ action: UIAlertAction) {
67+
actions.append(action)
68+
}
69+
70+
func present(
71+
on presentingViewController: UIViewController, animated flag: Bool,
72+
completion: (() -> Void)? = nil
73+
) {
74+
presented = true
75+
self.presentingViewController = presentingViewController
76+
}
77+
}
78+
#endif
79+
80+
final class StubAlertFactory: NSObject, FLADAlertFactory {
81+
#if os(macOS)
82+
var alert: TestAlert = TestAlert()
83+
#else
84+
var alertController: TestAlertController = TestAlertController()
85+
#endif
86+
87+
#if os(macOS)
88+
func createAlert() -> FLANSAlert {
89+
return self.alert
90+
}
91+
#else
92+
func createAlertController(
93+
withTitle title: String?, message: String?, preferredStyle: UIAlertController.Style
94+
) -> FLAUIAlertController {
95+
return self.alertController
96+
}
97+
#endif
98+
}
99+
26100
final class StubAuthContext: NSObject, FLADAuthContext {
27101
/// Whether calls to this stub are expected to be for biometric authentication.
28102
///
@@ -75,8 +149,12 @@ class FLALocalAuthPluginTests: XCTestCase {
75149

76150
func testSuccessfullAuthWithBiometrics() throws {
77151
let stubAuthContext = StubAuthContext()
152+
let alertFactory = StubAlertFactory()
153+
let viewProvider = StubViewProvider()
78154
let plugin = FLALocalAuthPlugin(
79-
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
155+
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]),
156+
alertFactory: alertFactory, viewProvider: viewProvider
157+
)
80158

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

100178
func testSuccessfullAuthWithoutBiometrics() {
101179
let stubAuthContext = StubAuthContext()
180+
let alertFactory = StubAlertFactory()
181+
let viewProvider = StubViewProvider()
182+
102183
let plugin = FLALocalAuthPlugin(
103-
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
184+
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]),
185+
alertFactory: alertFactory, viewProvider: viewProvider)
104186

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

124206
func testFailedAuthWithBiometrics() {
125207
let stubAuthContext = StubAuthContext()
208+
let alertFactory = StubAlertFactory()
209+
let viewProvider = StubViewProvider()
126210
let plugin = FLALocalAuthPlugin(
127-
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
211+
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]),
212+
alertFactory: alertFactory, viewProvider: viewProvider)
128213

129214
let strings = createAuthStrings()
130215
stubAuthContext.expectBiometrics = true
@@ -153,8 +238,11 @@ class FLALocalAuthPluginTests: XCTestCase {
153238

154239
func testFailedWithUnknownErrorCode() {
155240
let stubAuthContext = StubAuthContext()
241+
let alertFactory = StubAlertFactory()
242+
let viewProvider = StubViewProvider()
156243
let plugin = FLALocalAuthPlugin(
157-
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
244+
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]),
245+
alertFactory: alertFactory, viewProvider: viewProvider)
158246

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

178266
func testSystemCancelledWithoutStickyAuth() {
179267
let stubAuthContext = StubAuthContext()
268+
let alertFactory = StubAlertFactory()
269+
let viewProvider = StubViewProvider()
180270
let plugin = FLALocalAuthPlugin(
181-
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
271+
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]),
272+
alertFactory: alertFactory, viewProvider: viewProvider)
182273

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

202293
func testFailedAuthWithoutBiometrics() {
203294
let stubAuthContext = StubAuthContext()
295+
let alertFactory = StubAlertFactory()
296+
let viewProvider = StubViewProvider()
204297
let plugin = FLALocalAuthPlugin(
205-
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
298+
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]),
299+
alertFactory: alertFactory, viewProvider: viewProvider)
206300

207301
let strings = createAuthStrings()
208302
stubAuthContext.evaluateError = NSError(
@@ -228,10 +322,50 @@ class FLALocalAuthPluginTests: XCTestCase {
228322
self.waitForExpectations(timeout: timeout)
229323
}
230324

325+
func testFailedAuthShowsAlert() {
326+
let stubAuthContext = StubAuthContext()
327+
let alertFactory = StubAlertFactory()
328+
let viewProvider = StubViewProvider()
329+
let plugin = FLALocalAuthPlugin(
330+
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]),
331+
alertFactory: alertFactory, viewProvider: viewProvider)
332+
333+
let strings = createAuthStrings()
334+
stubAuthContext.canEvaluateError = NSError(
335+
domain: "error", code: LAError.biometryNotEnrolled.rawValue)
336+
337+
#if os(macOS)
338+
let expectation = expectation(description: "Result is called")
339+
#endif
340+
plugin.authenticate(
341+
with: FLADAuthOptions.make(
342+
withBiometricOnly: false,
343+
sticky: false,
344+
useErrorDialogs: true),
345+
strings: strings
346+
) { resultDetails, error in
347+
// TODO(stuartmorgan): Add a wrapper around UIAction to allow accessing the handler, so
348+
// that the test can trigger the callback on iOS as well, and then unfork this.
349+
#if os(macOS)
350+
expectation.fulfill()
351+
#endif
352+
}
353+
#if os(macOS)
354+
self.waitForExpectations(timeout: timeout)
355+
XCTAssertEqual(alertFactory.alert.presentingWindow, viewProvider.view.window)
356+
#else
357+
XCTAssertTrue(alertFactory.alertController.presented)
358+
XCTAssertEqual(alertFactory.alertController.actions.count, 2)
359+
#endif
360+
}
361+
231362
func testLocalizedFallbackTitle() {
232363
let stubAuthContext = StubAuthContext()
364+
let alertFactory = StubAlertFactory()
365+
let viewProvider = StubViewProvider()
233366
let plugin = FLALocalAuthPlugin(
234-
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
367+
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]),
368+
alertFactory: alertFactory, viewProvider: viewProvider)
235369

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

256390
func testSkippedLocalizedFallbackTitle() {
257391
let stubAuthContext = StubAuthContext()
392+
let alertFactory = StubAlertFactory()
393+
let viewProvider = StubViewProvider()
258394
let plugin = FLALocalAuthPlugin(
259-
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
395+
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]),
396+
alertFactory: alertFactory, viewProvider: viewProvider)
260397

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

279416
func testDeviceSupportsBiometrics_withEnrolledHardware() {
280417
let stubAuthContext = StubAuthContext()
418+
let alertFactory = StubAlertFactory()
419+
let viewProvider = StubViewProvider()
281420
let plugin = FLALocalAuthPlugin(
282-
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
421+
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]),
422+
alertFactory: alertFactory, viewProvider: viewProvider)
283423

284424
stubAuthContext.expectBiometrics = true
285425

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

292432
func testDeviceSupportsBiometrics_withNonEnrolledHardware() {
293433
let stubAuthContext = StubAuthContext()
434+
let alertFactory = StubAlertFactory()
435+
let viewProvider = StubViewProvider()
294436
let plugin = FLALocalAuthPlugin(
295-
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
437+
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]),
438+
alertFactory: alertFactory, viewProvider: viewProvider)
296439

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

307450
func testDeviceSupportsBiometrics_withNoBiometricHardware() {
308451
let stubAuthContext = StubAuthContext()
452+
let alertFactory = StubAlertFactory()
453+
let viewProvider = StubViewProvider()
309454
let plugin = FLALocalAuthPlugin(
310-
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
455+
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]),
456+
alertFactory: alertFactory, viewProvider: viewProvider)
311457

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

321467
func testGetEnrolledBiometricsWithFaceID() {
322468
let stubAuthContext = StubAuthContext()
469+
let alertFactory = StubAlertFactory()
470+
let viewProvider = StubViewProvider()
323471
let plugin = FLALocalAuthPlugin(
324-
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
472+
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]),
473+
alertFactory: alertFactory, viewProvider: viewProvider)
325474

326475
stubAuthContext.expectBiometrics = true
327-
stubAuthContext.biometryType = .faceID
476+
if #available(iOS 11, macOS 10.15, *) {
477+
stubAuthContext.biometryType = .faceID
478+
479+
}
328480

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

336488
func testGetEnrolledBiometricsWithTouchID() {
337489
let stubAuthContext = StubAuthContext()
490+
let alertFactory = StubAlertFactory()
491+
let viewProvider = StubViewProvider()
338492
let plugin = FLALocalAuthPlugin(
339-
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
493+
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]),
494+
alertFactory: alertFactory, viewProvider: viewProvider)
340495

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

351506
func testGetEnrolledBiometricsWithoutEnrolledHardware() {
352507
let stubAuthContext = StubAuthContext()
508+
let alertFactory = StubAlertFactory()
509+
let viewProvider = StubViewProvider()
353510
let plugin = FLALocalAuthPlugin(
354-
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
511+
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]),
512+
alertFactory: alertFactory, viewProvider: viewProvider)
355513

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

366524
func testIsDeviceSupportedHandlesSupported() {
367525
let stubAuthContext = StubAuthContext()
526+
let alertFactory = StubAlertFactory()
527+
let viewProvider = StubViewProvider()
368528
let plugin = FLALocalAuthPlugin(
369-
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
529+
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]),
530+
alertFactory: alertFactory, viewProvider: viewProvider)
370531

371532
var error: FlutterError?
372533
let result = plugin.isDeviceSupportedWithError(&error)
@@ -378,8 +539,11 @@ class FLALocalAuthPluginTests: XCTestCase {
378539
let stubAuthContext = StubAuthContext()
379540
// An arbitrary error to cause canEvaluatePolicy to return false.
380541
stubAuthContext.canEvaluateError = NSError(domain: "error", code: 1)
542+
let alertFactory = StubAlertFactory()
543+
let viewProvider = StubViewProvider()
381544
let plugin = FLALocalAuthPlugin(
382-
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
545+
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]),
546+
alertFactory: alertFactory, viewProvider: viewProvider)
383547

384548
var error: FlutterError?
385549
let result = plugin.isDeviceSupportedWithError(&error)

packages/local_auth/local_auth_darwin/darwin/local_auth_darwin.podspec

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@ Downloaded by pub (not CocoaPods).
1616
s.documentation_url = 'https://pub.dev/packages/local_auth_darwin'
1717
s.source_files = 'local_auth_darwin/Sources/local_auth_darwin/**/*.{h,m}'
1818
s.public_header_files = 'local_auth_darwin/Sources/local_auth_darwin/include/**/*.h'
19-
s.dependency 'Flutter'
20-
s.platform = :ios, '12.0'
19+
s.ios.dependency 'Flutter'
20+
s.osx.dependency 'FlutterMacOS'
21+
s.ios.deployment_target = '12.0'
22+
s.osx.deployment_target = '10.14'
23+
2124
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
2225
s.resource_bundles = {'local_auth_darwin_privacy' => ['local_auth_darwin/Sources/local_auth_darwin/Resources/PrivacyInfo.xcprivacy']}
2326
end

0 commit comments

Comments
 (0)