From 4589ef60d4606610e35d0e7eed8cde5479ae7ae0 Mon Sep 17 00:00:00 2001 From: Zubii Date: Fri, 10 Jul 2026 20:58:57 +0300 Subject: [PATCH 1/4] feat(local_auth): add support for optional localized reason for Android * implement a support query `requiresLocalizedReason` --- .../local_auth/lib/src/local_auth.dart | 8 +- .../io/flutter/plugins/localauth/Messages.kt | 181 ++++++++++++++++-- .../lib/local_auth_android.dart | 10 +- .../lib/src/messages.g.dart | 82 ++++++-- .../local_auth_android/pigeons/messages.dart | 4 +- .../lib/local_auth_darwin.dart | 12 +- .../lib/default_method_channel_platform.dart | 5 +- .../lib/local_auth_platform_interface.dart | 8 +- .../lib/local_auth_windows.dart | 10 +- 9 files changed, 268 insertions(+), 52 deletions(-) diff --git a/packages/local_auth/local_auth/lib/src/local_auth.dart b/packages/local_auth/local_auth/lib/src/local_auth.dart index a76b659fa1e5..f19dfebed816 100644 --- a/packages/local_auth/local_auth/lib/src/local_auth.dart +++ b/packages/local_auth/local_auth/lib/src/local_auth.dart @@ -28,7 +28,7 @@ class LocalAuthentication { /// /// [localizedReason] is the message to show to user while prompting them /// for authentication. This is typically along the lines of: 'Authenticate - /// to access MyApp.'. This must not be empty. + /// to access MyApp.'. /// /// Provide [authMessages] if you want to customize messages in the dialogs. /// @@ -46,7 +46,7 @@ class LocalAuthentication { /// retry the authentication on foregrounding instead of failing with an error /// on backgrounding. Future authenticate({ - required String localizedReason, + String? localizedReason, Iterable authMessages = const [ IOSAuthMessages(), AndroidAuthMessages(), @@ -97,4 +97,8 @@ class LocalAuthentication { /// permissions to use them. Future> getAvailableBiometrics() => LocalAuthPlatform.instance.getEnrolledBiometrics(); + + /// Returns true if the current platform implementation requires + /// a localized reason for authentication. + bool requiresLocalizedReason() => LocalAuthPlatform.instance.requiresLocalizedReason(); } diff --git a/packages/local_auth/local_auth_android/android/src/main/kotlin/io/flutter/plugins/localauth/Messages.kt b/packages/local_auth/local_auth_android/android/src/main/kotlin/io/flutter/plugins/localauth/Messages.kt index 5822b5b85b64..171d2707dece 100644 --- a/packages/local_auth/local_auth_android/android/src/main/kotlin/io/flutter/plugins/localauth/Messages.kt +++ b/packages/local_auth/local_auth_android/android/src/main/kotlin/io/flutter/plugins/localauth/Messages.kt @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v26.2.3), do not edit directly. +// Autogenerated from Pigeon (v26.3.4), do not edit directly. // See also: https://pub.dev/packages/pigeon @file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") @@ -32,7 +32,36 @@ private object MessagesPigeonUtils { } } + fun doubleEquals(a: Double, b: Double): Boolean { + // Normalize -0.0 to 0.0 and handle NaN equality. + return (if (a == 0.0) 0.0 else a) == (if (b == 0.0) 0.0 else b) || (a.isNaN() && b.isNaN()) + } + + fun floatEquals(a: Float, b: Float): Boolean { + // Normalize -0.0 to 0.0 and handle NaN equality. + return (if (a == 0.0f) 0.0f else a) == (if (b == 0.0f) 0.0f else b) || (a.isNaN() && b.isNaN()) + } + + fun doubleHash(d: Double): Int { + // Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes. + val normalized = if (d == 0.0) 0.0 else d + val bits = java.lang.Double.doubleToLongBits(normalized) + return (bits xor (bits ushr 32)).toInt() + } + + fun floatHash(f: Float): Int { + // Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes. + val normalized = if (f == 0.0f) 0.0f else f + return java.lang.Float.floatToIntBits(normalized) + } + fun deepEquals(a: Any?, b: Any?): Boolean { + if (a === b) { + return true + } + if (a == null || b == null) { + return false + } if (a is ByteArray && b is ByteArray) { return a.contentEquals(b) } @@ -43,20 +72,109 @@ private object MessagesPigeonUtils { return a.contentEquals(b) } if (a is DoubleArray && b is DoubleArray) { - return a.contentEquals(b) + if (a.size != b.size) return false + for (i in a.indices) { + if (!doubleEquals(a[i], b[i])) return false + } + return true + } + if (a is FloatArray && b is FloatArray) { + if (a.size != b.size) return false + for (i in a.indices) { + if (!floatEquals(a[i], b[i])) return false + } + return true } if (a is Array<*> && b is Array<*>) { - return a.size == b.size && a.indices.all { deepEquals(a[it], b[it]) } + if (a.size != b.size) return false + for (i in a.indices) { + if (!deepEquals(a[i], b[i])) return false + } + return true } if (a is List<*> && b is List<*>) { - return a.size == b.size && a.indices.all { deepEquals(a[it], b[it]) } + if (a.size != b.size) return false + val iterA = a.iterator() + val iterB = b.iterator() + while (iterA.hasNext() && iterB.hasNext()) { + if (!deepEquals(iterA.next(), iterB.next())) return false + } + return true } if (a is Map<*, *> && b is Map<*, *>) { - return a.size == b.size && - a.all { (b as Map).contains(it.key) && deepEquals(it.value, b[it.key]) } + if (a.size != b.size) return false + for (entry in a) { + val key = entry.key + var found = false + for (bEntry in b) { + if (deepEquals(key, bEntry.key)) { + if (deepEquals(entry.value, bEntry.value)) { + found = true + break + } else { + return false + } + } + } + if (!found) return false + } + return true + } + if (a is Double && b is Double) { + return doubleEquals(a, b) + } + if (a is Float && b is Float) { + return floatEquals(a, b) } return a == b } + + fun deepHash(value: Any?): Int { + return when (value) { + null -> 0 + is ByteArray -> value.contentHashCode() + is IntArray -> value.contentHashCode() + is LongArray -> value.contentHashCode() + is DoubleArray -> { + var result = 1 + for (item in value) { + result = 31 * result + doubleHash(item) + } + result + } + is FloatArray -> { + var result = 1 + for (item in value) { + result = 31 * result + floatHash(item) + } + result + } + is Array<*> -> { + var result = 1 + for (item in value) { + result = 31 * result + deepHash(item) + } + result + } + is List<*> -> { + var result = 1 + for (item in value) { + result = 31 * result + deepHash(item) + } + result + } + is Map<*, *> -> { + var result = 0 + for (entry in value) { + result += ((deepHash(entry.key) * 31) xor deepHash(entry.value)) + } + result + } + is Double -> doubleHash(value) + is Float -> floatHash(value) + else -> value.hashCode() + } + } } /** @@ -70,7 +188,7 @@ class FlutterError( val code: String, override val message: String? = null, val details: Any? = null -) : Throwable() +) : RuntimeException() /** Possible outcomes of an authentication attempt. */ enum class AuthResultCode(val raw: Int) { @@ -140,14 +258,14 @@ enum class AuthClassification(val raw: Int) { * Generated class from Pigeon that represents data sent in messages. */ data class AuthStrings( - val reason: String, + val reason: String? = null, val signInHint: String, val cancelButton: String, val signInTitle: String ) { companion object { fun fromList(pigeonVar_list: List): AuthStrings { - val reason = pigeonVar_list[0] as String + val reason = pigeonVar_list[0] as String? val signInHint = pigeonVar_list[1] as String val cancelButton = pigeonVar_list[2] as String val signInTitle = pigeonVar_list[3] as String @@ -165,16 +283,27 @@ data class AuthStrings( } override fun equals(other: Any?): Boolean { - if (other !is AuthStrings) { + if (other == null || other.javaClass != javaClass) { return false } if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + val other = other as AuthStrings + return MessagesPigeonUtils.deepEquals(this.reason, other.reason) && + MessagesPigeonUtils.deepEquals(this.signInHint, other.signInHint) && + MessagesPigeonUtils.deepEquals(this.cancelButton, other.cancelButton) && + MessagesPigeonUtils.deepEquals(this.signInTitle, other.signInTitle) } - override fun hashCode(): Int = toList().hashCode() + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + MessagesPigeonUtils.deepHash(this.reason) + result = 31 * result + MessagesPigeonUtils.deepHash(this.signInHint) + result = 31 * result + MessagesPigeonUtils.deepHash(this.cancelButton) + result = 31 * result + MessagesPigeonUtils.deepHash(this.signInTitle) + return result + } } /** @@ -204,16 +333,23 @@ data class AuthResult( } override fun equals(other: Any?): Boolean { - if (other !is AuthResult) { + if (other == null || other.javaClass != javaClass) { return false } if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + val other = other as AuthResult + return MessagesPigeonUtils.deepEquals(this.code, other.code) && + MessagesPigeonUtils.deepEquals(this.errorMessage, other.errorMessage) } - override fun hashCode(): Int = toList().hashCode() + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + MessagesPigeonUtils.deepHash(this.code) + result = 31 * result + MessagesPigeonUtils.deepHash(this.errorMessage) + return result + } } /** Generated class from Pigeon that represents data sent in messages. */ @@ -240,16 +376,25 @@ data class AuthOptions( } override fun equals(other: Any?): Boolean { - if (other !is AuthOptions) { + if (other == null || other.javaClass != javaClass) { return false } if (this === other) { return true } - return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + val other = other as AuthOptions + return MessagesPigeonUtils.deepEquals(this.biometricOnly, other.biometricOnly) && + MessagesPigeonUtils.deepEquals(this.sensitiveTransaction, other.sensitiveTransaction) && + MessagesPigeonUtils.deepEquals(this.sticky, other.sticky) } - override fun hashCode(): Int = toList().hashCode() + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + MessagesPigeonUtils.deepHash(this.biometricOnly) + result = 31 * result + MessagesPigeonUtils.deepHash(this.sensitiveTransaction) + result = 31 * result + MessagesPigeonUtils.deepHash(this.sticky) + return result + } } private open class MessagesPigeonCodec : StandardMessageCodec() { diff --git a/packages/local_auth/local_auth_android/lib/local_auth_android.dart b/packages/local_auth/local_auth_android/lib/local_auth_android.dart index 0cce6ca5c2b1..b107bbbcef5e 100644 --- a/packages/local_auth/local_auth_android/lib/local_auth_android.dart +++ b/packages/local_auth/local_auth_android/lib/local_auth_android.dart @@ -27,11 +27,11 @@ class LocalAuthAndroid extends LocalAuthPlatform { @override Future authenticate({ - required String localizedReason, + String? localizedReason, required Iterable authMessages, AuthenticationOptions options = const AuthenticationOptions(), }) async { - assert(localizedReason.isNotEmpty); + assert(!requiresLocalizedReason() || (localizedReason != null && localizedReason.isNotEmpty)); final AuthResult result = await _api.authenticate( AuthOptions( biometricOnly: options.biometricOnly, @@ -126,8 +126,12 @@ class LocalAuthAndroid extends LocalAuthPlatform { @override Future stopAuthentication() async => _api.stopAuthentication(); + /// Always returns false as Android does not require a localized reason for authentication. + @override + bool requiresLocalizedReason() => false; + AuthStrings _pigeonStringsFromAuthMessages( - String localizedReason, + String? localizedReason, Iterable messagesList, ) { AndroidAuthMessages? messages; diff --git a/packages/local_auth/local_auth_android/lib/src/messages.g.dart b/packages/local_auth/local_auth_android/lib/src/messages.g.dart index dcb19cade273..d2c6f59063cd 100644 --- a/packages/local_auth/local_auth_android/lib/src/messages.g.dart +++ b/packages/local_auth/local_auth_android/lib/src/messages.g.dart @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v26.2.3), do not edit directly. +// Autogenerated from Pigeon (v26.3.4), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: unused_import, unused_shown_name // ignore_for_file: type=lint @@ -38,21 +38,66 @@ Object? _extractReplyValueOrThrow( } bool _deepEquals(Object? a, Object? b) { + if (identical(a, b)) { + return true; + } + if (a is double && b is double) { + if (a.isNaN && b.isNaN) { + return true; + } + return a == b; + } if (a is List && b is List) { return a.length == b.length && a.indexed.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); } if (a is Map && b is Map) { - return a.length == b.length && - a.entries.every( - (MapEntry entry) => - (b as Map).containsKey(entry.key) && - _deepEquals(entry.value, b[entry.key]), - ); + if (a.length != b.length) { + return false; + } + for (final MapEntry entryA in a.entries) { + bool found = false; + for (final MapEntry entryB in b.entries) { + if (_deepEquals(entryA.key, entryB.key)) { + if (_deepEquals(entryA.value, entryB.value)) { + found = true; + break; + } else { + return false; + } + } + } + if (!found) { + return false; + } + } + return true; } return a == b; } +int _deepHash(Object? value) { + if (value is List) { + return Object.hashAll(value.map(_deepHash)); + } + if (value is Map) { + int result = 0; + for (final MapEntry entry in value.entries) { + result += (_deepHash(entry.key) * 31) ^ _deepHash(entry.value); + } + return result; + } + if (value is double && value.isNaN) { + // Normalize NaN to a consistent hash. + return 0x7FF8000000000000.hashCode; + } + if (value is double && value == 0.0) { + // Normalize -0.0 to 0.0 so they have the same hash code. + return 0.0.hashCode; + } + return value.hashCode; +} + /// Possible outcomes of an authentication attempt. enum AuthResultCode { /// The user authenticated successfully. @@ -119,13 +164,13 @@ enum AuthClassification { weak, strong } /// See auth_messages_android.dart for details. class AuthStrings { AuthStrings({ - required this.reason, + this.reason, required this.signInHint, required this.cancelButton, required this.signInTitle, }); - String reason; + String? reason; String signInHint; @@ -144,7 +189,7 @@ class AuthStrings { static AuthStrings decode(Object result) { result as List; return AuthStrings( - reason: result[0]! as String, + reason: result[0] as String?, signInHint: result[1]! as String, cancelButton: result[2]! as String, signInTitle: result[3]! as String, @@ -160,12 +205,15 @@ class AuthStrings { if (identical(this, other)) { return true; } - return _deepEquals(encode(), other.encode()); + return _deepEquals(reason, other.reason) && + _deepEquals(signInHint, other.signInHint) && + _deepEquals(cancelButton, other.cancelButton) && + _deepEquals(signInTitle, other.signInTitle); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => _deepHash([runtimeType, ..._toList()]); } /// The results of an authentication request. @@ -200,12 +248,12 @@ class AuthResult { if (identical(this, other)) { return true; } - return _deepEquals(encode(), other.encode()); + return _deepEquals(code, other.code) && _deepEquals(errorMessage, other.errorMessage); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => _deepHash([runtimeType, ..._toList()]); } class AuthOptions { @@ -247,12 +295,14 @@ class AuthOptions { if (identical(this, other)) { return true; } - return _deepEquals(encode(), other.encode()); + return _deepEquals(biometricOnly, other.biometricOnly) && + _deepEquals(sensitiveTransaction, other.sensitiveTransaction) && + _deepEquals(sticky, other.sticky); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => _deepHash([runtimeType, ..._toList()]); } class _PigeonCodec extends StandardMessageCodec { diff --git a/packages/local_auth/local_auth_android/pigeons/messages.dart b/packages/local_auth/local_auth_android/pigeons/messages.dart index e4d30d77c615..a35a2709589c 100644 --- a/packages/local_auth/local_auth_android/pigeons/messages.dart +++ b/packages/local_auth/local_auth_android/pigeons/messages.dart @@ -18,13 +18,13 @@ import 'package:pigeon/pigeon.dart'; class AuthStrings { /// Constructs a new instance. const AuthStrings({ - required this.reason, + this.reason, required this.signInHint, required this.cancelButton, required this.signInTitle, }); - final String reason; + final String? reason; final String signInHint; final String cancelButton; final String signInTitle; diff --git a/packages/local_auth/local_auth_darwin/lib/local_auth_darwin.dart b/packages/local_auth/local_auth_darwin/lib/local_auth_darwin.dart index 726f2e1ded95..53bd1bb039ee 100644 --- a/packages/local_auth/local_auth_darwin/lib/local_auth_darwin.dart +++ b/packages/local_auth/local_auth_darwin/lib/local_auth_darwin.dart @@ -35,16 +35,16 @@ class LocalAuthDarwin extends LocalAuthPlatform { @override Future authenticate({ - required String localizedReason, + String? localizedReason, required Iterable authMessages, AuthenticationOptions options = const AuthenticationOptions(), }) async { - assert(localizedReason.isNotEmpty); + assert(!requiresLocalizedReason() || (localizedReason != null && localizedReason.isNotEmpty)); final AuthResultDetails resultDetails = await _api.authenticate( AuthOptions(biometricOnly: options.biometricOnly, sticky: options.stickyAuth), _useMacOSAuthMessages - ? _pigeonStringsFromMacOSAuthMessages(localizedReason, authMessages) - : _pigeonStringsFromiOSAuthMessages(localizedReason, authMessages), + ? _pigeonStringsFromMacOSAuthMessages(localizedReason!, authMessages) + : _pigeonStringsFromiOSAuthMessages(localizedReason!, authMessages), ); LocalAuthExceptionCode code; switch (resultDetails.result) { @@ -116,6 +116,10 @@ class LocalAuthDarwin extends LocalAuthPlatform { @override Future stopAuthentication() async => false; + /// Always returns true as iOS and macOS require a localized reason for authentication. + @override + bool requiresLocalizedReason() => true; + AuthStrings _pigeonStringsFromiOSAuthMessages( String localizedReason, Iterable messagesList, 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 9270346f1056..7af221e0299a 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 @@ -17,13 +17,12 @@ const MethodChannel _channel = MethodChannel('plugins.flutter.io/local_auth'); class DefaultLocalAuthPlatform extends LocalAuthPlatform { @override Future authenticate({ - required String localizedReason, + String? localizedReason, required Iterable authMessages, AuthenticationOptions options = const AuthenticationOptions(), }) async { - assert(localizedReason.isNotEmpty); final args = { - 'localizedReason': localizedReason, + if (localizedReason != null) 'localizedReason': localizedReason, 'useErrorDialogs': options.useErrorDialogs, 'stickyAuth': options.stickyAuth, 'sensitiveTransaction': options.sensitiveTransaction, diff --git a/packages/local_auth/local_auth_platform_interface/lib/local_auth_platform_interface.dart b/packages/local_auth/local_auth_platform_interface/lib/local_auth_platform_interface.dart index af788c552dc7..8d774d866d87 100644 --- a/packages/local_auth/local_auth_platform_interface/lib/local_auth_platform_interface.dart +++ b/packages/local_auth/local_auth_platform_interface/lib/local_auth_platform_interface.dart @@ -57,7 +57,7 @@ abstract class LocalAuthPlatform extends PlatformInterface { /// /// Provide [options] for configuring further authentication related options. Future authenticate({ - required String localizedReason, + String? localizedReason, required Iterable authMessages, AuthenticationOptions options = const AuthenticationOptions(), }) async { @@ -97,4 +97,10 @@ abstract class LocalAuthPlatform extends PlatformInterface { Future stopAuthentication() async { throw UnimplementedError('stopAuthentication() has not been implemented.'); } + + /// Returns true if the current platform implementation requires + /// a localized reason for authentication. + bool requiresLocalizedReason() { + throw UnimplementedError('requiresLocalizedReason() has not been implemented.'); + } } diff --git a/packages/local_auth/local_auth_windows/lib/local_auth_windows.dart b/packages/local_auth/local_auth_windows/lib/local_auth_windows.dart index 3d9cf4550c61..0b1cc6ad604e 100644 --- a/packages/local_auth/local_auth_windows/lib/local_auth_windows.dart +++ b/packages/local_auth/local_auth_windows/lib/local_auth_windows.dart @@ -26,17 +26,17 @@ class LocalAuthWindows extends LocalAuthPlatform { @override Future authenticate({ - required String localizedReason, + String? localizedReason, required Iterable authMessages, AuthenticationOptions options = const AuthenticationOptions(), }) async { - assert(localizedReason.isNotEmpty); + assert(!requiresLocalizedReason() || (localizedReason != null && localizedReason.isNotEmpty)); if (options.biometricOnly) { throw UnsupportedError("Windows doesn't support the biometricOnly parameter."); } - return switch (await _api.authenticate(localizedReason)) { + return switch (await _api.authenticate(localizedReason!)) { AuthResult.success => true, AuthResult.failure => false, AuthResult.noHardware => throw const LocalAuthException( @@ -85,4 +85,8 @@ class LocalAuthWindows extends LocalAuthPlatform { /// Always returns false as this method is not supported on Windows. @override Future stopAuthentication() async => false; + + /// Always returns true as Windows requires a localized reason for authentication. + @override + bool requiresLocalizedReason() => true; } From e48766d59377d1a414f974c883213afbc40b627f Mon Sep 17 00:00:00 2001 From: Zubii Date: Fri, 10 Jul 2026 21:00:07 +0300 Subject: [PATCH 2/4] test(local_auth): add tests --- .../local_auth/test/local_auth_test.dart | 17 ++++++++++++++++- .../test/local_auth_test.dart | 10 ++++++++++ .../default_method_channel_platform_test.dart | 15 +++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/packages/local_auth/local_auth/test/local_auth_test.dart b/packages/local_auth/local_auth/test/local_auth_test.dart index 84eab2ed4175..bc0db0ecb2ac 100644 --- a/packages/local_auth/local_auth/test/local_auth_test.dart +++ b/packages/local_auth/local_auth/test/local_auth_test.dart @@ -73,6 +73,13 @@ void main() { expect(result, true); verify(mockLocalAuthPlatform.deviceSupportsBiometrics()).called(2); }); + + test('requiresLocalizedReason calls platform implementation', () { + when(mockLocalAuthPlatform.requiresLocalizedReason()).thenReturn(true); + final bool result = localAuthentication.requiresLocalizedReason(); + expect(result, true); + verify(mockLocalAuthPlatform.requiresLocalizedReason()).called(1); + }); } class MockLocalAuthPlatform extends Mock @@ -84,7 +91,7 @@ class MockLocalAuthPlatform extends Mock @override Future authenticate({ - required String? localizedReason, + String? localizedReason, required Iterable? authMessages, AuthenticationOptions? options = const AuthenticationOptions(), }) => @@ -129,4 +136,12 @@ class MockLocalAuthPlatform extends Mock returnValue: Future.value(false), ) as Future; + + @override + bool requiresLocalizedReason() => + super.noSuchMethod( + Invocation.method(#requiresLocalizedReason, []), + returnValue: false, + ) + as bool; } diff --git a/packages/local_auth/local_auth_android/test/local_auth_test.dart b/packages/local_auth/local_auth_android/test/local_auth_test.dart index a05adc02b886..afd3b037f95b 100644 --- a/packages/local_auth/local_auth_android/test/local_auth_test.dart +++ b/packages/local_auth/local_auth_android/test/local_auth_test.dart @@ -198,6 +198,16 @@ void main() { // auth_messages_android.dart expect(strings.signInTitle, androidSignInTitle); }); + + test('passes provided messages without reason', () async { + when( + api.authenticate(any, any), + ).thenAnswer((_) async => AuthResult(code: AuthResultCode.success)); + + final bool result = await plugin.authenticate(authMessages: []); + + expect(result, true); + }); }); group('options', () { 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 b41c7d31fb2d..54530aa60e6a 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 @@ -180,5 +180,20 @@ void main() { ]); }); }); + + test('authenticate with no localized reason.', () async { + await localAuthentication.authenticate(authMessages: []); + expect(log, [ + isMethodCall( + 'authenticate', + arguments: { + 'useErrorDialogs': true, + 'stickyAuth': false, + 'sensitiveTransaction': true, + 'biometricOnly': false, + }, + ), + ]); + }); }); } From e548bff1c90a6b53a7c3d7e6ae215707e1585a8f Mon Sep 17 00:00:00 2001 From: Zubii Date: Fri, 10 Jul 2026 21:01:50 +0300 Subject: [PATCH 3/4] chore(local_auth): bump package version and update changelogs --- packages/local_auth/local_auth/CHANGELOG.md | 6 ++++++ packages/local_auth/local_auth/pubspec.yaml | 2 +- packages/local_auth/local_auth_android/CHANGELOG.md | 6 ++++++ packages/local_auth/local_auth_android/pubspec.yaml | 2 +- packages/local_auth/local_auth_darwin/CHANGELOG.md | 5 ++++- packages/local_auth/local_auth_darwin/pubspec.yaml | 2 +- .../local_auth/local_auth_platform_interface/CHANGELOG.md | 5 ++++- .../local_auth/local_auth_platform_interface/pubspec.yaml | 2 +- packages/local_auth/local_auth_windows/CHANGELOG.md | 6 ++++-- packages/local_auth/local_auth_windows/pubspec.yaml | 2 +- 10 files changed, 29 insertions(+), 9 deletions(-) diff --git a/packages/local_auth/local_auth/CHANGELOG.md b/packages/local_auth/local_auth/CHANGELOG.md index f9a8a5a7785e..4a4fa1f40472 100644 --- a/packages/local_auth/local_auth/CHANGELOG.md +++ b/packages/local_auth/local_auth/CHANGELOG.md @@ -1,3 +1,9 @@ +## 3.1.0 + +* Adds `requiresLocalizedReason` to determine if the current platform implementation + requires a localized reason for authentication. +* Make `localizedReason` parameter optional in `authenticate` + ## 3.0.2 * Clarifies the `getAvailableBiometrics` documentation regarding iOS permission requirements. diff --git a/packages/local_auth/local_auth/pubspec.yaml b/packages/local_auth/local_auth/pubspec.yaml index bf35d018e959..228a3c2473e0 100644 --- a/packages/local_auth/local_auth/pubspec.yaml +++ b/packages/local_auth/local_auth/pubspec.yaml @@ -2,7 +2,7 @@ name: local_auth description: Flutter plugin to allow local authentication via biometrics, passcode, pin, or pattern. repository: https://github.com/flutter/packages/tree/main/packages/local_auth/local_auth issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22 -version: 3.0.2 +version: 3.1.0 environment: sdk: ^3.10.0 diff --git a/packages/local_auth/local_auth_android/CHANGELOG.md b/packages/local_auth/local_auth_android/CHANGELOG.md index 19ec5cccb452..8cb282a57cf9 100644 --- a/packages/local_auth/local_auth_android/CHANGELOG.md +++ b/packages/local_auth/local_auth_android/CHANGELOG.md @@ -1,3 +1,9 @@ +## 2.1.0 + +* Adds `requiresLocalizedReason` to determine if the current platform implementation + requires a localized reason for authentication. +* Make `localizedReason` parameter optional in `authenticate`. + ## 2.0.9 * Migrates to Built-in Kotlin to support AGP 9. diff --git a/packages/local_auth/local_auth_android/pubspec.yaml b/packages/local_auth/local_auth_android/pubspec.yaml index 392b3cf19bd6..4fe5b0010c26 100644 --- a/packages/local_auth/local_auth_android/pubspec.yaml +++ b/packages/local_auth/local_auth_android/pubspec.yaml @@ -2,7 +2,7 @@ name: local_auth_android description: Android implementation of the local_auth plugin. repository: https://github.com/flutter/packages/tree/main/packages/local_auth/local_auth_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22 -version: 2.0.9 +version: 2.1.0 environment: sdk: ^3.12.0 diff --git a/packages/local_auth/local_auth_darwin/CHANGELOG.md b/packages/local_auth/local_auth_darwin/CHANGELOG.md index d5a5766e0f71..db0dd35b6f19 100644 --- a/packages/local_auth/local_auth_darwin/CHANGELOG.md +++ b/packages/local_auth/local_auth_darwin/CHANGELOG.md @@ -1,5 +1,8 @@ -## NEXT +## 2.1.0 +* Adds `requiresLocalizedReason` to determine if the current platform implementation + requires a localized reason for authentication. +* Make `localizedReason` parameter optional in `authenticate`. * Updates minimum supported SDK version to Flutter 3.38/Dart 3.10. ## 2.0.3 diff --git a/packages/local_auth/local_auth_darwin/pubspec.yaml b/packages/local_auth/local_auth_darwin/pubspec.yaml index 976f2a614523..da8a39d6f4e0 100644 --- a/packages/local_auth/local_auth_darwin/pubspec.yaml +++ b/packages/local_auth/local_auth_darwin/pubspec.yaml @@ -2,7 +2,7 @@ name: local_auth_darwin description: iOS implementation of the local_auth plugin. repository: https://github.com/flutter/packages/tree/main/packages/local_auth/local_auth_darwin issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22 -version: 2.0.3 +version: 2.1.0 environment: sdk: ^3.10.0 diff --git a/packages/local_auth/local_auth_platform_interface/CHANGELOG.md b/packages/local_auth/local_auth_platform_interface/CHANGELOG.md index 508572819104..90c07c9b3612 100644 --- a/packages/local_auth/local_auth_platform_interface/CHANGELOG.md +++ b/packages/local_auth/local_auth_platform_interface/CHANGELOG.md @@ -1,5 +1,8 @@ -## NEXT +## 1.2.0 +* Adds `requiresLocalizedReason` to determine if the current platform implementation + requires a localized reason for authentication. +* Make `localizedReason` parameter optional in `authenticate`. * Updates minimum supported SDK version to Flutter 3.38/Dart 3.10. ## 1.1.0 diff --git a/packages/local_auth/local_auth_platform_interface/pubspec.yaml b/packages/local_auth/local_auth_platform_interface/pubspec.yaml index 0a314bd8a29f..22b837caf261 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 @@ repository: https://github.com/flutter/packages/tree/main/packages/local_auth/lo 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.1.0 +version: 1.2.0 environment: sdk: ^3.10.0 diff --git a/packages/local_auth/local_auth_windows/CHANGELOG.md b/packages/local_auth/local_auth_windows/CHANGELOG.md index 5a0ed17757cf..5c2d7437912e 100644 --- a/packages/local_auth/local_auth_windows/CHANGELOG.md +++ b/packages/local_auth/local_auth_windows/CHANGELOG.md @@ -1,5 +1,7 @@ -## NEXT - +## 2.1.0 +* Adds `requiresLocalizedReason` to determine if the current platform implementation + requires a localized reason for authentication. +* Make `localizedReason` parameter optional in `authenticate`. * Updates minimum supported SDK version to Flutter 3.38/Dart 3.10. ## 2.0.1 diff --git a/packages/local_auth/local_auth_windows/pubspec.yaml b/packages/local_auth/local_auth_windows/pubspec.yaml index 723b0d17f569..4b902ad672a9 100644 --- a/packages/local_auth/local_auth_windows/pubspec.yaml +++ b/packages/local_auth/local_auth_windows/pubspec.yaml @@ -2,7 +2,7 @@ name: local_auth_windows description: Windows implementation of the local_auth plugin. repository: https://github.com/flutter/packages/tree/main/packages/local_auth/local_auth_windows issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22 -version: 2.0.1 +version: 2.1.0 environment: sdk: ^3.10.0 From 022d37769a0b6140eed908edc54ee21c642f24f4 Mon Sep 17 00:00:00 2001 From: Zubii Date: Fri, 10 Jul 2026 21:02:17 +0300 Subject: [PATCH 4/4] chore(local_auth): add temporary dependency_overrides for federated plugin testing --- packages/local_auth/local_auth/example/pubspec.yaml | 5 +++++ packages/local_auth/local_auth/pubspec.yaml | 8 ++++++++ .../local_auth/local_auth_android/example/pubspec.yaml | 4 ++++ packages/local_auth/local_auth_android/pubspec.yaml | 4 ++++ .../local_auth/local_auth_darwin/example/pubspec.yaml | 4 ++++ packages/local_auth/local_auth_darwin/pubspec.yaml | 4 ++++ .../local_auth/local_auth_windows/example/pubspec.yaml | 4 ++++ packages/local_auth/local_auth_windows/pubspec.yaml | 4 ++++ 8 files changed, 37 insertions(+) diff --git a/packages/local_auth/local_auth/example/pubspec.yaml b/packages/local_auth/local_auth/example/pubspec.yaml index f39c76d15a71..ee96bd16aa77 100644 --- a/packages/local_auth/local_auth/example/pubspec.yaml +++ b/packages/local_auth/local_auth/example/pubspec.yaml @@ -28,3 +28,8 @@ dev_dependencies: flutter: uses-material-design: true +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + local_auth_android: {path: ../../../../packages/local_auth/local_auth_android} + local_auth_platform_interface: {path: ../../../../packages/local_auth/local_auth_platform_interface} diff --git a/packages/local_auth/local_auth/pubspec.yaml b/packages/local_auth/local_auth/pubspec.yaml index 228a3c2473e0..44e3907bcd08 100644 --- a/packages/local_auth/local_auth/pubspec.yaml +++ b/packages/local_auth/local_auth/pubspec.yaml @@ -38,3 +38,11 @@ topics: - authentication - biometrics - local-auth +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + local_auth_android: {path: ../../../packages/local_auth/local_auth_android} + local_auth_darwin: {path: ../../../packages/local_auth/local_auth_darwin} + local_auth_platform_interface: {path: ../../../packages/local_auth/local_auth_platform_interface} + local_auth_windows: {path: ../../../packages/local_auth/local_auth_windows} + diff --git a/packages/local_auth/local_auth_android/example/pubspec.yaml b/packages/local_auth/local_auth_android/example/pubspec.yaml index 8d4d751bdcc4..63c34695e8a5 100644 --- a/packages/local_auth/local_auth_android/example/pubspec.yaml +++ b/packages/local_auth/local_auth_android/example/pubspec.yaml @@ -26,3 +26,7 @@ dev_dependencies: flutter: uses-material-design: true +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + local_auth_platform_interface: {path: ../../../../packages/local_auth/local_auth_platform_interface} diff --git a/packages/local_auth/local_auth_android/pubspec.yaml b/packages/local_auth/local_auth_android/pubspec.yaml index 4fe5b0010c26..001ba8ae9e61 100644 --- a/packages/local_auth/local_auth_android/pubspec.yaml +++ b/packages/local_auth/local_auth_android/pubspec.yaml @@ -36,3 +36,7 @@ topics: - authentication - biometrics - local-auth +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + local_auth_platform_interface: {path: ../../../packages/local_auth/local_auth_platform_interface} diff --git a/packages/local_auth/local_auth_darwin/example/pubspec.yaml b/packages/local_auth/local_auth_darwin/example/pubspec.yaml index bca5c58d7d7b..caad7dd55c42 100644 --- a/packages/local_auth/local_auth_darwin/example/pubspec.yaml +++ b/packages/local_auth/local_auth_darwin/example/pubspec.yaml @@ -26,3 +26,7 @@ dev_dependencies: flutter: uses-material-design: true +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + local_auth_platform_interface: {path: ../../../../packages/local_auth/local_auth_platform_interface} diff --git a/packages/local_auth/local_auth_darwin/pubspec.yaml b/packages/local_auth/local_auth_darwin/pubspec.yaml index da8a39d6f4e0..6f02a684e80b 100644 --- a/packages/local_auth/local_auth_darwin/pubspec.yaml +++ b/packages/local_auth/local_auth_darwin/pubspec.yaml @@ -38,3 +38,7 @@ topics: - authentication - biometrics - local-auth +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + local_auth_platform_interface: {path: ../../../packages/local_auth/local_auth_platform_interface} diff --git a/packages/local_auth/local_auth_windows/example/pubspec.yaml b/packages/local_auth/local_auth_windows/example/pubspec.yaml index 1bcb0ba05b35..2450b0322d61 100644 --- a/packages/local_auth/local_auth_windows/example/pubspec.yaml +++ b/packages/local_auth/local_auth_windows/example/pubspec.yaml @@ -26,3 +26,7 @@ dev_dependencies: flutter: uses-material-design: true +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + local_auth_platform_interface: {path: ../../../../packages/local_auth/local_auth_platform_interface} diff --git a/packages/local_auth/local_auth_windows/pubspec.yaml b/packages/local_auth/local_auth_windows/pubspec.yaml index 4b902ad672a9..e209116ab60e 100644 --- a/packages/local_auth/local_auth_windows/pubspec.yaml +++ b/packages/local_auth/local_auth_windows/pubspec.yaml @@ -30,3 +30,7 @@ topics: - authentication - biometrics - local-auth +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + local_auth_platform_interface: {path: ../../../packages/local_auth/local_auth_platform_interface}