From 9cc5e16dbabac99a5e1760478f1b20576c9afd73 Mon Sep 17 00:00:00 2001 From: Tobias Leijs Date: Tue, 15 Apr 2025 10:19:11 +0200 Subject: [PATCH] feat: add support for biometric login --- CHANGELOG.md | 4 + README.md | 38 ++++ assets/2.0x/ios_fingerprint.png | Bin 0 -> 4403 bytes assets/3.0x/ios_fingerprint.png | Bin 0 -> 6900 bytes assets/ios_fingerprint.png | Bin 0 -> 1934 bytes example/.gitignore | 3 + example/README.md | 1 + example/analysis_options.yaml | 35 +--- example/lib/main.dart | 93 +++++---- example/pubspec.lock | 228 ---------------------- example/pubspec.yaml | 13 +- example/test/widget_test.dart | 7 + lib/src/config/login_options.dart | 38 ++++ lib/src/service/local_auth_service.dart | 35 ++++ lib/src/widgets/biometrics_button.dart | 45 +++++ lib/src/widgets/email_password_login.dart | 32 ++- pubspec.yaml | 14 +- test/flutter_login_widget_test.dart | 7 + 18 files changed, 278 insertions(+), 315 deletions(-) create mode 100644 assets/2.0x/ios_fingerprint.png create mode 100644 assets/3.0x/ios_fingerprint.png create mode 100644 assets/ios_fingerprint.png create mode 120000 example/README.md delete mode 100644 example/pubspec.lock create mode 100644 example/test/widget_test.dart create mode 100644 lib/src/service/local_auth_service.dart create mode 100644 lib/src/widgets/biometrics_button.dart create mode 100644 test/flutter_login_widget_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c71b78..d1c12e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 7.3.0 + +* Added Biometrics support to the LoginOptions + ## 7.2.0 * Added CustomSemantics widget that is used to wrap all the inputfields and buttons to make the component accessible for e2e testing. diff --git a/README.md b/README.md index db33954..eca0c54 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,44 @@ A package facilitating the basic ingredients for creating functional yet customi To use this package, add `flutter_login` as a dependency in your pubspec.yaml file. + +### Biometrics authentication +You can use faceID or fingerprint by adding: +```dart + loginWithBiometrics: true, + triggerBiometricsAutomatically: true, +``` +to the `LoginOptions` object. This will trigger the biometrics authentication immediately when the EmailPasswordLoginForm is shown. + +For the full biometrics setup you can follow the instructions in the [local_auth](https://pub.dev/packages/local_auth) package. + +You need to add the following permissions to your AndroidManifest.xml file: + +```xml + +``` + +```java +import io.flutter.embedding.android.FlutterFragmentActivity; + +public class MainActivity extends FlutterFragmentActivity { + // ... +} +``` + +or MainActivity.kt: + +```kotlin +import io.flutter.embedding.android.FlutterFragmentActivity + +class MainActivity: FlutterFragmentActivity() { + // ... +} +``` + +to inherit from `FlutterFragmentActivity`. + + ## How to use ```dart diff --git a/assets/2.0x/ios_fingerprint.png b/assets/2.0x/ios_fingerprint.png new file mode 100644 index 0000000000000000000000000000000000000000..24c7976e6aea89dc221c047884668bb840332528 GIT binary patch literal 4403 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 literal 0 HcmV?d00001 diff --git a/assets/3.0x/ios_fingerprint.png b/assets/3.0x/ios_fingerprint.png new file mode 100644 index 0000000000000000000000000000000000000000..6fb33baffc05854978bfdff9a4e2498242c30aa1 GIT binary patch literal 6900 zcmVRpS`_3{Ju=z^Vw_`fBNaCIsD782e%lXfByN4#MC2@nK=dK$o%wq`}f~} zw=n^3F>WohlQbVBCj6irj_I=?X+`P%jlMJX)8|uIX@GtDKhU>@nCfhk(mgzd8vv62qtxGT$zRw2GX2h-JFjk{gZI&yot!LW@+oXG z8CB$Yl9Y>&CCwfr#)tIPq`p3ZmHOvs6VspHBo+VBm`&>XHEep)F){u;eZMU2^Gye; z`o#3(RD8sLN4uT|?bDl}-mjh6C=KZbAd?&Rb4+US`7gixat0IN>#x5KA3l5t7;7_c z`<_C63FNt>Bbd&cZ!^&7`cC00)6u3s|Cmg_R%Qz-yR*lzg(M?m|N85%Ce?G8KAk5G z^$@BC>v2YdMn(s!KE&O0qfZvCdZaq*=8Jt{6O&q6lp27M) zpy_!2>#x7Qgloc1nVr=3Gg8~D##Yk&*sHQ@AsMWg`zFrou*e|ZzV`rfjs^q3P>`ttZ=U!^N zovZc9nN2q8dswSLypZoal4mga#)V|W_+w&xV23)UC-wZic0LJ2PR#6Ftxw8qV)XyP z`r=z+bd^C$a-}7W(83LdV2=?l^`6c>rE@!=GgliZV=)=CX%-44I^h45;yiJl_X0N6 z;;mqE35Dq`Ii%4z?}*fTH_&&*@JX0W3`2?TI!lZ?t({joa46qc>C*jEuWWBxZjv%tG}`k4Snq1`xL8%Yy z(2y)zLu$I}c?&lD={9lrU}kewj1EU?{5y@dR7bGNetqZ2Fdb1i7TC`>CL9a2T15!6 zyw_JUvBq$e@azCT7@Ha%#B5^1_w;Y1h8GI0#OQ~XeU`j*I`8RCjk(8!S#;hIhBriu z7@b;mGrQ>Af*2h&`ocT*FuL^82j$Z>VBDQTr*6qGTgZ@OYe>_j@y8#3jIaeH2Mioi zK9J!jr0o!G@>w3jK)9KmnD;tQY>=eAG-$aqR%LWSKX`U5W6OmMb6RKq785ID){d3T z1BZW7o|D>stl9oVdPceUQTE%#Y*JOPsSz{oE5a*}szZF!&Km;(NI%PQKMWpE9m6!1>%&IaQ zQJ6E8L406Pa^^y4d!OzvY6&y5wkMoCSgI}i%LasbVPQlrJ)dWUV_iKIZ(pVlKGeTR z^f_&MZ@GRLV5@m+ltfs~p;sM7!OwZ1IANTT>Ok^J_nvSfmufXb!=>8j#Ix+B*Br9K z*}%}`q~h?XQ=oZ82{>;))n`3$Klw9Sf#td_b

7%9Tbr(ql|s!1S}TJq9Wubb-b>6N~qrg}yHhA+PO;#i`?L6ytt-i!U)U^b=*skp#| zg>(W+6|gj?M1zu%O##|{$+2K`r}S^MY;^W~m&MYiDVZz*O%DygJos^{^aX{IDkyb} zz_oE2BU@H)Lw5fP5+gB^Sy1_rQCCZCKaps9X5;9~Y5pIAJgYl zqZ4xJ$8kVD9GQV_k=APX2Y*j}eEs3Whab=}MaElMu&%QSs8xV6Tv!iAFbeI0v;ksY zQ|CCXnai{sTC5A!n&Oc%a5iYl<2|#P#J&Xss5&v}u1NJh%Y41jq>lzfpt71%-eZgF zmyT7eiCNN!kL8933JVxr&*r{%j83xf&%P6ddZ1d-(y_L zkD&vXK6~g{Q#pSg;O{7)`y>wjI019wW@KB6arMQ8yEw1IF zFsrx6SRa}Y8g$yZk*Vp8$)kdgjU%xgM=gT0OJk$7p%PsH!xZ zMiruEoOv%;|=8IvbWMtkVkA7c7b*8XXQ;VpBp4h*lmD!G{XqoV4l zaaC@3AFElljW|^8?*1UPLsiA8)*=mV~OiDv-o*CZ!Ga*Kw zBoxDX$O()1KQUZQq@?RTg}T?>!dfa1m3q!{xm3Xn7d4txPxePPZ2OGtOxHP0BEx#O zUc_$_4ds+OXcEh5w4@DfQky%idGkjCqvtRou)ZS9Qf)HXZ;#|=iEE@ebsE^_fFLaw zEgB$`fxvu!U%q?yZa)gyh}Zz`JH|TPN;J#Wyzxdu0!>!oZ=yWQ6QCm16prN(X0aSF z1&Ue&!zW1up43 z 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 + Opacity( + opacity: isDisabled ? 0.5 : 1.0, + child: ElevatedButton( + onPressed: isDisabled ? onDisabledPress : onPressed, + child: const Text("Send request"), + ), + ), ); void main() { @@ -40,54 +43,46 @@ class LoginExample extends StatelessWidget { const LoginExample({super.key}); @override - Widget build(BuildContext context) { - return MaterialApp( - theme: ThemeData.dark(), - home: const LoginScreen(), - ); - } + Widget build(BuildContext context) => MaterialApp( + theme: ThemeData.dark(), + home: const LoginScreen(), + ); } class LoginScreen extends StatelessWidget { const LoginScreen({super.key}); @override - Widget build(BuildContext context) { - return Scaffold( - body: EmailPasswordLoginForm( - title: const Text('Login Demo'), - options: loginOptions, - onLogin: (email, password) => print('$email:$password'), - onRegister: (email, password, ctx) => print('Register!'), - onForgotPassword: (email, ctx) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) { - return const ForgotPasswordScreen(); - }, - ), - ); - }, - ), - ); - } + Widget build(BuildContext context) => Scaffold( + body: EmailPasswordLoginForm( + title: const Text("Login Demo"), + options: loginOptions, + onLogin: (email, password) => print("$email:$password"), + onRegister: (email, password, ctx) => print("Register!"), + onForgotPassword: (email, ctx) async { + await Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const ForgotPasswordScreen(), + ), + ); + }, + ), + ); } class ForgotPasswordScreen extends StatelessWidget { const ForgotPasswordScreen({super.key}); @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(), - body: ForgotPasswordForm( - options: loginOptions, - title: const Text('Forgot password'), - description: const Text('Hello world'), - onRequestForgotPassword: (email) { - print('Forgot password email sent to $email'); - }, - ), - ); - } + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(), + body: ForgotPasswordForm( + options: loginOptions, + title: const Text("Forgot password"), + description: const Text("Hello world"), + onRequestForgotPassword: (email) { + print("Forgot password email sent to $email"); + }, + ), + ); } diff --git a/example/pubspec.lock b/example/pubspec.lock deleted file mode 100644 index d1ab254..0000000 --- a/example/pubspec.lock +++ /dev/null @@ -1,228 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - async: - dependency: transitive - description: - name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" - url: "https://pub.dev" - source: hosted - version: "2.11.0" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - characters: - dependency: transitive - description: - name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" - url: "https://pub.dev" - source: hosted - version: "1.3.0" - clock: - dependency: transitive - description: - name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf - url: "https://pub.dev" - source: hosted - version: "1.1.1" - collection: - dependency: transitive - description: - name: collection - sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf - url: "https://pub.dev" - source: hosted - version: "1.19.0" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" - url: "https://pub.dev" - source: hosted - version: "1.3.1" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 - url: "https://pub.dev" - source: hosted - version: "2.0.3" - flutter_login: - dependency: "direct main" - description: - path: ".." - relative: true - source: path - version: "7.2.0" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" - url: "https://pub.dev" - source: hosted - version: "10.0.7" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" - url: "https://pub.dev" - source: hosted - version: "3.0.8" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" - url: "https://pub.dev" - source: hosted - version: "3.0.1" - lints: - dependency: transitive - description: - name: lints - sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - matcher: - dependency: transitive - description: - name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb - url: "https://pub.dev" - source: hosted - version: "0.12.16+1" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec - url: "https://pub.dev" - source: hosted - version: "0.11.1" - meta: - dependency: transitive - description: - name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 - url: "https://pub.dev" - source: hosted - version: "1.15.0" - path: - dependency: transitive - description: - name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" - url: "https://pub.dev" - source: hosted - version: "1.9.0" - pinput: - dependency: transitive - description: - name: pinput - sha256: "8a73be426a91fefec90a7f130763ca39772d547e92f19a827cf4aa02e323d35a" - url: "https://pub.dev" - source: hosted - version: "5.0.1" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.99" - source_span: - dependency: transitive - description: - name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" - url: "https://pub.dev" - source: hosted - version: "1.10.0" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" - url: "https://pub.dev" - source: hosted - version: "1.12.0" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 - url: "https://pub.dev" - source: hosted - version: "2.1.2" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" - url: "https://pub.dev" - source: hosted - version: "1.3.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 - url: "https://pub.dev" - source: hosted - version: "1.2.1" - test_api: - dependency: transitive - description: - name: test_api - sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" - url: "https://pub.dev" - source: hosted - version: "0.7.3" - universal_platform: - dependency: transitive - description: - name: universal_platform - sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b - url: "https://pub.dev" - source: hosted - version: "14.3.0" -sdks: - dart: ">=3.4.0 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index b873dba..6a9254b 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,14 +1,12 @@ name: example description: A new Flutter project. -# The following line prevents the package from being accidentally published to -# pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev +publish_to: "none" version: 1.0.0+1 environment: - sdk: '>=2.18.1 <3.0.0' + sdk: ">=2.18.1 <3.0.0" dependencies: flutter: @@ -16,11 +14,12 @@ dependencies: flutter_login: path: ../ - dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^2.0.0 + flutter_iconica_analysis: + hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub + version: ^7.0.0 flutter: - uses-material-design: true \ No newline at end of file + uses-material-design: true diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart new file mode 100644 index 0000000..090f3f2 --- /dev/null +++ b/example/test/widget_test.dart @@ -0,0 +1,7 @@ +import "package:flutter_test/flutter_test.dart"; + +void main() { + test("blank test", () { + expect(true, true); + }); +} diff --git a/lib/src/config/login_options.dart b/lib/src/config/login_options.dart index eddeeff..984f998 100644 --- a/lib/src/config/login_options.dart +++ b/lib/src/config/login_options.dart @@ -75,6 +75,7 @@ class LoginOptions { this.forgotPasswordCustomAppBar, this.suffixIconSize, this.suffixIconPadding, + this.biometricsOptions = const LoginBiometricsOptions(), }); /// Builds the login button. @@ -164,6 +165,41 @@ class LoginOptions { /// forgot password custom AppBar final AppBar? forgotPasswordCustomAppBar; + + /// Options for enabling and customizing biometrics login + final LoginBiometricsOptions biometricsOptions; +} + +class LoginBiometricsOptions { + const LoginBiometricsOptions({ + this.loginWithBiometrics = false, + this.triggerBiometricsAutomatically = false, + this.allowBiometricsAlternative = true, + this.onBiometricsSuccess, + this.onBiometricsError, + this.onBiometricsFail, + }); + + /// Ask the user to login with biometrics instead of email and password. + final bool loginWithBiometrics; + + /// Allow the user to login with biometrics even if they have no biometrics + /// set up on their device. This will use their device native login methods. + final bool allowBiometricsAlternative; + + /// Automatically open the native biometrics UI instead of waiting for the + /// user to press the biometrics button + final bool triggerBiometricsAutomatically; + + /// The callback function to be called when the biometrics login is + /// successful. + final OptionalAsyncCallback? onBiometricsSuccess; + + /// The callback function to be called when the biometrics login fails. + final OptionalAsyncCallback? onBiometricsFail; + + /// The callback function to be called when the biometrics login errors. + final OptionalAsyncCallback? onBiometricsError; } /// Translations for all the texts in the component @@ -177,6 +213,7 @@ class LoginTranslations { this.forgotPasswordButton = "Forgot password?", this.requestForgotPasswordButton = "Send link", this.registrationButton = "Create account", + this.biometricsLoginMessage = "Log in with biometrics", }); final String emailInvalid; @@ -186,6 +223,7 @@ class LoginTranslations { final String forgotPasswordButton; final String requestForgotPasswordButton; final String registrationButton; + final String biometricsLoginMessage; } /// Accessibility identifiers for the standard widgets in the component. diff --git a/lib/src/service/local_auth_service.dart b/lib/src/service/local_auth_service.dart new file mode 100644 index 0000000..0363918 --- /dev/null +++ b/lib/src/service/local_auth_service.dart @@ -0,0 +1,35 @@ +import "package:flutter/services.dart"; +import "package:flutter_login/src/config/login_options.dart"; +import "package:local_auth/local_auth.dart"; + +class LocalAuthService { + final LocalAuthentication _localAuth = LocalAuthentication(); + + Future authenticate(LoginOptions loginOptions) async { + var biometricsOptions = loginOptions.biometricsOptions; + + try { + if (!await _localAuth.isDeviceSupported()) { + biometricsOptions.onBiometricsError?.call(); + return; + } + var didAuthenticate = await _localAuth.authenticate( + localizedReason: loginOptions.translations.biometricsLoginMessage, + options: AuthenticationOptions( + biometricOnly: !biometricsOptions.allowBiometricsAlternative, + stickyAuth: true, + sensitiveTransaction: false, + ), + ); + if (didAuthenticate) { + biometricsOptions.onBiometricsSuccess?.call(); + } + + if (!didAuthenticate) { + biometricsOptions.onBiometricsFail?.call(); + } + } on PlatformException catch (_) { + biometricsOptions.onBiometricsError?.call(); + } + } +} diff --git a/lib/src/widgets/biometrics_button.dart b/lib/src/widgets/biometrics_button.dart new file mode 100644 index 0000000..897b2d5 --- /dev/null +++ b/lib/src/widgets/biometrics_button.dart @@ -0,0 +1,45 @@ +import "dart:async"; +import "dart:io"; + +import "package:flutter/foundation.dart"; +import "package:flutter/material.dart"; + +class BiometricsButton extends StatelessWidget { + const BiometricsButton({ + required this.onPressed, + super.key, + }); + + static const Size buttonSize = Size(40, 40); + + final FutureOr Function() onPressed; + + @override + Widget build(BuildContext context) { + // handle unsupported platforms + if (kIsWeb || Platform.isLinux) return SizedBox(width: buttonSize.width); + Widget biometricsWidget; + + if (Platform.isIOS || Platform.isMacOS) { + biometricsWidget = Image( + image: const AssetImage( + "assets/ios_fingerprint.png", + package: "flutter_login", + ), + width: buttonSize.width, + height: buttonSize.height, + ); + } else { + biometricsWidget = Icon( + Icons.fingerprint, + size: buttonSize.width, + ); + } + + return InkWell( + borderRadius: BorderRadius.circular(20), + onTap: onPressed, + child: biometricsWidget, + ); + } +} diff --git a/lib/src/widgets/email_password_login.dart b/lib/src/widgets/email_password_login.dart index a2cfab5..c1a5421 100644 --- a/lib/src/widgets/email_password_login.dart +++ b/lib/src/widgets/email_password_login.dart @@ -2,6 +2,8 @@ import "dart:async"; import "package:flutter/material.dart"; import "package:flutter_login/flutter_login.dart"; +import "package:flutter_login/src/service/local_auth_service.dart"; +import "package:flutter_login/src/widgets/biometrics_button.dart"; import "package:flutter_login/src/widgets/custom_semantics.dart"; class EmailPasswordLoginForm extends StatefulWidget { @@ -49,6 +51,8 @@ class _EmailPasswordLoginFormState extends State { String _currentEmail = ""; String _currentPassword = ""; + final LocalAuthService _localAuthService = LocalAuthService(); + void _updateCurrentEmail(String email) { _currentEmail = email; _validate(); @@ -87,6 +91,13 @@ class _EmailPasswordLoginFormState extends State { _currentEmail = widget.options.initialEmail; _currentPassword = widget.options.initialPassword; _validate(); + + if (widget.options.biometricsOptions.loginWithBiometrics && + widget.options.biometricsOptions.triggerBiometricsAutomatically) { + WidgetsBinding.instance.addPostFrameCallback((_) async { + await _localAuthService.authenticate(widget.options); + }); + } } @override @@ -195,6 +206,10 @@ class _EmailPasswordLoginFormState extends State { ), ); + var biometricsButton = BiometricsButton( + onPressed: () async => LocalAuthService().authenticate(options), + ); + return Scaffold( backgroundColor: options.loginBackgroundColor, body: CustomScrollView( @@ -235,7 +250,22 @@ class _EmailPasswordLoginFormState extends State { if (options.spacers.spacerAfterForm != null) ...[ Spacer(flex: options.spacers.spacerAfterForm!), ], - loginButton, + if (options + .biometricsOptions.loginWithBiometrics) ...[ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + SizedBox( + width: BiometricsButton.buttonSize.width, + ), + loginButton, + biometricsButton, + ], + ), + ] else ...[ + loginButton, + ], if (widget.onRegister != null) ...[ registrationButton, ], diff --git a/pubspec.yaml b/pubspec.yaml index 4465171..50d9308 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_login description: Flutter Login Component -version: 7.2.0 +version: 7.3.0 publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub @@ -11,12 +11,18 @@ environment: dependencies: flutter: sdk: flutter + local_auth: ^2.3.0 pinput: ^5.0.1 dev_dependencies: flutter_test: sdk: flutter flutter_iconica_analysis: - git: - url: https://github.com/Iconica-Development/flutter_iconica_analysis - ref: 7.0.0 + hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub + version: ^7.0.0 + +flutter: + assets: + - assets/ + - assets/2.0x/ + - assets/3.0x/ diff --git a/test/flutter_login_widget_test.dart b/test/flutter_login_widget_test.dart new file mode 100644 index 0000000..090f3f2 --- /dev/null +++ b/test/flutter_login_widget_test.dart @@ -0,0 +1,7 @@ +import "package:flutter_test/flutter_test.dart"; + +void main() { + test("blank test", () { + expect(true, true); + }); +}