diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f158a4..59041b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,16 @@ SPDX-FileCopyrightText: 2022 Iconica SPDX-License-Identifier: GPL-3.0-or-later --> -## 0.0.1 +## 0.2.0 -* Initial version +- Added the abilty to add labels +- The default password step now includes two textfields ## 0.0.2 -* Firebase integration -* Registration capabilities \ No newline at end of file +- Firebase integration +- Registration capabilities + +## 0.0.1 + +- Initial version diff --git a/example/pubspec.lock b/example/pubspec.lock index a0a564a..bb97a29 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -5,49 +5,56 @@ packages: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + url: "https://pub.dev" source: hosted - version: "2.9.0" + version: "2.10.0" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + url: "https://pub.dev" source: hosted version: "1.2.1" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted version: "1.1.1" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - url: "https://pub.dartlang.org" + sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be + url: "https://pub.dev" source: hosted version: "1.0.5" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" source: hosted version: "1.3.1" flutter: @@ -59,14 +66,16 @@ packages: dependency: transitive description: name: flutter_hooks - url: "https://pub.dartlang.org" + sha256: "2b202559a4ed3656bbb7aae9d8b335fb0037b23acc7ae3f377d1ba0b95c21aec" + url: "https://pub.dev" source: hosted version: "0.18.5+1" flutter_lints: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + url: "https://pub.dev" source: hosted version: "2.0.1" flutter_localizations: @@ -80,7 +89,7 @@ packages: path: ".." relative: true source: path - version: "0.1.0" + version: "0.2.0" flutter_test: dependency: "direct dev" description: flutter @@ -90,42 +99,56 @@ packages: dependency: transitive description: name: intl - url: "https://pub.dartlang.org" + sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + url: "https://pub.dev" source: hosted version: "0.17.0" + js: + dependency: transitive + description: + name: js + sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + url: "https://pub.dev" + source: hosted + version: "0.6.5" lints: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + sha256: "5cfd6509652ff5e7fe149b6df4859e687fca9048437857cb2e65c8d780f396e3" + url: "https://pub.dev" source: hosted version: "2.0.0" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + url: "https://pub.dev" source: hosted - version: "0.12.12" + version: "0.12.13" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" source: hosted - version: "0.1.5" + version: "0.2.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + url: "https://pub.dev" source: hosted version: "1.8.0" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + url: "https://pub.dev" source: hosted version: "1.8.2" sky_engine: @@ -137,51 +160,58 @@ packages: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted version: "1.2.1" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 + url: "https://pub.dev" source: hosted - version: "0.4.12" + version: "0.4.16" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" sdks: dart: ">=2.18.0 <3.0.0" flutter: ">=3.0.0" diff --git a/lib/src/auth_screen.dart b/lib/src/auth_screen.dart index 9d4b3f8..44955f2 100644 --- a/lib/src/auth_screen.dart +++ b/lib/src/auth_screen.dart @@ -16,6 +16,8 @@ class AuthScreen extends StatefulWidget { required this.previousBtnTitle, required this.onFinish, this.customAppBar, + this.nextButtonBuilder, + this.previousButtonBuilder, super.key, }) : assert(steps.length > 0, 'At least one step is required'); @@ -29,6 +31,10 @@ class AuthScreen extends StatefulWidget { final String nextBtnTitle; final String previousBtnTitle; final AppBar? customAppBar; + final Widget Function(VoidCallback onPressed, String label)? + nextButtonBuilder; + final Widget Function(VoidCallback onPressed, String label)? + previousButtonBuilder; @override State createState() => _AuthScreenState(); @@ -46,9 +52,52 @@ class _AuthScreenState extends State { title: Text(widget.title), ); + void onPrevious() { + _pageController.previousPage( + duration: _animationDuration, + curve: _animationCurve, + ); + } + + void onNext(AuthStep step) { + if (!_formKey.currentState!.validate()) { + return; + } + + _formKey.currentState!.save(); + + FocusScope.of(context).unfocus(); + + if (widget.steps.last == step) { + var values = HashMap(); + + for (var step in widget.steps) { + for (var field in step.fields) { + values[field.name] = field.value; + } + } + + widget.onFinish( + values: values, + onError: () => _pageController.animateToPage( + 0, + duration: _animationDuration, + curve: _animationCurve, + ), + ); + + return; + } else { + _pageController.nextPage( + duration: _animationDuration, + curve: _animationCurve, + ); + } + } + @override Widget build(BuildContext context) => Scaffold( - backgroundColor: Theme.of(context).backgroundColor, + backgroundColor: Theme.of(context).colorScheme.background, appBar: _appBar, body: Form( key: _formKey, @@ -75,18 +124,9 @@ class _AuthScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: const EdgeInsets.only( - top: 24.0, - bottom: 12.0, - ), - child: Text( - field.title, - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - ), + if (field.title != null) ...[ + field.title!, + ], field.build(), ], ), @@ -108,75 +148,53 @@ class _AuthScreenState extends State { : MainAxisAlignment.end, children: [ if (widget.steps.first != step) - ElevatedButton( - onPressed: () => _pageController.previousPage( - duration: _animationDuration, - curve: _animationCurve, - ), - child: Row( - children: [ - const Icon( - Icons.arrow_back, - size: 18, - ), - Padding( - padding: const EdgeInsets.only(left: 4.0), - child: Text(widget.previousBtnTitle), - ), - ], - ), - ), - ElevatedButton( - onPressed: () { - if (!_formKey.currentState!.validate()) { - return; - } - - FocusScope.of(context).unfocus(); - - if (widget.steps.last == step) { - var values = HashMap(); - - for (var step in widget.steps) { - for (var field in step.fields) { - values[field.name] = field.value; - } - } - - widget.onFinish( - values: values, - onError: () => _pageController.animateToPage( - 0, - duration: _animationDuration, - curve: _animationCurve, - ), - ); - - return; - } - - _pageController.nextPage( - duration: _animationDuration, - curve: _animationCurve, - ); - }, - child: Row( - children: [ - Text( - widget.steps.last == step - ? widget.submitBtnTitle - : widget.nextBtnTitle, - ), - const Padding( - padding: EdgeInsets.only(left: 4.0), - child: Icon( - Icons.arrow_forward, - size: 18, + widget.previousButtonBuilder?.call( + onPrevious, + widget.previousBtnTitle, + ) ?? + ElevatedButton( + onPressed: onPrevious, + child: Row( + children: [ + const Icon( + Icons.arrow_back, + size: 18, + ), + Padding( + padding: + const EdgeInsets.only(left: 4.0), + child: Text(widget.previousBtnTitle), + ), + ], ), ), - ], - ), - ), + widget.nextButtonBuilder?.call( + () => onNext(step), + widget.steps.last == step + ? widget.submitBtnTitle + : widget.nextBtnTitle, + ) ?? + ElevatedButton( + onPressed: () { + onNext(step); + }, + child: Row( + children: [ + Text( + widget.steps.last == step + ? widget.submitBtnTitle + : widget.nextBtnTitle, + ), + const Padding( + padding: EdgeInsets.only(left: 4.0), + child: Icon( + Icons.arrow_forward, + size: 18, + ), + ), + ], + ), + ), ], ), ) diff --git a/lib/src/config/registration_options.dart b/lib/src/config/registration_options.dart index a03aeb4..fb4d175 100644 --- a/lib/src/config/registration_options.dart +++ b/lib/src/config/registration_options.dart @@ -12,6 +12,8 @@ class RegistrationOptions { required this.afterRegistration, this.registrationTranslations = const RegistrationTranslations(), this.customAppbarBuilder, + this.nextButtonBuilder, + this.previousButtonBuilder, }); final RegistrationTranslations registrationTranslations; @@ -19,44 +21,101 @@ class RegistrationOptions { final VoidCallback afterRegistration; final RegistrationRepository registrationRepository; final AppBar Function(String title)? customAppbarBuilder; + final Widget Function(VoidCallback onPressed, String label)? + nextButtonBuilder; + final Widget Function(VoidCallback onPressed, String label)? + previousButtonBuilder; static List getDefaultSteps({ RegistrationTranslations translations = const RegistrationTranslations(), - }) => - [ - AuthStep( - fields: [ - AuthTextField( - name: 'email', - title: translations.defaultEmailTitle, - hintText: translations.defaultEmailHint, - validators: [ - (email) => (email == null || email.isEmpty) - ? translations.defaultEmailEmpty - : null, - (email) => - RegExp(r"""(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])""") - .hasMatch(email!) - ? null - : translations.defaultEmailValidatorMessage, - ], - ) - ], - ), - AuthStep( - fields: [ - AuthTextField( - name: 'password', - title: translations.defaultPasswordTitle, - hintText: translations.defaultPasswordHint, - obscureText: true, - validators: [ - (value) => (value == null || value.isEmpty) - ? translations.defaultPasswordValidatorMessage - : null, - ], - ), - ], - ), - ]; + Function(String title)? titleBuilder, + Function(String label)? labelBuilder, + TextStyle? textStyle, + }) { + var password1 = ''; + + return [ + AuthStep( + fields: [ + AuthTextField( + name: 'email', + title: titleBuilder?.call( + translations.defaultEmailTitle, + ) ?? + Padding( + padding: const EdgeInsets.only( + top: 24.0, + bottom: 12.0, + ), + child: Text( + translations.defaultEmailTitle, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ), + label: labelBuilder?.call(translations.defaultEmailLabel), + hintText: translations.defaultEmailHint, + textStyle: textStyle, + validators: [ + (email) => (email == null || email.isEmpty) + ? translations.defaultEmailEmpty + : null, + (email) => + RegExp(r"""(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])""") + .hasMatch(email!) + ? null + : translations.defaultEmailValidatorMessage, + ], + ) + ], + ), + AuthStep( + fields: [ + AuthTextField( + name: 'password1', + title: titleBuilder?.call( + translations.defaultPassword1Title, + ) ?? + Padding( + padding: const EdgeInsets.only( + top: 24.0, + bottom: 12.0, + ), + child: Text( + translations.defaultPassword1Title, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ), + label: labelBuilder?.call(translations.defaultPassword1Label), + hintText: translations.defaultPassword1Hint, + textStyle: textStyle, + obscureText: true, + validators: [ + (value) => (value == null || value.isEmpty) + ? translations.defaultPassword1ValidatorMessage + : null, + ], + onChange: (value) { + password1 = value; + }, + ), + AuthTextField( + name: 'password2', + label: labelBuilder?.call(translations.defaultPassword2Label), + hintText: translations.defaultPassword2Hint, + textStyle: textStyle, + obscureText: true, + validators: [ + (value) => (value != password1) + ? translations.defaultPassword2ValidatorMessage + : null, + ], + ), + ], + ), + ]; + } } diff --git a/lib/src/config/registration_translations.dart b/lib/src/config/registration_translations.dart index 55a7dbe..4c0d15b 100644 --- a/lib/src/config/registration_translations.dart +++ b/lib/src/config/registration_translations.dart @@ -10,12 +10,17 @@ class RegistrationTranslations { this.nextStepBtn = 'Volgende stap', this.closeBtn = 'Sluiten', this.defaultEmailTitle = 'Wat is je e-mailadres?', + this.defaultEmailLabel = '', this.defaultEmailHint = 'iemand@voorbeeld.nl', this.defaultEmailEmpty = 'Geef uw e-mailadres op', this.defaultEmailValidatorMessage = 'Geef een geldig e-mailadres op', - this.defaultPasswordTitle = 'Kies een wachtwoord', - this.defaultPasswordHint = '', - this.defaultPasswordValidatorMessage = 'Geef een wachtwoord op', + this.defaultPassword1Title = 'Kies een wachtwoord', + this.defaultPassword1Label = '', + this.defaultPassword1Hint = '', + this.defaultPassword1ValidatorMessage = 'Geef een wachtwoord op', + this.defaultPassword2Label = '', + this.defaultPassword2Hint = '', + this.defaultPassword2ValidatorMessage = 'Wachtwoorden moeten gelijk zijn', }); final String title; @@ -24,10 +29,15 @@ class RegistrationTranslations { final String nextStepBtn; final String closeBtn; final String defaultEmailTitle; + final String defaultEmailLabel; final String defaultEmailHint; final String defaultEmailEmpty; final String defaultEmailValidatorMessage; - final String defaultPasswordTitle; - final String defaultPasswordHint; - final String defaultPasswordValidatorMessage; + final String defaultPassword1Title; + final String defaultPassword1Label; + final String defaultPassword1Hint; + final String defaultPassword1ValidatorMessage; + final String defaultPassword2Label; + final String defaultPassword2Hint; + final String defaultPassword2ValidatorMessage; } diff --git a/lib/src/model/auth_field.dart b/lib/src/model/auth_field.dart index df3e015..23ad82b 100644 --- a/lib/src/model/auth_field.dart +++ b/lib/src/model/auth_field.dart @@ -7,13 +7,13 @@ import 'package:flutter/material.dart'; abstract class AuthField { AuthField({ required this.name, - required this.title, + this.title, this.validators = const [], this.value = '', }); final String name; - final String title; + final Widget? title; List validators; String value; diff --git a/lib/src/model/auth_text_field.dart b/lib/src/model/auth_text_field.dart index 1dec690..dd6b17a 100644 --- a/lib/src/model/auth_text_field.dart +++ b/lib/src/model/auth_text_field.dart @@ -8,11 +8,14 @@ import 'package:flutter_registration/flutter_registration.dart'; class AuthTextField extends AuthField { AuthTextField({ required super.name, - required super.title, + super.title, super.validators = const [], super.value = '', this.obscureText = false, this.hintText, + this.label, + this.textStyle, + this.onChange, }) { _textEditingController = TextEditingController(); } @@ -20,16 +23,22 @@ class AuthTextField extends AuthField { late TextEditingController _textEditingController; final bool obscureText; final String? hintText; + final Widget? label; + final TextStyle? textStyle; + final Function(String value)? onChange; @override Widget build() => TextFormField( + style: textStyle, decoration: InputDecoration( + label: label, hintText: hintText, ), controller: _textEditingController, obscureText: obscureText, onChanged: (v) { value = v; + onChange?.call(value); }, validator: (value) { for (var validator in validators) { diff --git a/lib/src/registration_screen.dart b/lib/src/registration_screen.dart index b57d24c..4ea7ba2 100644 --- a/lib/src/registration_screen.dart +++ b/lib/src/registration_screen.dart @@ -44,6 +44,8 @@ class RegistrationScreen extends StatelessWidget { submitBtnTitle: translations.registerBtn, nextBtnTitle: translations.nextStepBtn, previousBtnTitle: translations.previousStepBtn, + nextButtonBuilder: registrationOptions.nextButtonBuilder, + previousButtonBuilder: registrationOptions.previousButtonBuilder, ); } } diff --git a/pubspec.yaml b/pubspec.yaml index 9dbedfd..1ab313b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ name: flutter_registration description: A Flutter Registration package -version: 0.1.0 +version: 0.2.0 repository: https://github.com/Iconica-Development/flutter_registration environment: