[local_auth] Fix default deviceSupportsBiometrics (#5321)

diff --git a/packages/local_auth/local_auth_platform_interface/CHANGELOG.md b/packages/local_auth/local_auth_platform_interface/CHANGELOG.md
index 0ff4d16..9a2ae7a 100644
--- a/packages/local_auth/local_auth_platform_interface/CHANGELOG.md
+++ b/packages/local_auth/local_auth_platform_interface/CHANGELOG.md
@@ -1,3 +1,9 @@
+## 1.0.3
+
+* Fixes regression in the default method channel implementation of
+  `deviceSupportsBiometrics` from federation that would cause it to return true
+  only if something is enrolled.
+
 ## 1.0.2
 
 * Adopts `Object.hash`.
diff --git a/packages/local_auth/local_auth_platform_interface/lib/default_method_channel_platform.dart b/packages/local_auth/local_auth_platform_interface/lib/default_method_channel_platform.dart
index c68a3bf..3e695fa 100644
--- a/packages/local_auth/local_auth_platform_interface/lib/default_method_channel_platform.dart
+++ b/packages/local_auth/local_auth_platform_interface/lib/default_method_channel_platform.dart
@@ -57,6 +57,8 @@
           biometrics.add(BiometricType.iris);
           break;
         case 'undefined':
+          // Sentinel value for the case when nothing is enrolled, but hardware
+          // support for biometrics is available.
           break;
       }
     }
@@ -65,7 +67,14 @@
 
   @override
   Future<bool> deviceSupportsBiometrics() async {
-    return (await getEnrolledBiometrics()).isNotEmpty;
+    final List<String> availableBiometrics =
+        (await _channel.invokeListMethod<String>(
+              'getAvailableBiometrics',
+            )) ??
+            <String>[];
+    // If anything, including the 'undefined' sentinel, is returned, then there
+    // is device support for biometrics.
+    return availableBiometrics.isNotEmpty;
   }
 
   @override
diff --git a/packages/local_auth/local_auth_platform_interface/pubspec.yaml b/packages/local_auth/local_auth_platform_interface/pubspec.yaml
index b5d476d..ee6c1e9 100644
--- a/packages/local_auth/local_auth_platform_interface/pubspec.yaml
+++ b/packages/local_auth/local_auth_platform_interface/pubspec.yaml
@@ -4,7 +4,7 @@
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22
 # NOTE: We strongly prefer non-breaking changes, even at the expense of a
 # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes
-version: 1.0.2
+version: 1.0.3
 
 environment:
   sdk: ">=2.14.0 <3.0.0"
diff --git a/packages/local_auth/local_auth_platform_interface/test/default_method_channel_platform_test.dart b/packages/local_auth/local_auth_platform_interface/test/default_method_channel_platform_test.dart
index 3853fd8..96e78e1 100644
--- a/packages/local_auth/local_auth_platform_interface/test/default_method_channel_platform_test.dart
+++ b/packages/local_auth/local_auth_platform_interface/test/default_method_channel_platform_test.dart
@@ -10,7 +10,6 @@
 import 'package:local_auth_platform_interface/local_auth_platform_interface.dart';
 import 'package:local_auth_platform_interface/types/auth_messages.dart';
 import 'package:local_auth_platform_interface/types/auth_options.dart';
-import 'package:local_auth_platform_interface/types/biometric_type.dart';
 
 void main() {
   TestWidgetsFlutterBinding.ensureInitialized();
@@ -19,9 +18,13 @@
     'plugins.flutter.io/local_auth',
   );
 
-  final List<MethodCall> log = <MethodCall>[];
+  late List<MethodCall> log;
   late LocalAuthPlatform localAuthentication;
 
+  setUp(() async {
+    log = <MethodCall>[];
+  });
+
   test(
       'DefaultLocalAuthPlatform is registered as the default platform implementation',
       () async {
@@ -32,10 +35,9 @@
   test('getAvailableBiometrics', () async {
     channel.setMockMethodCallHandler((MethodCall methodCall) {
       log.add(methodCall);
-      return Future<dynamic>.value(<BiometricType>[]);
+      return Future<dynamic>.value(<String>[]);
     });
     localAuthentication = DefaultLocalAuthPlatform();
-    log.clear();
     await localAuthentication.getEnrolledBiometrics();
     expect(
       log,
@@ -45,6 +47,29 @@
     );
   });
 
+  test('deviceSupportsBiometrics handles special sentinal value', () async {
+    // The pre-federation implementation of the platform channels, which the
+    // default implementation retains compatibility with for the benefit of any
+    // existing unendorsed implementations, used 'undefined' as a special
+    // return value from `getAvailableBiometrics` to indicate that nothing was
+    // enrolled, but that the hardware does support biometrics.
+    channel.setMockMethodCallHandler((MethodCall methodCall) {
+      log.add(methodCall);
+      return Future<dynamic>.value(<String>['undefined']);
+    });
+
+    localAuthentication = DefaultLocalAuthPlatform();
+    final bool supportsBiometrics =
+        await localAuthentication.deviceSupportsBiometrics();
+    expect(supportsBiometrics, true);
+    expect(
+      log,
+      <Matcher>[
+        isMethodCall('getAvailableBiometrics', arguments: null),
+      ],
+    );
+  });
+
   group('Boolean returning methods', () {
     setUp(() {
       channel.setMockMethodCallHandler((MethodCall methodCall) {
@@ -52,7 +77,6 @@
         return Future<dynamic>.value(true);
       });
       localAuthentication = DefaultLocalAuthPlatform();
-      log.clear();
     });
 
     test('isDeviceSupported', () async {