Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/local_auth/local_auth_ios/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 1.0.4

* Fixes `deviceSupportsBiometrics` to return true when biometric hardware
is available but not enrolled.

## 1.0.3

* Adopts `Object.hash`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objectVersion = 50;
objects = {

/* Begin PBXBuildFile section */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,4 +269,281 @@ - (void)testSkippedLocalizedFallbackTitle {
[self waitForExpectationsWithTimeout:kTimeout handler:nil];
}

- (void)testDeviceSupportsBiometrics_withEnrolledHardware {
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init];
id mockAuthContext = OCMClassMock([LAContext class]);
plugin.authContextOverrides = @[ mockAuthContext ];

const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES);

FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"deviceSupportsBiometrics"
arguments:@{}];
XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"];
[plugin handleMethodCall:call
result:^(id _Nullable result) {
XCTAssertTrue([NSThread isMainThread]);
XCTAssertTrue([result isKindOfClass:[NSNumber class]]);
XCTAssertTrue([result boolValue]);
[expectation fulfill];
}];

[self waitForExpectationsWithTimeout:kTimeout handler:nil];
}

- (void)testDeviceSupportsBiometrics_withNonEnrolledHardware_iOS11 {
if (@available(iOS 11.0.1, *)) {
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init];
id mockAuthContext = OCMClassMock([LAContext class]);
plugin.authContextOverrides = @[ mockAuthContext ];

const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
void (^canEvaluatePolicyHandler)(NSInvocation *) = ^(NSInvocation *invocation) {
// Write error
NSError *__autoreleasing *authError;
[invocation getArgument:&authError atIndex:3];
*authError = [NSError errorWithDomain:@"error" code:LAErrorBiometryNotEnrolled userInfo:nil];
// Write return value
BOOL returnValue = NO;
NSValue *nsReturnValue = [NSValue valueWithBytes:&returnValue objCType:@encode(BOOL)];
[invocation setReturnValue:&nsReturnValue];
};
OCMStub([mockAuthContext canEvaluatePolicy:policy
error:(NSError * __autoreleasing *)[OCMArg anyPointer]])
.andDo(canEvaluatePolicyHandler);

FlutterMethodCall *call =
[FlutterMethodCall methodCallWithMethodName:@"deviceSupportsBiometrics" arguments:@{}];
XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"];
[plugin handleMethodCall:call
result:^(id _Nullable result) {
XCTAssertTrue([NSThread isMainThread]);
XCTAssertTrue([result isKindOfClass:[NSNumber class]]);
XCTAssertTrue([result boolValue]);
[expectation fulfill];
}];

[self waitForExpectationsWithTimeout:kTimeout handler:nil];
}
}

- (void)testDeviceSupportsBiometrics_withNonEnrolledTouchID {
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init];
id mockAuthContext = OCMClassMock([LAContext class]);
plugin.authContextOverrides = @[ mockAuthContext ];

const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
void (^canEvaluatePolicyHandler)(NSInvocation *) = ^(NSInvocation *invocation) {
// Write error
NSError *__autoreleasing *authError;
[invocation getArgument:&authError atIndex:3];
*authError = [NSError errorWithDomain:@"error" code:LAErrorTouchIDNotEnrolled userInfo:nil];
// Write return value
BOOL returnValue = NO;
NSValue *nsReturnValue = [NSValue valueWithBytes:&returnValue objCType:@encode(BOOL)];
[invocation setReturnValue:&nsReturnValue];
};
OCMStub([mockAuthContext canEvaluatePolicy:policy
error:(NSError * __autoreleasing *)[OCMArg anyPointer]])
.andDo(canEvaluatePolicyHandler);

FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"deviceSupportsBiometrics"
arguments:@{}];
XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"];
[plugin handleMethodCall:call
result:^(id _Nullable result) {
XCTAssertTrue([NSThread isMainThread]);
XCTAssertTrue([result isKindOfClass:[NSNumber class]]);
XCTAssertTrue([result boolValue]);
[expectation fulfill];
}];

[self waitForExpectationsWithTimeout:kTimeout handler:nil];
}

- (void)testDeviceSupportsBiometrics_withNoBiometricHardware {
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init];
id mockAuthContext = OCMClassMock([LAContext class]);
plugin.authContextOverrides = @[ mockAuthContext ];

const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
void (^canEvaluatePolicyHandler)(NSInvocation *) = ^(NSInvocation *invocation) {
// Write error
NSError *__autoreleasing *authError;
[invocation getArgument:&authError atIndex:3];
*authError = [NSError errorWithDomain:@"error" code:0 userInfo:nil];
// Write return value
BOOL returnValue = NO;
NSValue *nsReturnValue = [NSValue valueWithBytes:&returnValue objCType:@encode(BOOL)];
[invocation setReturnValue:&nsReturnValue];
};
OCMStub([mockAuthContext canEvaluatePolicy:policy
error:(NSError * __autoreleasing *)[OCMArg anyPointer]])
.andDo(canEvaluatePolicyHandler);

FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"deviceSupportsBiometrics"
arguments:@{}];
XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"];
[plugin handleMethodCall:call
result:^(id _Nullable result) {
XCTAssertTrue([NSThread isMainThread]);
XCTAssertTrue([result isKindOfClass:[NSNumber class]]);
XCTAssertFalse([result boolValue]);
[expectation fulfill];
}];

[self waitForExpectationsWithTimeout:kTimeout handler:nil];
}

- (void)testGetEnrolledBiometrics_withFaceID_iOS11 {
if (@available(iOS 11.0.1, *)) {
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init];
id mockAuthContext = OCMClassMock([LAContext class]);
plugin.authContextOverrides = @[ mockAuthContext ];

const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES);
OCMStub([mockAuthContext biometryType]).andReturn(LABiometryTypeFaceID);

FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics"
arguments:@{}];
XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"];
[plugin handleMethodCall:call
result:^(id _Nullable result) {
XCTAssertTrue([NSThread isMainThread]);
XCTAssertTrue([result isKindOfClass:[NSArray class]]);
XCTAssertEqual([result count], 1);
XCTAssertEqualObjects(result[0], @"face");
[expectation fulfill];
}];

[self waitForExpectationsWithTimeout:kTimeout handler:nil];
}
}

- (void)testGetEnrolledBiometrics_withTouchID_iOS11 {
if (@available(iOS 11.0.1, *)) {
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init];
id mockAuthContext = OCMClassMock([LAContext class]);
plugin.authContextOverrides = @[ mockAuthContext ];

const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES);
OCMStub([mockAuthContext biometryType]).andReturn(LABiometryTypeTouchID);

FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics"
arguments:@{}];
XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"];
[plugin handleMethodCall:call
result:^(id _Nullable result) {
XCTAssertTrue([NSThread isMainThread]);
XCTAssertTrue([result isKindOfClass:[NSArray class]]);
XCTAssertEqual([result count], 1);
XCTAssertEqualObjects(result[0], @"fingerprint");
[expectation fulfill];
}];

[self waitForExpectationsWithTimeout:kTimeout handler:nil];
}
}

- (void)testGetEnrolledBiometrics_withTouchID_preIOS11 {
if (@available(iOS 11.0.1, *)) {
return;
}
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init];
id mockAuthContext = OCMClassMock([LAContext class]);
plugin.authContextOverrides = @[ mockAuthContext ];

const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES);

FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics"
arguments:@{}];
XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"];
[plugin handleMethodCall:call
result:^(id _Nullable result) {
XCTAssertTrue([NSThread isMainThread]);
XCTAssertTrue([result isKindOfClass:[NSArray class]]);
XCTAssertEqual([result count], 1);
XCTAssertEqualObjects(result[0], @"fingerprint");
[expectation fulfill];
}];

[self waitForExpectationsWithTimeout:kTimeout handler:nil];
}

- (void)testGetEnrolledBiometrics_withoutEnrolledHardware_iOS11 {
if (@available(iOS 11.0.1, *)) {
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init];
id mockAuthContext = OCMClassMock([LAContext class]);
plugin.authContextOverrides = @[ mockAuthContext ];

const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
void (^canEvaluatePolicyHandler)(NSInvocation *) = ^(NSInvocation *invocation) {
// Write error
NSError *__autoreleasing *authError;
[invocation getArgument:&authError atIndex:3];
*authError = [NSError errorWithDomain:@"error" code:LAErrorBiometryNotEnrolled userInfo:nil];
// Write return value
BOOL returnValue = NO;
NSValue *nsReturnValue = [NSValue valueWithBytes:&returnValue objCType:@encode(BOOL)];
[invocation setReturnValue:&nsReturnValue];
};
OCMStub([mockAuthContext canEvaluatePolicy:policy
error:(NSError * __autoreleasing *)[OCMArg anyPointer]])
.andDo(canEvaluatePolicyHandler);

FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics"
arguments:@{}];
XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"];
[plugin handleMethodCall:call
result:^(id _Nullable result) {
XCTAssertTrue([NSThread isMainThread]);
XCTAssertTrue([result isKindOfClass:[NSArray class]]);
XCTAssertEqual([result count], 0);
[expectation fulfill];
}];

[self waitForExpectationsWithTimeout:kTimeout handler:nil];
}
}

- (void)testGetEnrolledBiometrics_withoutEnrolledTouchID_preIOS11 {
if (@available(iOS 11.0.1, *)) {
return;
}
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init];
id mockAuthContext = OCMClassMock([LAContext class]);
plugin.authContextOverrides = @[ mockAuthContext ];

const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
void (^canEvaluatePolicyHandler)(NSInvocation *) = ^(NSInvocation *invocation) {
// Write error
NSError *__autoreleasing *authError;
[invocation getArgument:&authError atIndex:3];
*authError = [NSError errorWithDomain:@"error" code:LAErrorTouchIDNotEnrolled userInfo:nil];
// Write return value
BOOL returnValue = NO;
NSValue *nsReturnValue = [NSValue valueWithBytes:&returnValue objCType:@encode(BOOL)];
[invocation setReturnValue:&nsReturnValue];
};
OCMStub([mockAuthContext canEvaluatePolicy:policy
error:(NSError * __autoreleasing *)[OCMArg anyPointer]])
.andDo(canEvaluatePolicyHandler);

FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics"
arguments:@{}];
XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"];
[plugin handleMethodCall:call
result:^(id _Nullable result) {
XCTAssertTrue([NSThread isMainThread]);
XCTAssertTrue([result isKindOfClass:[NSArray class]]);
XCTAssertEqual([result count], 0);
[expectation fulfill];
}];

[self waitForExpectationsWithTimeout:kTimeout handler:nil];
}

@end
26 changes: 13 additions & 13 deletions packages/local_auth/local_auth_ios/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class MyApp extends StatefulWidget {
class _MyAppState extends State<MyApp> {
_SupportState _supportState = _SupportState.unknown;
bool? _canCheckBiometrics;
List<BiometricType>? _availableBiometrics;
List<BiometricType>? _enrolledBiometrics;
String _authorized = 'Not Authorized';
bool _isAuthenticating = false;

Expand All @@ -40,38 +40,38 @@ class _MyAppState extends State<MyApp> {
}

Future<void> _checkBiometrics() async {
late bool canCheckBiometrics;
late bool deviceSupportsBiometrics;
try {
canCheckBiometrics =
(await LocalAuthPlatform.instance.getEnrolledBiometrics()).isNotEmpty;
deviceSupportsBiometrics =
await LocalAuthPlatform.instance.deviceSupportsBiometrics();
} on PlatformException catch (e) {
canCheckBiometrics = false;
deviceSupportsBiometrics = false;
print(e);
}
if (!mounted) {
return;
}

setState(() {
_canCheckBiometrics = canCheckBiometrics;
_canCheckBiometrics = deviceSupportsBiometrics;
});
}

Future<void> _getEnrolledBiometrics() async {
late List<BiometricType> availableBiometrics;
late List<BiometricType> enrolledBiometrics;
try {
availableBiometrics =
enrolledBiometrics =
await LocalAuthPlatform.instance.getEnrolledBiometrics();
} on PlatformException catch (e) {
availableBiometrics = <BiometricType>[];
enrolledBiometrics = <BiometricType>[];
print(e);
}
if (!mounted) {
return;
}

setState(() {
_availableBiometrics = availableBiometrics;
_enrolledBiometrics = enrolledBiometrics;
});
}

Expand Down Expand Up @@ -173,15 +173,15 @@ class _MyAppState extends State<MyApp> {
else
const Text('This device is not supported'),
const Divider(height: 100),
Text('Can check biometrics: $_canCheckBiometrics\n'),
Text('Device supports biometrics: $_canCheckBiometrics\n'),
ElevatedButton(
child: const Text('Check biometrics'),
onPressed: _checkBiometrics,
),
const Divider(height: 100),
Text('Available biometrics: $_availableBiometrics\n'),
Text('Enrolled biometrics: $_enrolledBiometrics\n'),
ElevatedButton(
child: const Text('Get available biometrics'),
child: const Text('Get enrolled biometrics'),
onPressed: _getEnrolledBiometrics,
),
const Divider(height: 100),
Expand Down
Loading