diff --git a/example/pubspec.lock b/example/pubspec.lock index b9b62f0..7a10e65 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -68,7 +68,11 @@ packages: path: ".." relative: true source: path +<<<<<<< HEAD version: "6.0.0" +======= + version: "5.2.0" +>>>>>>> 60112c8 (feat: two step login) flutter_test: dependency: "direct dev" description: flutter @@ -79,30 +83,6 @@ packages: description: flutter source: sdk version: "0.0.0" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" - url: "https://pub.dev" - source: hosted - version: "10.0.0" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 - url: "https://pub.dev" - source: hosted - version: "2.0.1" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 - url: "https://pub.dev" - source: hosted - version: "2.0.1" lints: dependency: transitive description: @@ -115,34 +95,34 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.5.0" meta: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.10.0" path: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.8.3" pinput: dependency: transitive description: @@ -228,14 +208,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" - vm_service: + web: dependency: transitive description: - name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + name: web + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "0.3.0" sdks: - dart: ">=3.2.0-0 <4.0.0" + dart: ">=3.2.0-194.0.dev <4.0.0" flutter: ">=3.7.0" diff --git a/lib/flutter_login.dart b/lib/flutter_login.dart index 8b0b619..408fc8e 100644 --- a/lib/flutter_login.dart +++ b/lib/flutter_login.dart @@ -9,3 +9,4 @@ export 'src/service/validation.dart'; export 'src/widgets/email_password_login.dart'; export 'src/widgets/forgot_password_form.dart'; export 'src/widgets/mfa_widget.dart'; +export 'src/widgets/two_step_login.dart'; diff --git a/lib/src/widgets/two_step_login.dart b/lib/src/widgets/two_step_login.dart new file mode 100644 index 0000000..0eae8d3 --- /dev/null +++ b/lib/src/widgets/two_step_login.dart @@ -0,0 +1,175 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_login/flutter_login.dart'; + +class TwoStepLoginForm extends StatefulWidget { + /// Constructs an [TwoStepLoginForm] widget. + /// + /// [onCheckEmail]: Callback function for when a user has filled in its email + /// [options]: The options for configuring the login form. + const TwoStepLoginForm({ + required this.onCheckEmail, + super.key, + this.options = const LoginOptions(), + }); + + final LoginOptions options; + final FutureOr Function(String email) onCheckEmail; + @override + State createState() => _TwoStepLoginFormState(); +} + +class _TwoStepLoginFormState extends State { + final _formKey = GlobalKey(); + final ValueNotifier _formValid = ValueNotifier(false); + bool _obscurePassword = true; + + String _currentEmail = ''; + + void _updateCurrentEmail(String email) { + _currentEmail = email; + _validate(); + } + + void _validate() { + late var isValid = + widget.options.validations.validateEmail(_currentEmail) == null; + if (isValid != _formValid.value) { + _formValid.value = isValid; + } + } + + Future _handleLogin() async { + if (mounted) { + var form = _formKey.currentState!; + if (form.validate()) { + await widget.onCheckEmail( + _currentEmail, + ); + } + } + } + + @override + void initState() { + super.initState(); + _currentEmail = widget.options.initialEmail; + _validate(); + } + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + var options = widget.options; + return CustomScrollView( + physics: const ScrollPhysics(), + slivers: [ + SliverFillRemaining( + hasScrollBody: false, + fillOverscroll: true, + child: Column( + children: [ + Expanded( + flex: options.spacers.titleSpacer, + child: Column( + children: [ + if (options.spacers.spacerBeforeTitle != null) ...[ + Spacer(flex: options.spacers.spacerBeforeTitle!), + ], + if (options.title != null) ...[ + Align( + alignment: Alignment.topCenter, + child: _wrapWithDefaultStyle( + options.title, + theme.textTheme.headlineSmall, + ), + ), + ], + if (options.spacers.spacerAfterTitle != null) ...[ + Spacer(flex: options.spacers.spacerAfterTitle!), + ], + if (options.subtitle != null) ...[ + Align( + alignment: Alignment.topCenter, + child: _wrapWithDefaultStyle( + options.subtitle, + theme.textTheme.titleSmall, + ), + ), + ], + if (options.spacers.spacerAfterSubtitle != null) ...[ + Spacer(flex: options.spacers.spacerAfterSubtitle!), + ], + if (options.image != null) ...[ + Padding( + padding: const EdgeInsets.all(16), + child: options.image, + ), + ], + if (options.spacers.spacerAfterImage != null) ...[ + Spacer(flex: options.spacers.spacerAfterImage!), + ], + ], + ), + ), + Expanded( + flex: options.spacers.formFlexValue, + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: options.maxFormWidth ?? 300, + ), + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + options.emailInputContainerBuilder( + TextFormField( + onChanged: _updateCurrentEmail, + validator: widget.options.validations.validateEmail, + initialValue: options.initialEmail, + keyboardType: TextInputType.emailAddress, + textInputAction: TextInputAction.next, + style: options.emailTextStyle, + decoration: options.emailDecoration, + ), + ), + if (options.spacers.spacerAfterForm != null) ...[ + Spacer(flex: options.spacers.spacerAfterForm!), + ], + AnimatedBuilder( + animation: _formValid, + builder: (context, _) => options.loginButtonBuilder( + context, + _handleLogin, + !_formValid.value, + () { + _formKey.currentState?.validate(); + }, + options, + ), + ), + if (options.spacers.spacerAfterButton != null) ...[ + Spacer(flex: options.spacers.spacerAfterButton!), + ], + ], + ), + ), + ), + ), + ], + ), + ), + ], + ); + } + + Widget? _wrapWithDefaultStyle(Widget? widget, TextStyle? style) { + if (style == null || widget == null) { + return widget; + } else { + return DefaultTextStyle(style: style, child: widget); + } + } +}