From 490ebcef5fcfee530c9754076f3b181ef90d93b1 Mon Sep 17 00:00:00 2001 From: Freek van de Ven Date: Thu, 12 Jun 2025 10:42:02 +0200 Subject: [PATCH 01/16] chore: change nullable variables in flutter_user navigator userstory to late's to remove null checks --- .../src/flutter_user_navigator_userstory.dart | 74 ++++++++++--------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/packages/flutter_user/lib/src/flutter_user_navigator_userstory.dart b/packages/flutter_user/lib/src/flutter_user_navigator_userstory.dart index d9bb945..206f67f 100644 --- a/packages/flutter_user/lib/src/flutter_user_navigator_userstory.dart +++ b/packages/flutter_user/lib/src/flutter_user_navigator_userstory.dart @@ -33,10 +33,11 @@ class FlutterUserNavigatorUserstory extends StatefulWidget { class _FlutterUserNavigatorUserstoryState extends State { - UserService? userService; - FlutterUserOptions? options; - ForgotPasswordTranslations? forgotPasswordTranslations; - RegistrationOptions? registrationOptions; + late final UserService userService; + late final FlutterUserOptions options; + late final ForgotPasswordTranslations forgotPasswordTranslations; + late final RegistrationOptions registrationOptions; + @override void initState() { userService = widget.userService ?? UserService(); @@ -86,17 +87,17 @@ class _FlutterUserNavigatorUserstoryState var theme = Theme.of(context); var title = Text( - options!.loginTranslations.loginTitle, + options.loginTranslations.loginTitle, style: theme.textTheme.headlineLarge, ); - var subtitle = Text(options?.loginTranslations.loginSubtitle ?? ""); + var subtitle = Text(options.loginTranslations.loginSubtitle ?? ""); FutureOr onLogin(String email, String password) async { - await options?.beforeLogin?.call(email, password); + await options.beforeLogin?.call(email, password); if (!mounted) return; unawaited(showLoadingIndicator(context)); try { - await userService?.loginWithEmailAndPassword( + await userService.loginWithEmailAndPassword( email: email, password: password, ); @@ -104,21 +105,22 @@ class _FlutterUserNavigatorUserstoryState if (!mounted) return; Navigator.of(context, rootNavigator: true).pop(); if (!context.mounted) return; - var authErrorDetails = options!.authExceptionFormatter.format(e); + var authErrorDetails = options.authExceptionFormatter.format(e); await errorScaffoldMessenger(context, authErrorDetails); return; } - await options?.afterLogin?.call(); + await options.afterLogin?.call(); - var onboardingUser = await options?.onBoardedUser?.call(); + var onboardingUser = await options.onBoardedUser?.call(); if (!mounted) return; Navigator.of(context, rootNavigator: true).pop(); - if (options!.useOnboarding && onboardingUser?.onboarded == false) { + + if (options.useOnboarding && onboardingUser?.onboarded == false) { await push( Onboarding( onboardingFinished: (results) async { - await options?.onOnboardingComplete?.call(results); + await options.onOnboardingComplete?.call(results); if (!mounted || !context.mounted) return; Navigator.of(context).pop(); await pushReplacement(widget.afterLoginScreen); @@ -134,14 +136,14 @@ class _FlutterUserNavigatorUserstoryState return EmailPasswordLoginForm( title: title, subtitle: subtitle, - options: options!.loginOptions, + options: options.loginOptions, onLogin: onLogin, onForgotPassword: (email, ctx) async { - await options?.onForgotPassword?.call(email, ctx) ?? + await options.onForgotPassword?.call(email, ctx) ?? await push(_forgotPasswordScreen()); }, onRegister: (email, password, context) async { - await options?.onRegister?.call(email, password, context) ?? + await options.onRegister?.call(email, password, context) ?? await push(_registrationScreen()); }, ); @@ -150,27 +152,27 @@ class _FlutterUserNavigatorUserstoryState Widget _forgotPasswordScreen() { var theme = Theme.of(context); var title = Text( - options!.forgotPasswordTranslations.forgotPasswordTitle, + options.forgotPasswordTranslations.forgotPasswordTitle, style: theme.textTheme.headlineLarge, ); var description = Padding( padding: const EdgeInsets.only(top: 8, bottom: 32), child: Text( - options!.forgotPasswordTranslations.forgotPasswordDescription, + options.forgotPasswordTranslations.forgotPasswordDescription, textAlign: TextAlign.center, ), ); FutureOr onRequestForgotPassword(String email) async { - if (options?.onRequestForgotPassword != null) { - await options!.onRequestForgotPassword!(email); + if (options.onRequestForgotPassword != null) { + await options.onRequestForgotPassword!(email); return; } unawaited(showLoadingIndicator(context)); try { - var response = await userService!.requestChangePassword(email: email); + var response = await userService.requestChangePassword(email: email); if (!mounted) return; Navigator.of(context).pop(); if (response.requestSuccesfull) { @@ -189,38 +191,38 @@ class _FlutterUserNavigatorUserstoryState return ForgotPasswordForm( title: title, description: description, - loginOptions: options!.loginOptions, + loginOptions: options.loginOptions, forgotPasswordOptions: widget.forgotPasswordOptions, onRequestForgotPassword: onRequestForgotPassword, ); } Widget _forgotPasswordSuccessScreen() => ForgotPasswordSuccess( - translations: options!.forgotPasswordTranslations, + translations: options.forgotPasswordTranslations, onRequestForgotPassword: () async { - await options?.onForgotPasswordSuccess?.call() ?? + await options.onForgotPasswordSuccess?.call() ?? // ignore: use_build_context_synchronously Navigator.of(context).pop(); }, ); Widget _forgotPasswordUnsuccessfullScreen() => ForgotPasswordUnsuccessfull( - translations: forgotPasswordTranslations!, + translations: forgotPasswordTranslations, onPressed: () async { - await options?.onForgotPasswordUnsuccessful?.call() ?? + await options.onForgotPasswordUnsuccessful?.call() ?? // ignore: use_build_context_synchronously Navigator.of(context).pop(); }, ); Widget _registrationScreen() => RegistrationScreen( - registrationOptions: registrationOptions!, - userService: userService!, + registrationOptions: registrationOptions, + userService: userService, onError: (error) async { - var errorDetails = options!.authExceptionFormatter.format(error); + var errorDetails = options.authExceptionFormatter.format(error); - if (options?.onRegistrationError != null) { - return options!.onRegistrationError!(error, errorDetails); + if (options.onRegistrationError != null) { + return options.onRegistrationError!(error, errorDetails); } await push( _registrationUnsuccessfullScreen( @@ -235,15 +237,15 @@ class _FlutterUserNavigatorUserstoryState return null; }, afterRegistration: () async { - options?.afterRegistration?.call() ?? + options.afterRegistration?.call() ?? await pushReplacement(_registrationSuccessScreen()); }, ); Widget _registrationSuccessScreen() => RegistrationSuccess( - registrationOptions: registrationOptions!, + registrationOptions: registrationOptions, onPressed: () async { - await options?.afterRegistrationSuccess?.call() ?? + await options.afterRegistrationSuccess?.call() ?? // ignore: use_build_context_synchronously Navigator.of(context).pop(); }, @@ -251,9 +253,9 @@ class _FlutterUserNavigatorUserstoryState Widget _registrationUnsuccessfullScreen(AuthErrorDetails errorDetails) => RegistrationUnsuccessfull( - registrationOptions: registrationOptions!, + registrationOptions: registrationOptions, onPressed: () async { - await options!.afterRegistrationUnsuccessful?.call() ?? + await options.afterRegistrationUnsuccessful?.call() ?? // ignore: use_build_context_synchronously Navigator.of(context).pop(); }, From 3d08717308fdef05fc5a3f6d9b95c83f333600a3 Mon Sep 17 00:00:00 2001 From: Freek van de Ven Date: Thu, 12 Jun 2025 10:50:49 +0200 Subject: [PATCH 02/16] feat: change afterLoginScreen to a nullable widget to make screen pushes optional --- CHANGELOG.md | 4 ++++ .../src/flutter_user_navigator_userstory.dart | 23 ++++++++++++------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a9cbe6..6dac0b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## Unreleased + +- Changed afterLoginScreen to a nullable Widget so a screen isn't automatically pushed after login. + ## 6.4.0 - Added proper use of exceptions for all auth methods. diff --git a/packages/flutter_user/lib/src/flutter_user_navigator_userstory.dart b/packages/flutter_user/lib/src/flutter_user_navigator_userstory.dart index 206f67f..b2a094f 100644 --- a/packages/flutter_user/lib/src/flutter_user_navigator_userstory.dart +++ b/packages/flutter_user/lib/src/flutter_user_navigator_userstory.dart @@ -6,7 +6,7 @@ import "package:flutter_user/src/models/auth_error_details.dart"; class FlutterUserNavigatorUserstory extends StatefulWidget { const FlutterUserNavigatorUserstory({ - required this.afterLoginScreen, + this.afterLoginScreen, this.afterRegistration, this.userService, this.options, @@ -18,7 +18,12 @@ class FlutterUserNavigatorUserstory extends StatefulWidget { final FlutterUserOptions? options; final UserService? userService; - final Widget afterLoginScreen; + + /// Provide a widget to push after login is successful. + /// If not provided, nothing will happen and you will need to handle the + /// navigation yourself through the [afterLogin] callback in the + /// [FlutterUserOptions]. + final Widget? afterLoginScreen; /// A callback function executed after successful registration. final VoidCallback? afterRegistration; @@ -123,13 +128,13 @@ class _FlutterUserNavigatorUserstoryState await options.onOnboardingComplete?.call(results); if (!mounted || !context.mounted) return; Navigator.of(context).pop(); - await pushReplacement(widget.afterLoginScreen); + await pushReplacementIfNotNull(widget.afterLoginScreen); }, ), ); } else { if (!context.mounted) return; - await pushReplacement(widget.afterLoginScreen); + await pushReplacementIfNotNull(widget.afterLoginScreen); } } @@ -176,7 +181,7 @@ class _FlutterUserNavigatorUserstoryState if (!mounted) return; Navigator.of(context).pop(); if (response.requestSuccesfull) { - await pushReplacement(_forgotPasswordSuccessScreen()); + await pushReplacementIfNotNull(_forgotPasswordSuccessScreen()); } else { await push(_forgotPasswordUnsuccessfullScreen()); } @@ -238,7 +243,7 @@ class _FlutterUserNavigatorUserstoryState }, afterRegistration: () async { options.afterRegistration?.call() ?? - await pushReplacement(_registrationSuccessScreen()); + await pushReplacementIfNotNull(_registrationSuccessScreen()); }, ); @@ -271,8 +276,10 @@ class _FlutterUserNavigatorUserstoryState ); } - Future pushReplacement(Widget screen) async { - if (!context.mounted) return; + /// Pushes a new screen and replaces the current one if the provided screen is + /// not null. + Future pushReplacementIfNotNull(Widget? screen) async { + if (!context.mounted || screen == null) return; await Navigator.of(context).pushReplacement( MaterialPageRoute( builder: (context) => screen, From d2a35db3b777ca4d20ff94cd2b43108ee81a6186 Mon Sep 17 00:00:00 2001 From: Freek van de Ven Date: Thu, 12 Jun 2025 11:25:15 +0200 Subject: [PATCH 03/16] fix: move the RegistrationOptions, ForgotPasswordOptions to the FlutterUserOptions There was no clear structure and a lot of inconsistency so everything is now accessed through the FlutterUserOptions --- CHANGELOG.md | 2 +- .../src/flutter_user_navigator_userstory.dart | 66 ++++++++----------- .../lib/src/models/flutter_user_options.dart | 8 +-- 3 files changed, 30 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dac0b6..4a07c35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ## Unreleased - - Changed afterLoginScreen to a nullable Widget so a screen isn't automatically pushed after login. +- Moved the RegistrationOptions and ForgotPasswordOptions to the FlutterUserOptions. ## 6.4.0 diff --git a/packages/flutter_user/lib/src/flutter_user_navigator_userstory.dart b/packages/flutter_user/lib/src/flutter_user_navigator_userstory.dart index b2a094f..2c73a94 100644 --- a/packages/flutter_user/lib/src/flutter_user_navigator_userstory.dart +++ b/packages/flutter_user/lib/src/flutter_user_navigator_userstory.dart @@ -10,13 +10,15 @@ class FlutterUserNavigatorUserstory extends StatefulWidget { this.afterRegistration, this.userService, this.options, - this.forgotPasswordTranslations, - this.registrationOptions, - this.forgotPasswordOptions = const ForgotPasswordOptions(), super.key, }); + /// The options for the user story. + /// This includes the login options, registration options and forgot password + /// options. final FlutterUserOptions? options; + + /// The user service to use for authentication and registration. final UserService? userService; /// Provide a widget to push after login is successful. @@ -27,9 +29,6 @@ class FlutterUserNavigatorUserstory extends StatefulWidget { /// A callback function executed after successful registration. final VoidCallback? afterRegistration; - final ForgotPasswordTranslations? forgotPasswordTranslations; - final RegistrationOptions? registrationOptions; - final ForgotPasswordOptions forgotPasswordOptions; @override State createState() => @@ -38,18 +37,13 @@ class FlutterUserNavigatorUserstory extends StatefulWidget { class _FlutterUserNavigatorUserstoryState extends State { - late final UserService userService; - late final FlutterUserOptions options; - late final ForgotPasswordTranslations forgotPasswordTranslations; - late final RegistrationOptions registrationOptions; + late UserService userService; + late FlutterUserOptions options; @override void initState() { userService = widget.userService ?? UserService(); options = widget.options ?? FlutterUserOptions(); - forgotPasswordTranslations = - widget.forgotPasswordTranslations ?? const ForgotPasswordTranslations(); - registrationOptions = widget.registrationOptions ?? RegistrationOptions(); super.initState(); } @@ -68,21 +62,6 @@ class _FlutterUserNavigatorUserstoryState userService = widget.userService ?? UserService(); }); } - - if (widget.forgotPasswordTranslations != - oldWidget.forgotPasswordTranslations) { - setState(() { - forgotPasswordTranslations = widget.forgotPasswordTranslations ?? - const ForgotPasswordTranslations(); - }); - } - - if (widget.registrationOptions != oldWidget.registrationOptions) { - setState(() { - registrationOptions = - widget.registrationOptions ?? RegistrationOptions(); - }); - } } @override @@ -90,12 +69,15 @@ class _FlutterUserNavigatorUserstoryState Widget _loginScreen() { var theme = Theme.of(context); + var textTheme = theme.textTheme; + var loginOptions = options.loginOptions; + var loginTranslations = loginOptions.translations; var title = Text( - options.loginTranslations.loginTitle, - style: theme.textTheme.headlineLarge, + loginTranslations.loginTitle, + style: textTheme.headlineLarge, ); - var subtitle = Text(options.loginTranslations.loginSubtitle ?? ""); + var subtitle = Text(loginTranslations.loginSubtitle ?? ""); FutureOr onLogin(String email, String password) async { await options.beforeLogin?.call(email, password); @@ -156,15 +138,19 @@ class _FlutterUserNavigatorUserstoryState Widget _forgotPasswordScreen() { var theme = Theme.of(context); + var textTheme = theme.textTheme; + var forgotOptions = options.forgotPasswordOptions; + var forgotTranslations = forgotOptions.translations; + var title = Text( - options.forgotPasswordTranslations.forgotPasswordTitle, - style: theme.textTheme.headlineLarge, + forgotTranslations.forgotPasswordTitle, + style: textTheme.headlineLarge, ); var description = Padding( padding: const EdgeInsets.only(top: 8, bottom: 32), child: Text( - options.forgotPasswordTranslations.forgotPasswordDescription, + forgotTranslations.forgotPasswordDescription, textAlign: TextAlign.center, ), ); @@ -197,13 +183,13 @@ class _FlutterUserNavigatorUserstoryState title: title, description: description, loginOptions: options.loginOptions, - forgotPasswordOptions: widget.forgotPasswordOptions, + forgotPasswordOptions: options.forgotPasswordOptions, onRequestForgotPassword: onRequestForgotPassword, ); } Widget _forgotPasswordSuccessScreen() => ForgotPasswordSuccess( - translations: options.forgotPasswordTranslations, + translations: options.forgotPasswordOptions.translations, onRequestForgotPassword: () async { await options.onForgotPasswordSuccess?.call() ?? // ignore: use_build_context_synchronously @@ -212,7 +198,7 @@ class _FlutterUserNavigatorUserstoryState ); Widget _forgotPasswordUnsuccessfullScreen() => ForgotPasswordUnsuccessfull( - translations: forgotPasswordTranslations, + translations: options.forgotPasswordOptions.translations, onPressed: () async { await options.onForgotPasswordUnsuccessful?.call() ?? // ignore: use_build_context_synchronously @@ -221,7 +207,7 @@ class _FlutterUserNavigatorUserstoryState ); Widget _registrationScreen() => RegistrationScreen( - registrationOptions: registrationOptions, + registrationOptions: options.registrationOptions, userService: userService, onError: (error) async { var errorDetails = options.authExceptionFormatter.format(error); @@ -248,7 +234,7 @@ class _FlutterUserNavigatorUserstoryState ); Widget _registrationSuccessScreen() => RegistrationSuccess( - registrationOptions: registrationOptions, + registrationOptions: options.registrationOptions, onPressed: () async { await options.afterRegistrationSuccess?.call() ?? // ignore: use_build_context_synchronously @@ -258,7 +244,7 @@ class _FlutterUserNavigatorUserstoryState Widget _registrationUnsuccessfullScreen(AuthErrorDetails errorDetails) => RegistrationUnsuccessfull( - registrationOptions: registrationOptions, + registrationOptions: options.registrationOptions, onPressed: () async { await options.afterRegistrationUnsuccessful?.call() ?? // ignore: use_build_context_synchronously diff --git a/packages/flutter_user/lib/src/models/flutter_user_options.dart b/packages/flutter_user/lib/src/models/flutter_user_options.dart index fb3b05e..4823a7c 100644 --- a/packages/flutter_user/lib/src/models/flutter_user_options.dart +++ b/packages/flutter_user/lib/src/models/flutter_user_options.dart @@ -5,8 +5,7 @@ import "package:flutter_user/src/models/auth_error_details.dart"; class FlutterUserOptions { FlutterUserOptions({ this.loginOptions = const LoginOptions(), - this.loginTranslations = const LoginTranslations(), - this.forgotPasswordTranslations = const ForgotPasswordTranslations(), + this.forgotPasswordOptions = const ForgotPasswordOptions(), this.authExceptionFormatter = const AuthExceptionFormatter(), this.beforeLogin, this.afterLogin, @@ -26,9 +25,8 @@ class FlutterUserOptions { }) : registrationOptions = registrationOptions ?? RegistrationOptions(); final LoginOptions loginOptions; - final LoginTranslations loginTranslations; - final RegistrationOptions? registrationOptions; - final ForgotPasswordTranslations forgotPasswordTranslations; + final RegistrationOptions registrationOptions; + final ForgotPasswordOptions forgotPasswordOptions; final AuthExceptionFormatter authExceptionFormatter; final Future Function(String email, String password)? beforeLogin; final Future Function()? afterLogin; From 020a686600cae5422a234b081331431694082fd6 Mon Sep 17 00:00:00 2001 From: Freek van de Ven Date: Thu, 12 Jun 2025 11:26:25 +0200 Subject: [PATCH 04/16] chore: improve example app with more configuration for testing --- packages/flutter_user/example/lib/main.dart | 32 ++++++++++++++------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/packages/flutter_user/example/lib/main.dart b/packages/flutter_user/example/lib/main.dart index 6191e5c..e4e10c7 100644 --- a/packages/flutter_user/example/lib/main.dart +++ b/packages/flutter_user/example/lib/main.dart @@ -13,15 +13,29 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) => MaterialApp( title: "flutter_user Example", theme: theme, - home: FlutterUserNavigatorUserstory( - afterLoginScreen: const Home(), - options: FlutterUserOptions( - loginOptions: const LoginOptions( - biometricsOptions: LoginBiometricsOptions( - loginWithBiometrics: true, - ), + home: const UserstoryScreen(), + ); +} + +class UserstoryScreen extends StatelessWidget { + const UserstoryScreen({super.key}); + + @override + Widget build(BuildContext context) => FlutterUserNavigatorUserstory( + afterLoginScreen: const Home(), + options: FlutterUserOptions( + loginOptions: const LoginOptions( + biometricsOptions: LoginBiometricsOptions( + loginWithBiometrics: true, + ), + translations: LoginTranslations( + loginTitle: "Login", + loginButton: "Log in", + loginSubtitle: "Welcome back!", ), ), + forgotPasswordOptions: const ForgotPasswordOptions(), + registrationOptions: RegistrationOptions(), ), ); } @@ -39,9 +53,7 @@ class Home extends StatelessWidget { onPressed: () async => Navigator.pushReplacement( context, MaterialPageRoute( - builder: (context) => const FlutterUserNavigatorUserstory( - afterLoginScreen: Home(), - ), + builder: (context) => const UserstoryScreen(), ), ), child: const Text("Logout"), From 69baaa8492e0b80d63be8e9c490826c90c583211 Mon Sep 17 00:00:00 2001 From: Freek van de Ven Date: Thu, 12 Jun 2025 13:28:34 +0200 Subject: [PATCH 05/16] chore: add dependabot configuration --- .github/dependabot.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..cae4f64 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +version: 2 + +updates: + - package-ecosystem: "pub" + directory: "packages/firebase_user_repository" + schedule: + interval: "weekly" + + - package-ecosystem: "pub" + directory: "packages/flutter_user" + schedule: + interval: "weekly" + + - package-ecosystem: "pub" + directory: "packages/user_repository_interface" + schedule: + interval: "weekly" From e40605dd0f30d06fa653ce9616ea6ee59ee759bb Mon Sep 17 00:00:00 2001 From: Freek van de Ven Date: Thu, 12 Jun 2025 13:34:49 +0200 Subject: [PATCH 06/16] feat: add rest_user_repository package which implements the UserRepositoryInterface in a restful way using the dart_api_service --- .github/dependabot.yml | 5 + CHANGELOG.md | 2 + packages/rest_user_repository/.gitignore | 29 +++ packages/rest_user_repository/CHANGELOG.md | 1 + packages/rest_user_repository/CONTRIBUTING.md | 1 + packages/rest_user_repository/LICENSE | 1 + packages/rest_user_repository/README.md | 1 + .../analysis_options.yaml | 9 + .../lib/rest_user_repository.dart | 178 ++++++++++++++++++ packages/rest_user_repository/pubspec.yaml | 22 +++ .../user_repository_interface/pubspec.yaml | 5 - 11 files changed, 249 insertions(+), 5 deletions(-) create mode 100644 packages/rest_user_repository/.gitignore create mode 120000 packages/rest_user_repository/CHANGELOG.md create mode 120000 packages/rest_user_repository/CONTRIBUTING.md create mode 120000 packages/rest_user_repository/LICENSE create mode 120000 packages/rest_user_repository/README.md create mode 100644 packages/rest_user_repository/analysis_options.yaml create mode 100644 packages/rest_user_repository/lib/rest_user_repository.dart create mode 100644 packages/rest_user_repository/pubspec.yaml diff --git a/.github/dependabot.yml b/.github/dependabot.yml index cae4f64..24b7c2d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,6 +11,11 @@ updates: schedule: interval: "weekly" + - package-ecosystem: "pub" + directory: "packages/rest_user_repository" + schedule: + interval: "weekly" + - package-ecosystem: "pub" directory: "packages/user_repository_interface" schedule: diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a07c35..f2ec527 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,6 @@ ## Unreleased + +- Added rest_user_repository package which is a REST implementation of the UserRepositoryInterface. - Changed afterLoginScreen to a nullable Widget so a screen isn't automatically pushed after login. - Moved the RegistrationOptions and ForgotPasswordOptions to the FlutterUserOptions. diff --git a/packages/rest_user_repository/.gitignore b/packages/rest_user_repository/.gitignore new file mode 100644 index 0000000..ac5aa98 --- /dev/null +++ b/packages/rest_user_repository/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +build/ diff --git a/packages/rest_user_repository/CHANGELOG.md b/packages/rest_user_repository/CHANGELOG.md new file mode 120000 index 0000000..699cc9e --- /dev/null +++ b/packages/rest_user_repository/CHANGELOG.md @@ -0,0 +1 @@ +../../CHANGELOG.md \ No newline at end of file diff --git a/packages/rest_user_repository/CONTRIBUTING.md b/packages/rest_user_repository/CONTRIBUTING.md new file mode 120000 index 0000000..f939e75 --- /dev/null +++ b/packages/rest_user_repository/CONTRIBUTING.md @@ -0,0 +1 @@ +../../CONTRIBUTING.md \ No newline at end of file diff --git a/packages/rest_user_repository/LICENSE b/packages/rest_user_repository/LICENSE new file mode 120000 index 0000000..30cff74 --- /dev/null +++ b/packages/rest_user_repository/LICENSE @@ -0,0 +1 @@ +../../LICENSE \ No newline at end of file diff --git a/packages/rest_user_repository/README.md b/packages/rest_user_repository/README.md new file mode 120000 index 0000000..fe84005 --- /dev/null +++ b/packages/rest_user_repository/README.md @@ -0,0 +1 @@ +../../README.md \ No newline at end of file diff --git a/packages/rest_user_repository/analysis_options.yaml b/packages/rest_user_repository/analysis_options.yaml new file mode 100644 index 0000000..0736605 --- /dev/null +++ b/packages/rest_user_repository/analysis_options.yaml @@ -0,0 +1,9 @@ +include: package:flutter_iconica_analysis/components_options.yaml + +# Possible to overwrite the rules from the package + +analyzer: + exclude: + +linter: + rules: diff --git a/packages/rest_user_repository/lib/rest_user_repository.dart b/packages/rest_user_repository/lib/rest_user_repository.dart new file mode 100644 index 0000000..bbc2ca8 --- /dev/null +++ b/packages/rest_user_repository/lib/rest_user_repository.dart @@ -0,0 +1,178 @@ +import "dart:async"; +import "dart:convert"; + +import "package:dart_api_service/dart_api_service.dart"; +import "package:user_repository_interface/user_repository_interface.dart"; + +class _TokenAuthService extends AuthenticationService { + String? _token; + + set token(String? newToken) => _token = newToken; + void clearToken() => _token = null; + bool get hasToken => _token != null; + + @override + FutureOr getCredentials() { + if (!hasToken) { + throw const RequiresRecentLoginError( + message: "User is not authenticated.", + ); + } + return TokenAuthCredentials(token: _token!); + } +} + +/// An implementation of [UserRepositoryInterface] that uses a REST API. +class RestUserRepository implements UserRepositoryInterface { + /// Creates an instance of the REST user repository. + /// + /// Requires the [baseUrl] for the API endpoints. + RestUserRepository({required String baseUrl}) { + _authService = _TokenAuthService(); + _apiService = HttpApiService( + baseUrl: Uri.parse(baseUrl), + authenticationService: _authService, + defaultHeaders: { + "Content-Type": "application/json", + "Accept": "application/json", + }, + ); + } + + late final HttpApiService _apiService; + late final _TokenAuthService _authService; + + /// Logs in a user with the provided [email] and [password]. + /// + /// On a successful login, it returns an [AuthResponse] and stores the + /// authentication token. Throws an [AuthException] on failure. + @override + Future loginWithEmailAndPassword({ + required String email, + required String password, + }) async { + try { + var converter = + ModelJsonResponseConverter>( + deserialize: (json) => AuthResponse(userObject: json["user"]), + serialize: (body) => body, + ); + var endpoint = + _apiService.endpoint("/auth/login").withConverter(converter); + + var response = await endpoint.post( + requestModel: {"email": email, "password": password}, + ); + + var userMap = response.result?.userObject as Map?; + _authService.token = userMap?["token"] as String?; + + return response.result!; + } on ApiException catch (e) { + throw _handleAuthError(e); + } + } + + /// Registers a new user with the given [values]. + /// + /// On a successful registration, it returns an [AuthResponse] and stores + /// the authentication token. Throws an [AuthException] on failure. + @override + Future register({ + required Map values, + }) async { + try { + var converter = + ModelJsonResponseConverter>( + deserialize: (json) => AuthResponse(userObject: json["user"]), + serialize: (body) => body, + ); + var endpoint = + _apiService.endpoint("/auth/register").withConverter(converter); + + var response = await endpoint.post(requestModel: values); + + var userMap = response.result?.userObject as Map?; + _authService.token = userMap?["token"] as String?; + + return response.result!; + } on ApiException catch (e) { + throw _handleAuthError(e); + } + } + + /// Requests a password change for the user associated with the [email]. + /// + /// Returns a [RequestPasswordResponse] indicating if the request was + /// successful. + @override + Future requestChangePassword({ + required String email, + }) async { + try { + var converter = ModelJsonResponseConverter>( + deserialize: (json) => RequestPasswordResponse( + requestSuccesfull: json["success"] ?? false, + ), + serialize: (body) => body, + ); + var endpoint = _apiService + .endpoint("/auth/request-password-change") + .withConverter(converter); + + var response = await endpoint.post(requestModel: {"email": email}); + + return response.result!; + } on ApiException catch (e) { + throw _handleAuthError(e); + } + } + + /// Fetches the profile of the currently logged-in user. + /// + /// Throws a [RequiresRecentLoginError] if the user is not authenticated. + @override + Future getLoggedInUser() async { + try { + var endpoint = _apiService.endpoint("/users/me").authenticate(); + var response = await endpoint.get(); + return jsonDecode(response.inner.body); + } on ApiException catch (e) { + throw _handleAuthError(e); + } + } + + /// Checks if a user is currently logged in. + @override + Future isLoggedIn() async => _authService.hasToken; + + /// Logs the current user out by clearing the stored authentication token. + @override + Future logout() async { + _authService.clearToken(); + return true; + } + + AuthException _handleAuthError(ApiException e) { + var message = _getErrorMessage(e.inner.body); + return switch (e.statusCode) { + 400 => InvalidCredentialError(message: message ?? "Invalid request."), + 401 => WrongPasswordError(message: message ?? "Incorrect credentials."), + 403 => UserDisabledError(message: message ?? "User account is disabled."), + 404 => UserNotFoundError(message: message ?? "User not found."), + 409 => + EmailAlreadyInUseError(message: message ?? "Email is already in use."), + 429 => TooManyRequestsError(message: message ?? "Too many requests."), + _ => GenericAuthError(message: message ?? "An unknown error occurred."), + }; + } + + String? _getErrorMessage(String body) { + try { + return (jsonDecode(body) as Map)["message"] as String?; + } on Exception { + return null; + } + } +} diff --git a/packages/rest_user_repository/pubspec.yaml b/packages/rest_user_repository/pubspec.yaml new file mode 100644 index 0000000..5e60c37 --- /dev/null +++ b/packages/rest_user_repository/pubspec.yaml @@ -0,0 +1,22 @@ +name: rest_user_repository +description: "RESTful implementation of the user_repository_interface for flutter_user package" +version: 6.4.0 +repository: https://github.com/Iconica-Development/flutter_user + +publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub + +environment: + sdk: ^3.5.1 + +dependencies: + dart_api_service: + hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub/ + version: ^1.1.1 + user_repository_interface: + hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub/ + version: ^6.4.0 + +dev_dependencies: + flutter_iconica_analysis: + hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub/ + version: ^7.0.0 diff --git a/packages/user_repository_interface/pubspec.yaml b/packages/user_repository_interface/pubspec.yaml index 14b9f77..0fdd67c 100644 --- a/packages/user_repository_interface/pubspec.yaml +++ b/packages/user_repository_interface/pubspec.yaml @@ -7,11 +7,6 @@ publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub environment: sdk: ^3.5.1 - flutter: ">=1.17.0" - -dependencies: - flutter: - sdk: flutter dev_dependencies: flutter_iconica_analysis: From 02f2ad26dd04b86a1c2aa27a9dc8e5be1afd215b Mon Sep 17 00:00:00 2001 From: Freek van de Ven Date: Wed, 18 Jun 2025 10:11:09 +0200 Subject: [PATCH 07/16] fix: remove default color values and refactor the code for the UI --- CHANGELOG.md | 2 +- .../forgot_password_options.dart | 2 +- .../lib/src/models/login/login_options.dart | 14 +--- .../registration/registration_options.dart | 10 ++- .../registration_translations.dart | 13 +--- .../screens/email_password_login_form.dart | 37 ++++++----- .../lib/src/screens/forgot_password_form.dart | 66 +++++++------------ .../lib/src/screens/registration_screen.dart | 5 ++ .../lib/src/widgets/optional_spacer.dart | 17 +++++ 9 files changed, 72 insertions(+), 94 deletions(-) create mode 100644 packages/flutter_user/lib/src/widgets/optional_spacer.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index f2ec527..5652016 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ ## Unreleased - +- Removed the default values for colors in the LoginOptions, RegistrationOptions, and ForgotPasswordOptions. - Added rest_user_repository package which is a REST implementation of the UserRepositoryInterface. - Changed afterLoginScreen to a nullable Widget so a screen isn't automatically pushed after login. - Moved the RegistrationOptions and ForgotPasswordOptions to the FlutterUserOptions. diff --git a/packages/flutter_user/lib/src/models/forgot_password/forgot_password_options.dart b/packages/flutter_user/lib/src/models/forgot_password/forgot_password_options.dart index 829b1af..f92c17a 100644 --- a/packages/flutter_user/lib/src/models/forgot_password/forgot_password_options.dart +++ b/packages/flutter_user/lib/src/models/forgot_password/forgot_password_options.dart @@ -9,7 +9,7 @@ import "package:flutter_user/src/widgets/primary_button.dart"; class ForgotPasswordOptions { const ForgotPasswordOptions({ this.forgotPasswordCustomAppBar, - this.forgotPasswordBackgroundColor = const Color(0xffFAF9F6), + this.forgotPasswordBackgroundColor, this.forgotPasswordScreenPadding = const Padding( padding: EdgeInsets.symmetric(horizontal: 60), ), diff --git a/packages/flutter_user/lib/src/models/login/login_options.dart b/packages/flutter_user/lib/src/models/login/login_options.dart index d1010d8..1227627 100644 --- a/packages/flutter_user/lib/src/models/login/login_options.dart +++ b/packages/flutter_user/lib/src/models/login/login_options.dart @@ -26,7 +26,7 @@ class LoginOptions extends Equatable { this.showObscurePassword = true, this.suffixIconSize, this.suffixIconPadding, - this.loginBackgroundColor = const Color(0xffFAF9F6), + this.loginBackgroundColor, this.forgotPasswordButtonBuilder = _createForgotPasswordButton, this.loginButtonBuilder = _createLoginButton, this.registrationButtonBuilder = _createRegisterButton, @@ -36,11 +36,6 @@ class LoginOptions extends Equatable { contentPadding: EdgeInsets.symmetric(horizontal: 8), labelText: "Email address", border: OutlineInputBorder(), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Color(0xff71C6D1), - ), - ), labelStyle: TextStyle( color: Colors.black, fontWeight: FontWeight.w400, @@ -51,11 +46,6 @@ class LoginOptions extends Equatable { contentPadding: EdgeInsets.symmetric(horizontal: 8), labelText: "Password", border: OutlineInputBorder(), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Color(0xff71C6D1), - ), - ), labelStyle: TextStyle( color: Colors.black, fontWeight: FontWeight.w400, @@ -83,7 +73,7 @@ class LoginOptions extends Equatable { final LoginButtonBuilder forgotPasswordButtonBuilder; final LoginButtonBuilder loginButtonBuilder; final LoginButtonBuilder registrationButtonBuilder; - final Color loginBackgroundColor; + final Color? loginBackgroundColor; final InputContainerBuilder emailInputContainerBuilder; final InputContainerBuilder passwordInputContainerBuilder; diff --git a/packages/flutter_user/lib/src/models/registration/registration_options.dart b/packages/flutter_user/lib/src/models/registration/registration_options.dart index ca162bd..672a1dd 100644 --- a/packages/flutter_user/lib/src/models/registration/registration_options.dart +++ b/packages/flutter_user/lib/src/models/registration/registration_options.dart @@ -10,9 +10,9 @@ class RegistrationOptions { RegistrationOptions({ this.translations = const RegistrationTranslations.empty(), this.accessibilityIdentifiers = const LoginAccessibilityIdentifiers.empty(), - this.registrationBackgroundColor = const Color(0xffFAF9F6), + this.registrationBackgroundColor, this.maxFormWidth = 300, - this.customAppbarBuilder = _createCustomAppBar, + this.customAppbarBuilder = _defaultAppBar, this.steps = const [], this.title, this.spacerOptions = const RegistrationSpacerOptions(), @@ -33,7 +33,7 @@ class RegistrationOptions { /// This is used for testing purposes. final LoginAccessibilityIdentifiers accessibilityIdentifiers; - final Color registrationBackgroundColor; + final Color? registrationBackgroundColor; final double maxFormWidth; final AppBar Function(String title) customAppbarBuilder; List steps; @@ -46,10 +46,8 @@ class RegistrationOptions { final Widget? loginButton; } -AppBar _createCustomAppBar(String title) => AppBar( - iconTheme: const IconThemeData(color: Colors.black, size: 16), +AppBar _defaultAppBar(String title) => AppBar( title: Text(title), - backgroundColor: Colors.transparent, ); List getDefaultSteps({ diff --git a/packages/flutter_user/lib/src/models/registration/registration_translations.dart b/packages/flutter_user/lib/src/models/registration/registration_translations.dart index d36a24e..4f0e24e 100644 --- a/packages/flutter_user/lib/src/models/registration/registration_translations.dart +++ b/packages/flutter_user/lib/src/models/registration/registration_translations.dart @@ -29,20 +29,9 @@ class RegistrationTranslations { required this.registrationUnsuccessButtonTitle, }); - // this.registrationSuccessTitle = "your registration was successful", - // this.registrationSuccessButtonTitle = "Finish", - // this.registrationUnsuccessfullTitle = "something went wrong", - // this.registrationEmailUnsuccessfullDescription = - // "This email address is already" - // " associated with an account. Please try again.", - // this.registrationPasswordUnsuccessfullDescription = - // "The password you entered" - // " is invalid. Please try again.", - // this.registrationUnsuccessButtonTitle = "Try again", - /// Constructs a [RegistrationTranslations] object with empty strings. const RegistrationTranslations.empty() - : title = "", + : title = "Registration", registerBtn = "Register", previousStepBtn = "Previous", nextStepBtn = "Next", diff --git a/packages/flutter_user/lib/src/screens/email_password_login_form.dart b/packages/flutter_user/lib/src/screens/email_password_login_form.dart index 0bdea1c..64545d1 100644 --- a/packages/flutter_user/lib/src/screens/email_password_login_form.dart +++ b/packages/flutter_user/lib/src/screens/email_password_login_form.dart @@ -5,6 +5,7 @@ import "package:flutter_accessibility/flutter_accessibility.dart"; import "package:flutter_user/src/models/login/login_options.dart"; import "package:flutter_user/src/services/local_auth.dart"; import "package:flutter_user/src/widgets/biometrics_button.dart"; +import "package:flutter_user/src/widgets/optional_spacer.dart"; class EmailPasswordLoginForm extends StatefulWidget { /// Constructs an [EmailPasswordLoginForm] widget. @@ -263,9 +264,9 @@ class _EmailPasswordLoginFormState extends State { ), ), forgotPasswordButton, - if (options.spacers.spacerAfterForm != null) ...[ - Spacer(flex: options.spacers.spacerAfterForm!), - ], + ...buildOptionalSpacer( + options.spacers.spacerAfterForm, + ), if (options .biometricsOptions.loginWithBiometrics) ...[ Row( @@ -290,9 +291,9 @@ class _EmailPasswordLoginFormState extends State { if (widget.onRegister != null) ...[ registerButton, ], - if (options.spacers.spacerAfterButton != null) ...[ - Spacer(flex: options.spacers.spacerAfterButton!), - ], + ...buildOptionalSpacer( + options.spacers.spacerAfterButton, + ), ], ), ), @@ -324,9 +325,9 @@ class _LoginTitle extends StatelessWidget { var theme = Theme.of(context); return Column( children: [ - if (options.spacers.spacerBeforeTitle != null) ...[ - Spacer(flex: options.spacers.spacerBeforeTitle!), - ], + ...buildOptionalSpacer( + options.spacers.spacerBeforeTitle, + ), if (title != null) ...[ Align( alignment: Alignment.topCenter, @@ -336,9 +337,9 @@ class _LoginTitle extends StatelessWidget { ), ), ], - if (options.spacers.spacerAfterTitle != null) ...[ - Spacer(flex: options.spacers.spacerAfterTitle!), - ], + ...buildOptionalSpacer( + options.spacers.spacerAfterTitle, + ), if (subtitle != null) ...[ Align( alignment: Alignment.topCenter, @@ -348,18 +349,18 @@ class _LoginTitle extends StatelessWidget { ), ), ], - if (options.spacers.spacerAfterSubtitle != null) ...[ - Spacer(flex: options.spacers.spacerAfterSubtitle!), - ], + ...buildOptionalSpacer( + 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!), - ], + ...buildOptionalSpacer( + options.spacers.spacerAfterImage, + ), ], ); } diff --git a/packages/flutter_user/lib/src/screens/forgot_password_form.dart b/packages/flutter_user/lib/src/screens/forgot_password_form.dart index 029f6ae..55987f0 100644 --- a/packages/flutter_user/lib/src/screens/forgot_password_form.dart +++ b/packages/flutter_user/lib/src/screens/forgot_password_form.dart @@ -5,6 +5,7 @@ import "package:flutter_accessibility/flutter_accessibility.dart"; import "package:flutter_user/src/models/forgot_password/forgot_password_options.dart"; import "package:flutter_user/src/models/login/login_options.dart"; import "package:flutter_user/src/screens/email_password_login_form.dart"; +import "package:flutter_user/src/widgets/optional_spacer.dart"; class ForgotPasswordForm extends StatefulWidget { /// Constructs a [ForgotPasswordForm] widget. @@ -77,10 +78,7 @@ class _ForgotPasswordFormState extends State { return Scaffold( backgroundColor: forgotPasswordOptions.forgotPasswordBackgroundColor, - appBar: forgotPasswordOptions.forgotPasswordCustomAppBar ?? - AppBar( - backgroundColor: const Color(0xffFAF9F6), - ), + appBar: forgotPasswordOptions.forgotPasswordCustomAppBar ?? AppBar(), body: Padding( padding: forgotPasswordOptions.forgotPasswordScreenPadding.padding, child: CustomScrollView( @@ -91,14 +89,10 @@ class _ForgotPasswordFormState extends State { fillOverscroll: true, child: Column( children: [ - if (forgotPasswordOptions - .forgotPasswordSpacerOptions.spacerBeforeTitle != - null) ...[ - Spacer( - flex: forgotPasswordOptions - .forgotPasswordSpacerOptions.spacerBeforeTitle!, - ), - ], + ...buildOptionalSpacer( + forgotPasswordOptions + .forgotPasswordSpacerOptions.spacerBeforeTitle, + ), Align( alignment: Alignment.topCenter, child: wrapWithDefaultStyle( @@ -106,14 +100,10 @@ class _ForgotPasswordFormState extends State { theme.textTheme.displaySmall, ), ), - if (forgotPasswordOptions - .forgotPasswordSpacerOptions.spacerAfterTitle != - null) ...[ - Spacer( - flex: forgotPasswordOptions - .forgotPasswordSpacerOptions.spacerAfterTitle!, - ), - ], + ...buildOptionalSpacer( + forgotPasswordOptions + .forgotPasswordSpacerOptions.spacerAfterTitle, + ), Align( alignment: Alignment.topCenter, child: wrapWithDefaultStyle( @@ -123,14 +113,10 @@ class _ForgotPasswordFormState extends State { ), ), ), - if (forgotPasswordOptions - .forgotPasswordSpacerOptions.spacerAfterDescription != - null) ...[ - Spacer( - flex: forgotPasswordOptions - .forgotPasswordSpacerOptions.spacerAfterDescription!, - ), - ], + ...buildOptionalSpacer( + forgotPasswordOptions + .forgotPasswordSpacerOptions.spacerAfterDescription, + ), Expanded( flex: forgotPasswordOptions .forgotPasswordSpacerOptions.formFlexValue, @@ -168,14 +154,10 @@ class _ForgotPasswordFormState extends State { ), ), ), - if (forgotPasswordOptions - .forgotPasswordSpacerOptions.spacerBeforeButton != - null) ...[ - Spacer( - flex: forgotPasswordOptions - .forgotPasswordSpacerOptions.spacerBeforeButton!, - ), - ], + ...buildOptionalSpacer( + forgotPasswordOptions + .forgotPasswordSpacerOptions.spacerBeforeButton, + ), AnimatedBuilder( animation: _formValid, builder: (context, snapshot) => Align( @@ -201,14 +183,10 @@ class _ForgotPasswordFormState extends State { ), ), ), - if (forgotPasswordOptions - .forgotPasswordSpacerOptions.spacerAfterButton != - null) ...[ - Spacer( - flex: forgotPasswordOptions - .forgotPasswordSpacerOptions.spacerAfterButton!, - ), - ], + ...buildOptionalSpacer( + forgotPasswordOptions + .forgotPasswordSpacerOptions.spacerAfterButton, + ), ], ), ), diff --git a/packages/flutter_user/lib/src/screens/registration_screen.dart b/packages/flutter_user/lib/src/screens/registration_screen.dart index 6fe85cf..6b77888 100644 --- a/packages/flutter_user/lib/src/screens/registration_screen.dart +++ b/packages/flutter_user/lib/src/screens/registration_screen.dart @@ -116,6 +116,7 @@ class _RegistrationScreenState extends State { Widget build(BuildContext context) { var theme = Theme.of(context); var registrationOptions = widget.registrationOptions; + return Scaffold( backgroundColor: registrationOptions.registrationBackgroundColor, appBar: registrationOptions.customAppbarBuilder.call( @@ -213,6 +214,7 @@ class _RegistrationScreenState extends State { }, ), ), + const SizedBox(width: 16), registrationOptions.nextButtonBuilder?.call( onPrevious, currentStep == @@ -247,6 +249,9 @@ class _RegistrationScreenState extends State { ), if (registrationOptions.loginButton != null) ...[ registrationOptions.loginButton!, + const SizedBox( + height: 8, + ), ], ], ), diff --git a/packages/flutter_user/lib/src/widgets/optional_spacer.dart b/packages/flutter_user/lib/src/widgets/optional_spacer.dart new file mode 100644 index 0000000..a8ffb89 --- /dev/null +++ b/packages/flutter_user/lib/src/widgets/optional_spacer.dart @@ -0,0 +1,17 @@ +import "package:flutter/material.dart"; + +/// Helper function to conditionally add a Spacer. +/// +/// If [flex] is not null, a [Spacer] with the given flex value is returned. +/// Otherwise, an empty list is returned, effectively adding nothing to the +/// widget tree. +List buildOptionalSpacer(int? flex) { + if (flex != null) { + return [ + Spacer( + flex: flex, + ), + ]; + } + return []; +} From 84ea72ba5a541e2c05bdde28541e2608ad09e33c Mon Sep 17 00:00:00 2001 From: Freek van de Ven Date: Wed, 18 Jun 2025 10:15:45 +0200 Subject: [PATCH 08/16] chore: move auth files to auth folder in registration --- packages/flutter_user/lib/flutter_user.dart | 14 +++++++------- .../registration/{ => auth}/auth_action.dart | 2 +- .../registration/{ => auth}/auth_bool_field.dart | 2 +- .../registration/{ => auth}/auth_drop_down.dart | 2 +- .../models/registration/{ => auth}/auth_field.dart | 2 +- .../registration/{ => auth}/auth_pass_field.dart | 2 +- .../models/registration/{ => auth}/auth_step.dart | 6 +++--- .../registration/{ => auth}/auth_text_field.dart | 2 +- .../models/registration/registration_options.dart | 6 +++--- 9 files changed, 19 insertions(+), 19 deletions(-) rename packages/flutter_user/lib/src/models/registration/{ => auth}/auth_action.dart (95%) rename packages/flutter_user/lib/src/models/registration/{ => auth}/auth_bool_field.dart (96%) rename packages/flutter_user/lib/src/models/registration/{ => auth}/auth_drop_down.dart (96%) rename packages/flutter_user/lib/src/models/registration/{ => auth}/auth_field.dart (96%) rename packages/flutter_user/lib/src/models/registration/{ => auth}/auth_pass_field.dart (97%) rename packages/flutter_user/lib/src/models/registration/{ => auth}/auth_step.dart (66%) rename packages/flutter_user/lib/src/models/registration/{ => auth}/auth_text_field.dart (97%) diff --git a/packages/flutter_user/lib/flutter_user.dart b/packages/flutter_user/lib/flutter_user.dart index 5fae4f6..4e38732 100644 --- a/packages/flutter_user/lib/flutter_user.dart +++ b/packages/flutter_user/lib/flutter_user.dart @@ -16,13 +16,13 @@ export "src/models/image_picker_theme.dart"; export "src/models/login/login_options.dart"; export "src/models/login/login_spacer_options.dart"; export "src/models/login/login_translations.dart"; -export "src/models/registration/auth_action.dart"; -export "src/models/registration/auth_bool_field.dart"; -export "src/models/registration/auth_drop_down.dart"; -export "src/models/registration/auth_field.dart"; -export "src/models/registration/auth_pass_field.dart"; -export "src/models/registration/auth_step.dart"; -export "src/models/registration/auth_text_field.dart"; +export "src/models/registration/auth/auth_action.dart"; +export "src/models/registration/auth/auth_bool_field.dart"; +export "src/models/registration/auth/auth_drop_down.dart"; +export "src/models/registration/auth/auth_field.dart"; +export "src/models/registration/auth/auth_pass_field.dart"; +export "src/models/registration/auth/auth_step.dart"; +export "src/models/registration/auth/auth_text_field.dart"; export "src/models/registration/registration_options.dart"; export "src/models/registration/registration_spacer_options.dart"; export "src/models/registration/registration_translations.dart"; diff --git a/packages/flutter_user/lib/src/models/registration/auth_action.dart b/packages/flutter_user/lib/src/models/registration/auth/auth_action.dart similarity index 95% rename from packages/flutter_user/lib/src/models/registration/auth_action.dart rename to packages/flutter_user/lib/src/models/registration/auth/auth_action.dart index b00a46c..7ceb029 100644 --- a/packages/flutter_user/lib/src/models/registration/auth_action.dart +++ b/packages/flutter_user/lib/src/models/registration/auth/auth_action.dart @@ -7,7 +7,7 @@ import "package:flutter/material.dart"; /// An action that can be performed during authentication. class AuthAction { /// Constructs an [AuthAction] object. - AuthAction({ + const AuthAction({ required this.title, required this.onPress, }); diff --git a/packages/flutter_user/lib/src/models/registration/auth_bool_field.dart b/packages/flutter_user/lib/src/models/registration/auth/auth_bool_field.dart similarity index 96% rename from packages/flutter_user/lib/src/models/registration/auth_bool_field.dart rename to packages/flutter_user/lib/src/models/registration/auth/auth_bool_field.dart index bf90373..e28e791 100644 --- a/packages/flutter_user/lib/src/models/registration/auth_bool_field.dart +++ b/packages/flutter_user/lib/src/models/registration/auth/auth_bool_field.dart @@ -4,7 +4,7 @@ import "package:flutter/material.dart"; import "package:flutter_input_library/flutter_input_library.dart"; -import "package:flutter_user/src/models/registration/auth_field.dart"; +import "package:flutter_user/src/models/registration/auth/auth_field.dart"; /// A field for capturing boolean values in a Flutter form. /// diff --git a/packages/flutter_user/lib/src/models/registration/auth_drop_down.dart b/packages/flutter_user/lib/src/models/registration/auth/auth_drop_down.dart similarity index 96% rename from packages/flutter_user/lib/src/models/registration/auth_drop_down.dart rename to packages/flutter_user/lib/src/models/registration/auth/auth_drop_down.dart index a15fbc9..2b47618 100644 --- a/packages/flutter_user/lib/src/models/registration/auth_drop_down.dart +++ b/packages/flutter_user/lib/src/models/registration/auth/auth_drop_down.dart @@ -1,5 +1,5 @@ import "package:flutter/material.dart"; -import "package:flutter_user/src/models/registration/auth_field.dart"; +import "package:flutter_user/src/models/registration/auth/auth_field.dart"; /// A field for capturing dropdown selections in a Flutter form. /// diff --git a/packages/flutter_user/lib/src/models/registration/auth_field.dart b/packages/flutter_user/lib/src/models/registration/auth/auth_field.dart similarity index 96% rename from packages/flutter_user/lib/src/models/registration/auth_field.dart rename to packages/flutter_user/lib/src/models/registration/auth/auth_field.dart index 9b0bc4f..3b87fd8 100644 --- a/packages/flutter_user/lib/src/models/registration/auth_field.dart +++ b/packages/flutter_user/lib/src/models/registration/auth/auth_field.dart @@ -42,7 +42,7 @@ abstract class AuthField { final Widget? title; /// A list of validation functions for the field. - List validators; + final List validators; /// Builds the widget representing the field. /// diff --git a/packages/flutter_user/lib/src/models/registration/auth_pass_field.dart b/packages/flutter_user/lib/src/models/registration/auth/auth_pass_field.dart similarity index 97% rename from packages/flutter_user/lib/src/models/registration/auth_pass_field.dart rename to packages/flutter_user/lib/src/models/registration/auth/auth_pass_field.dart index 7a55926..1a349b0 100644 --- a/packages/flutter_user/lib/src/models/registration/auth_pass_field.dart +++ b/packages/flutter_user/lib/src/models/registration/auth/auth_pass_field.dart @@ -4,7 +4,7 @@ import "package:flutter/material.dart"; import "package:flutter_input_library/flutter_input_library.dart"; -import "package:flutter_user/src/models/registration/auth_field.dart"; +import "package:flutter_user/src/models/registration/auth/auth_field.dart"; /// A field for capturing password inputs in a Flutter form. /// diff --git a/packages/flutter_user/lib/src/models/registration/auth_step.dart b/packages/flutter_user/lib/src/models/registration/auth/auth_step.dart similarity index 66% rename from packages/flutter_user/lib/src/models/registration/auth_step.dart rename to packages/flutter_user/lib/src/models/registration/auth/auth_step.dart index 6ef85f7..7914047 100644 --- a/packages/flutter_user/lib/src/models/registration/auth_step.dart +++ b/packages/flutter_user/lib/src/models/registration/auth/auth_step.dart @@ -2,15 +2,15 @@ // // SPDX-License-Identifier: BSD-3-Clause -import "package:flutter_user/src/models/registration/auth_field.dart"; +import "package:flutter_user/src/models/registration/auth/auth_field.dart"; /// A step in the authentication process. class AuthStep { /// Constructs an [AuthStep] object. - AuthStep({ + const AuthStep({ required this.fields, }); /// The fields in the step. - List fields; + final List fields; } diff --git a/packages/flutter_user/lib/src/models/registration/auth_text_field.dart b/packages/flutter_user/lib/src/models/registration/auth/auth_text_field.dart similarity index 97% rename from packages/flutter_user/lib/src/models/registration/auth_text_field.dart rename to packages/flutter_user/lib/src/models/registration/auth/auth_text_field.dart index c4d00de..f8e9e75 100644 --- a/packages/flutter_user/lib/src/models/registration/auth_text_field.dart +++ b/packages/flutter_user/lib/src/models/registration/auth/auth_text_field.dart @@ -3,7 +3,7 @@ // SPDX-License-Identifier: BSD-3-Clause import "package:flutter/material.dart"; -import "package:flutter_user/src/models/registration/auth_field.dart"; +import "package:flutter_user/src/models/registration/auth/auth_field.dart"; /// A field for capturing text inputs in a Flutter form. /// diff --git a/packages/flutter_user/lib/src/models/registration/registration_options.dart b/packages/flutter_user/lib/src/models/registration/registration_options.dart index 672a1dd..232b191 100644 --- a/packages/flutter_user/lib/src/models/registration/registration_options.dart +++ b/packages/flutter_user/lib/src/models/registration/registration_options.dart @@ -1,8 +1,8 @@ import "package:flutter/material.dart"; import "package:flutter_user/src/models/login/login_options.dart"; -import "package:flutter_user/src/models/registration/auth_pass_field.dart"; -import "package:flutter_user/src/models/registration/auth_step.dart"; -import "package:flutter_user/src/models/registration/auth_text_field.dart"; +import "package:flutter_user/src/models/registration/auth/auth_pass_field.dart"; +import "package:flutter_user/src/models/registration/auth/auth_step.dart"; +import "package:flutter_user/src/models/registration/auth/auth_text_field.dart"; import "package:flutter_user/src/models/registration/registration_spacer_options.dart"; import "package:flutter_user/src/models/registration/registration_translations.dart"; From 71fc338a8ee417158c013dc0f42396d2acd84b36 Mon Sep 17 00:00:00 2001 From: Freek van de Ven Date: Wed, 18 Jun 2025 11:43:33 +0200 Subject: [PATCH 09/16] feat: make all translations required in the constructor of ForgotPasswordTranslations and LoginTranslations --- CHANGELOG.md | 1 + packages/flutter_user/example/lib/main.dart | 2 +- .../forgot_password_options.dart | 2 +- .../forgot_password_translations.dart | 18 ++++++++++++++++++ .../lib/src/models/login/login_options.dart | 2 +- .../src/models/login/login_translations.dart | 12 ++++++++++++ .../src/screens/forgot_password_success.dart | 2 +- 7 files changed, 35 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5652016..cc983dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ ## Unreleased +- Made all translations required in the constructor of ForgotPasswordTranslations and LoginTranslations. - Removed the default values for colors in the LoginOptions, RegistrationOptions, and ForgotPasswordOptions. - Added rest_user_repository package which is a REST implementation of the UserRepositoryInterface. - Changed afterLoginScreen to a nullable Widget so a screen isn't automatically pushed after login. diff --git a/packages/flutter_user/example/lib/main.dart b/packages/flutter_user/example/lib/main.dart index e4e10c7..4ef62bf 100644 --- a/packages/flutter_user/example/lib/main.dart +++ b/packages/flutter_user/example/lib/main.dart @@ -28,7 +28,7 @@ class UserstoryScreen extends StatelessWidget { biometricsOptions: LoginBiometricsOptions( loginWithBiometrics: true, ), - translations: LoginTranslations( + translations: LoginTranslations.empty( loginTitle: "Login", loginButton: "Log in", loginSubtitle: "Welcome back!", diff --git a/packages/flutter_user/lib/src/models/forgot_password/forgot_password_options.dart b/packages/flutter_user/lib/src/models/forgot_password/forgot_password_options.dart index f92c17a..0931e92 100644 --- a/packages/flutter_user/lib/src/models/forgot_password/forgot_password_options.dart +++ b/packages/flutter_user/lib/src/models/forgot_password/forgot_password_options.dart @@ -15,7 +15,7 @@ class ForgotPasswordOptions { ), this.forgotPasswordSpacerOptions = const ForgotPasswordSpacerOptions(), this.maxFormWidth = 300, - this.translations = const ForgotPasswordTranslations(), + this.translations = const ForgotPasswordTranslations.empty(), this.accessibilityIdentifiers = const LoginAccessibilityIdentifiers.empty(), this.requestForgotPasswordButtonBuilder = _createRequestForgotPasswordButton, diff --git a/packages/flutter_user/lib/src/models/forgot_password/forgot_password_translations.dart b/packages/flutter_user/lib/src/models/forgot_password/forgot_password_translations.dart index 1ba20de..5ed44c4 100644 --- a/packages/flutter_user/lib/src/models/forgot_password/forgot_password_translations.dart +++ b/packages/flutter_user/lib/src/models/forgot_password/forgot_password_translations.dart @@ -1,5 +1,23 @@ +/// class ForgotPasswordTranslations { + /// Creates a [ForgotPasswordTranslations] with the provided values. const ForgotPasswordTranslations({ + required this.forgotPasswordTitle, + required this.forgotPasswordDescription, + required this.requestForgotPasswordButton, + required this.forgotPasswordSuccessTitle, + required this.forgotPasswordSuccessButtonTitle, + required this.registrationSuccessTitle, + required this.registrationSuccessButtonTitle, + required this.forgotPasswordUnsuccessfullTitle, + required this.forgotPasswordUnsuccessfullDescription, + required this.forgotPasswordUnsuccessButtonTitle, + required this.registrationUnsuccessfullTitle, + }); + + /// Creates a [ForgotPasswordTranslations] with default values. + /// /// This constructor is used when no specific translations are provided. + const ForgotPasswordTranslations.empty({ this.forgotPasswordTitle = "forgot password", this.forgotPasswordDescription = "No worries. Enter your email address below" diff --git a/packages/flutter_user/lib/src/models/login/login_options.dart b/packages/flutter_user/lib/src/models/login/login_options.dart index 1227627..11bd3f0 100644 --- a/packages/flutter_user/lib/src/models/login/login_options.dart +++ b/packages/flutter_user/lib/src/models/login/login_options.dart @@ -12,7 +12,7 @@ class LoginOptions extends Equatable { const LoginOptions({ this.image, this.spacers = const LoginSpacerOptions(), - this.translations = const LoginTranslations(), + this.translations = const LoginTranslations.empty(), this.validationService, this.biometricsOptions = const LoginBiometricsOptions(), this.accessibilityIdentifiers = const LoginAccessibilityIdentifiers.empty(), diff --git a/packages/flutter_user/lib/src/models/login/login_translations.dart b/packages/flutter_user/lib/src/models/login/login_translations.dart index 689cbff..e61716b 100644 --- a/packages/flutter_user/lib/src/models/login/login_translations.dart +++ b/packages/flutter_user/lib/src/models/login/login_translations.dart @@ -2,6 +2,18 @@ import "package:equatable/equatable.dart"; class LoginTranslations extends Equatable { const LoginTranslations({ + required this.loginTitle, + required this.loginSubtitle, + required this.emailEmpty, + required this.passwordEmpty, + required this.emailInvalid, + required this.loginButton, + required this.forgotPasswordButton, + required this.registrationButton, + required this.biometricsLoginMessage, + }); + + const LoginTranslations.empty({ this.loginTitle = "log in", this.loginSubtitle, this.emailEmpty = "Please enter your email address", diff --git a/packages/flutter_user/lib/src/screens/forgot_password_success.dart b/packages/flutter_user/lib/src/screens/forgot_password_success.dart index 3395613..3f720da 100644 --- a/packages/flutter_user/lib/src/screens/forgot_password_success.dart +++ b/packages/flutter_user/lib/src/screens/forgot_password_success.dart @@ -7,7 +7,7 @@ class ForgotPasswordSuccess extends StatelessWidget { /// Forgot Password Success constructor const ForgotPasswordSuccess({ required this.onRequestForgotPassword, - this.translations = const ForgotPasswordTranslations(), + this.translations = const ForgotPasswordTranslations.empty(), super.key, }); From 1c0be942602f78647a7424145fe9bb0cd68c4470 Mon Sep 17 00:00:00 2001 From: Freek van de Ven Date: Thu, 19 Jun 2025 16:36:43 +0200 Subject: [PATCH 10/16] fix: add apiPrefix to rest_user_repository to allow for anything in the path before /auth/token and the other endpoints --- .../lib/rest_user_repository.dart | 56 +++++++++++-------- packages/rest_user_repository/pubspec.yaml | 2 +- 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/packages/rest_user_repository/lib/rest_user_repository.dart b/packages/rest_user_repository/lib/rest_user_repository.dart index bbc2ca8..319aa89 100644 --- a/packages/rest_user_repository/lib/rest_user_repository.dart +++ b/packages/rest_user_repository/lib/rest_user_repository.dart @@ -23,24 +23,34 @@ class _TokenAuthService extends AuthenticationService { } /// An implementation of [UserRepositoryInterface] that uses a REST API. -class RestUserRepository implements UserRepositoryInterface { +class RestUserRepository extends HttpApiService + implements UserRepositoryInterface { /// Creates an instance of the REST user repository. /// /// Requires the [baseUrl] for the API endpoints. - RestUserRepository({required String baseUrl}) { - _authService = _TokenAuthService(); - _apiService = HttpApiService( - baseUrl: Uri.parse(baseUrl), - authenticationService: _authService, - defaultHeaders: { - "Content-Type": "application/json", - "Accept": "application/json", - }, - ); - } - - late final HttpApiService _apiService; - late final _TokenAuthService _authService; + RestUserRepository({ + required super.baseUrl, // Pass baseUrl to HttpApiService + super.client, + this.apiPrefix = "", + }) : _authService = _TokenAuthService(), + super( + authenticationService: _TokenAuthService(), + defaultHeaders: const { + "Content-Type": "application/json", + "Accept": "application/json", + }, + apiResponseConverter: const MapJsonResponseConverter(), + ); + + final _TokenAuthService _authService; + + /// The prefix for the API endpoints, allowing for versioning + /// or path adjustments. + final String apiPrefix; + + /// The base endpoint for all API calls within this repository, + /// incorporating the [apiPrefix]. + Endpoint get _baseEndpoint => endpoint(apiPrefix); /// Logs in a user with the provided [email] and [password]. /// @@ -58,7 +68,7 @@ class RestUserRepository implements UserRepositoryInterface { serialize: (body) => body, ); var endpoint = - _apiService.endpoint("/auth/login").withConverter(converter); + _baseEndpoint.child("/auth/token").withConverter(converter); var response = await endpoint.post( requestModel: {"email": email, "password": password}, @@ -87,8 +97,7 @@ class RestUserRepository implements UserRepositoryInterface { deserialize: (json) => AuthResponse(userObject: json["user"]), serialize: (body) => body, ); - var endpoint = - _apiService.endpoint("/auth/register").withConverter(converter); + var endpoint = _baseEndpoint.child("/user").withConverter(converter); var response = await endpoint.post(requestModel: values); @@ -117,13 +126,14 @@ class RestUserRepository implements UserRepositoryInterface { ), serialize: (body) => body, ); - var endpoint = _apiService - .endpoint("/auth/request-password-change") + var endpoint = _baseEndpoint + .child("/user/password-reset/request") .withConverter(converter); var response = await endpoint.post(requestModel: {"email": email}); - - return response.result!; + return response.statusCode == 200 + ? const RequestPasswordResponse(requestSuccesfull: true) + : response.result!; } on ApiException catch (e) { throw _handleAuthError(e); } @@ -135,7 +145,7 @@ class RestUserRepository implements UserRepositoryInterface { @override Future getLoggedInUser() async { try { - var endpoint = _apiService.endpoint("/users/me").authenticate(); + var endpoint = _baseEndpoint.child("/users/me").authenticate(); var response = await endpoint.get(); return jsonDecode(response.inner.body); } on ApiException catch (e) { diff --git a/packages/rest_user_repository/pubspec.yaml b/packages/rest_user_repository/pubspec.yaml index 5e60c37..a597b54 100644 --- a/packages/rest_user_repository/pubspec.yaml +++ b/packages/rest_user_repository/pubspec.yaml @@ -11,7 +11,7 @@ environment: dependencies: dart_api_service: hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub/ - version: ^1.1.1 + version: ^1.1.2 user_repository_interface: hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub/ version: ^6.4.0 From 109a0a3bce9760a557d6f645b71c07e6880792e9 Mon Sep 17 00:00:00 2001 From: Freek van de Ven Date: Fri, 20 Jun 2025 11:03:32 +0200 Subject: [PATCH 11/16] feat: make the endpoints for the Restful User repository configurable --- .../lib/rest_user_repository.dart | 41 +++++++++++++++---- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/packages/rest_user_repository/lib/rest_user_repository.dart b/packages/rest_user_repository/lib/rest_user_repository.dart index 319aa89..85d3949 100644 --- a/packages/rest_user_repository/lib/rest_user_repository.dart +++ b/packages/rest_user_repository/lib/rest_user_repository.dart @@ -32,6 +32,11 @@ class RestUserRepository extends HttpApiService required super.baseUrl, // Pass baseUrl to HttpApiService super.client, this.apiPrefix = "", + this.loginEndpoint = "/auth/token", + this.registerEndpoint = "/user", + this.passwordResetEndpoint = "/user/password-reset/request", + this.loggedInUserEndpoint = "/users/me", + this.onTokenReceived, }) : _authService = _TokenAuthService(), super( authenticationService: _TokenAuthService(), @@ -48,6 +53,21 @@ class RestUserRepository extends HttpApiService /// or path adjustments. final String apiPrefix; + /// Endpoint for user login. + final String loginEndpoint; + + /// Endpoint for user registration. + final String registerEndpoint; + + /// Endpoint for password reset requests. + final String passwordResetEndpoint; + + /// Endpoint for fetching the logged-in user's profile. + final String loggedInUserEndpoint; + + /// Callback to handle the received token. + final void Function(String? token)? onTokenReceived; + /// The base endpoint for all API calls within this repository, /// incorporating the [apiPrefix]. Endpoint get _baseEndpoint => endpoint(apiPrefix); @@ -68,14 +88,16 @@ class RestUserRepository extends HttpApiService serialize: (body) => body, ); var endpoint = - _baseEndpoint.child("/auth/token").withConverter(converter); + _baseEndpoint.child(loginEndpoint).withConverter(converter); var response = await endpoint.post( requestModel: {"email": email, "password": password}, ); var userMap = response.result?.userObject as Map?; - _authService.token = userMap?["token"] as String?; + var token = userMap?["token"] as String?; + _authService.token = token; + onTokenReceived?.call(token); return response.result!; } on ApiException catch (e) { @@ -97,12 +119,15 @@ class RestUserRepository extends HttpApiService deserialize: (json) => AuthResponse(userObject: json["user"]), serialize: (body) => body, ); - var endpoint = _baseEndpoint.child("/user").withConverter(converter); + var endpoint = + _baseEndpoint.child(registerEndpoint).withConverter(converter); var response = await endpoint.post(requestModel: values); var userMap = response.result?.userObject as Map?; - _authService.token = userMap?["token"] as String?; + var token = userMap?["token"] as String?; + _authService.token = token; + onTokenReceived?.call(token); return response.result!; } on ApiException catch (e) { @@ -126,9 +151,8 @@ class RestUserRepository extends HttpApiService ), serialize: (body) => body, ); - var endpoint = _baseEndpoint - .child("/user/password-reset/request") - .withConverter(converter); + var endpoint = + _baseEndpoint.child(passwordResetEndpoint).withConverter(converter); var response = await endpoint.post(requestModel: {"email": email}); return response.statusCode == 200 @@ -145,7 +169,7 @@ class RestUserRepository extends HttpApiService @override Future getLoggedInUser() async { try { - var endpoint = _baseEndpoint.child("/users/me").authenticate(); + var endpoint = _baseEndpoint.child(loggedInUserEndpoint).authenticate(); var response = await endpoint.get(); return jsonDecode(response.inner.body); } on ApiException catch (e) { @@ -161,6 +185,7 @@ class RestUserRepository extends HttpApiService @override Future logout() async { _authService.clearToken(); + onTokenReceived?.call(null); return true; } From a1f2f837c70e8247cccddb03d3f9e07532f0609d Mon Sep 17 00:00:00 2001 From: Freek van de Ven Date: Tue, 24 Jun 2025 14:08:26 +0200 Subject: [PATCH 12/16] fix: rewrite the onFinish for the registration screen to only call the success callback if there is no error --- CHANGELOG.md | 1 + .../lib/src/screens/registration_screen.dart | 66 +++++++++---------- 2 files changed, 31 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc983dd..431828b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ ## Unreleased +- Fixed a bug with registration errors still triggering the success callback. - Made all translations required in the constructor of ForgotPasswordTranslations and LoginTranslations. - Removed the default values for colors in the LoginOptions, RegistrationOptions, and ForgotPasswordOptions. - Added rest_user_repository package which is a REST implementation of the UserRepositoryInterface. diff --git a/packages/flutter_user/lib/src/screens/registration_screen.dart b/packages/flutter_user/lib/src/screens/registration_screen.dart index 6b77888..8b2364a 100644 --- a/packages/flutter_user/lib/src/screens/registration_screen.dart +++ b/packages/flutter_user/lib/src/screens/registration_screen.dart @@ -55,7 +55,7 @@ class _RegistrationScreenState extends State { ); } - Future onNext() async { + Future onClickNext() async { FocusScope.of(context).unfocus(); if (!_formKey.currentState!.validate()) { @@ -65,51 +65,45 @@ class _RegistrationScreenState extends State { _formKey.currentState!.save(); _validate(_pageController.page!.toInt()); - var success = await onFinish(); - if (success) { - return; - } - - await _pageController.nextPage( - duration: _animationDuration, - curve: _animationCurve, - ); + await goToNextPage(); } - Future onFinish() async { + Future goToNextPage() async { if (_pageController.page!.toInt() == widget.registrationOptions.steps.length - 1) { - var values = {}; - - for (var step in widget.registrationOptions.steps) { - for (var field in step.fields) { - values[field.name] = field.value; - } - } + await onFinish(); + } else { + await _pageController.nextPage( + duration: _animationDuration, + curve: _animationCurve, + ); + } + } - try { - await widget.userService.register(values: values); - } on AuthException catch (e) { - var pageToReturn = await widget.onError.call(e); + Future onFinish() async { + var values = {}; - if (pageToReturn != null) { - if (pageToReturn == _pageController.page!.toInt()) { - return true; - } - await _pageController.animateToPage( - pageToReturn, - duration: _animationDuration, - curve: _animationCurve, - ); - return true; - } + for (var step in widget.registrationOptions.steps) { + for (var field in step.fields) { + values[field.name] = field.value; } + } + try { + await widget.userService.register(values: values); await widget.afterRegistration.call(); + } on AuthException catch (e) { + var pageToReturn = await widget.onError.call(e); - return true; + if (pageToReturn != null && + pageToReturn != _pageController.page!.toInt()) { + await _pageController.animateToPage( + pageToReturn, + duration: _animationDuration, + curve: _animationCurve, + ); + } } - return false; } @override @@ -237,7 +231,7 @@ class _RegistrationScreenState extends State { : registrationOptions .translations.nextStepBtn, onTap: () async { - await onNext(); + await onClickNext(); }, ), ], From c55516ec1b7496f2f4cf92a44566e9b5fa766977 Mon Sep 17 00:00:00 2001 From: Freek van de Ven Date: Tue, 24 Jun 2025 14:10:12 +0200 Subject: [PATCH 13/16] chore: mark next release as 7.0.0 because the changes are breaking the API --- CHANGELOG.md | 2 +- packages/firebase_user_repository/pubspec.yaml | 4 ++-- packages/flutter_user/pubspec.yaml | 4 ++-- packages/rest_user_repository/pubspec.yaml | 4 ++-- packages/user_repository_interface/pubspec.yaml | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 431828b..eb018d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## Unreleased +## 7.0.0 - Fixed a bug with registration errors still triggering the success callback. - Made all translations required in the constructor of ForgotPasswordTranslations and LoginTranslations. - Removed the default values for colors in the LoginOptions, RegistrationOptions, and ForgotPasswordOptions. diff --git a/packages/firebase_user_repository/pubspec.yaml b/packages/firebase_user_repository/pubspec.yaml index fd2ec27..141c544 100644 --- a/packages/firebase_user_repository/pubspec.yaml +++ b/packages/firebase_user_repository/pubspec.yaml @@ -1,6 +1,6 @@ name: firebase_user_repository description: "firebase_user_repository for flutter_user package" -version: 6.4.0 +version: 7.0.0 repository: https://github.com/Iconica-Development/flutter_user publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub @@ -14,7 +14,7 @@ dependencies: sdk: flutter user_repository_interface: hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub/ - version: ^6.4.0 + version: ^7.0.0 cloud_firestore: ^5.4.2 firebase_auth: ^5.3.0 diff --git a/packages/flutter_user/pubspec.yaml b/packages/flutter_user/pubspec.yaml index 17e1247..5088c38 100644 --- a/packages/flutter_user/pubspec.yaml +++ b/packages/flutter_user/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_user description: "Flutter Userstory for onboarding, login, and registration." -version: 6.4.0 +version: 7.0.0 repository: https://github.com/Iconica-Development/flutter_user publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub @@ -22,7 +22,7 @@ dependencies: version: ^4.1.0 user_repository_interface: hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub/ - version: ^6.4.0 + version: ^7.0.0 flutter_accessibility: hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub version: ^0.0.3 diff --git a/packages/rest_user_repository/pubspec.yaml b/packages/rest_user_repository/pubspec.yaml index a597b54..6c16374 100644 --- a/packages/rest_user_repository/pubspec.yaml +++ b/packages/rest_user_repository/pubspec.yaml @@ -1,6 +1,6 @@ name: rest_user_repository description: "RESTful implementation of the user_repository_interface for flutter_user package" -version: 6.4.0 +version: 7.0.0 repository: https://github.com/Iconica-Development/flutter_user publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub @@ -14,7 +14,7 @@ dependencies: version: ^1.1.2 user_repository_interface: hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub/ - version: ^6.4.0 + version: ^7.0.0 dev_dependencies: flutter_iconica_analysis: diff --git a/packages/user_repository_interface/pubspec.yaml b/packages/user_repository_interface/pubspec.yaml index 0fdd67c..4fc94b3 100644 --- a/packages/user_repository_interface/pubspec.yaml +++ b/packages/user_repository_interface/pubspec.yaml @@ -1,6 +1,6 @@ name: user_repository_interface description: "user_repository_interface for flutter_user package" -version: 6.4.0 +version: 7.0.0 repository: https://github.com/Iconica-Development/flutter_user publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub From a3272f28ea230561f40f8fd5a19fdffbac529e7a Mon Sep 17 00:00:00 2001 From: Freek van de Ven Date: Fri, 18 Jul 2025 13:06:52 +0200 Subject: [PATCH 14/16] feat: change image option for login page to the top --- CHANGELOG.md | 1 + .../models/login/login_spacer_options.dart | 16 +++++++------- .../registration/registration_options.dart | 2 -- .../screens/email_password_login_form.dart | 21 +++++++++---------- .../lib/src/screens/registration_screen.dart | 6 ++---- 5 files changed, 21 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb018d7..ce21aff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Added rest_user_repository package which is a REST implementation of the UserRepositoryInterface. - Changed afterLoginScreen to a nullable Widget so a screen isn't automatically pushed after login. - Moved the RegistrationOptions and ForgotPasswordOptions to the FlutterUserOptions. +- Changed the image option for the Login page to the top of the screen. ## 6.4.0 diff --git a/packages/flutter_user/lib/src/models/login/login_spacer_options.dart b/packages/flutter_user/lib/src/models/login/login_spacer_options.dart index 34c70a5..7d38ffd 100644 --- a/packages/flutter_user/lib/src/models/login/login_spacer_options.dart +++ b/packages/flutter_user/lib/src/models/login/login_spacer_options.dart @@ -9,23 +9,23 @@ class LoginSpacerOptions extends Equatable { this.spacerAfterForm, this.spacerAfterButton, this.titleSpacer = 1, - this.spacerBeforeTitle = 8, + this.spacerBeforeImage = 8, this.spacerAfterTitle = 2, this.formFlexValue = 2, }); - /// Flex value for the spacer before the title. - final int? spacerBeforeTitle; + /// Flex value for the spacer before the image. + final int? spacerBeforeImage; + + /// Flex value for the spacer between the image and title + final int? spacerAfterImage; /// Flex value for the spacer between the title and subtitle. final int? spacerAfterTitle; - /// Flex value for the spacer between the subtitle and image. + /// Flex value for the spacer between the subtitle and form final int? spacerAfterSubtitle; - /// Flex value for the spacer between the image and form. - final int? spacerAfterImage; - /// Flex value for the spacer between the form and button. final int? spacerAfterForm; @@ -40,7 +40,7 @@ class LoginSpacerOptions extends Equatable { @override List get props => [ - spacerBeforeTitle, + spacerBeforeImage, spacerAfterTitle, spacerAfterSubtitle, spacerAfterImage, diff --git a/packages/flutter_user/lib/src/models/registration/registration_options.dart b/packages/flutter_user/lib/src/models/registration/registration_options.dart index 232b191..e38d87b 100644 --- a/packages/flutter_user/lib/src/models/registration/registration_options.dart +++ b/packages/flutter_user/lib/src/models/registration/registration_options.dart @@ -84,8 +84,6 @@ List getDefaultSteps({ contentPadding: const EdgeInsets.symmetric(horizontal: 8), label: labelBuilder?.call(translations.defaultEmailLabel), hintText: translations.defaultEmailHint, - border: const OutlineInputBorder(), - focusedBorder: const OutlineInputBorder(), ), textStyle: textStyle, padding: const EdgeInsets.symmetric(vertical: 20), diff --git a/packages/flutter_user/lib/src/screens/email_password_login_form.dart b/packages/flutter_user/lib/src/screens/email_password_login_form.dart index 64545d1..7f2171c 100644 --- a/packages/flutter_user/lib/src/screens/email_password_login_form.dart +++ b/packages/flutter_user/lib/src/screens/email_password_login_form.dart @@ -224,7 +224,6 @@ class _EmailPasswordLoginFormState extends State { return Scaffold( backgroundColor: options.loginBackgroundColor, body: CustomScrollView( - physics: const ScrollPhysics(), slivers: [ SliverFillRemaining( hasScrollBody: false, @@ -326,7 +325,16 @@ class _LoginTitle extends StatelessWidget { return Column( children: [ ...buildOptionalSpacer( - options.spacers.spacerBeforeTitle, + options.spacers.spacerBeforeImage, + ), + if (options.image != null) ...[ + Padding( + padding: const EdgeInsets.all(16), + child: options.image, + ), + ], + ...buildOptionalSpacer( + options.spacers.spacerAfterImage, ), if (title != null) ...[ Align( @@ -352,15 +360,6 @@ class _LoginTitle extends StatelessWidget { ...buildOptionalSpacer( options.spacers.spacerAfterSubtitle, ), - if (options.image != null) ...[ - Padding( - padding: const EdgeInsets.all(16), - child: options.image, - ), - ], - ...buildOptionalSpacer( - options.spacers.spacerAfterImage, - ), ], ); } diff --git a/packages/flutter_user/lib/src/screens/registration_screen.dart b/packages/flutter_user/lib/src/screens/registration_screen.dart index 8b2364a..3bcb47c 100644 --- a/packages/flutter_user/lib/src/screens/registration_screen.dart +++ b/packages/flutter_user/lib/src/screens/registration_screen.dart @@ -136,16 +136,14 @@ class _RegistrationScreenState extends State { mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Expanded( + Spacer( flex: registrationOptions .spacerOptions.beforeTitleFlex, - child: Container(), ), registrationOptions.title!, - Expanded( + Spacer( flex: registrationOptions .spacerOptions.afterTitleFlex, - child: Container(), ), ], ), From ce09cdf6bd925231c673d8e8d34d00e702d2fdae Mon Sep 17 00:00:00 2001 From: Kiril Tijsma Date: Mon, 16 Feb 2026 09:22:48 +0100 Subject: [PATCH 15/16] fix: correct error screen layout --- .../screens/email_password_login_form.dart | 11 ++++++- .../src/screens/forgot_password_success.dart | 2 +- .../lib/src/screens/registration_screen.dart | 30 ++++++++----------- .../screens/registration_unsuccessfull.dart | 11 ++++--- 4 files changed, 30 insertions(+), 24 deletions(-) diff --git a/packages/flutter_user/lib/src/screens/email_password_login_form.dart b/packages/flutter_user/lib/src/screens/email_password_login_form.dart index 7f2171c..f7cd03c 100644 --- a/packages/flutter_user/lib/src/screens/email_password_login_form.dart +++ b/packages/flutter_user/lib/src/screens/email_password_login_form.dart @@ -262,7 +262,13 @@ class _EmailPasswordLoginFormState extends State { passwordTextFormField, ), ), - forgotPasswordButton, + Padding( + padding: const EdgeInsets.only( + top: 4.0, + bottom: 8.0, + ), + child: forgotPasswordButton, + ), ...buildOptionalSpacer( options.spacers.spacerAfterForm, ), @@ -288,6 +294,9 @@ class _EmailPasswordLoginFormState extends State { ), ], if (widget.onRegister != null) ...[ + const SizedBox( + height: 8.0, + ), registerButton, ], ...buildOptionalSpacer( diff --git a/packages/flutter_user/lib/src/screens/forgot_password_success.dart b/packages/flutter_user/lib/src/screens/forgot_password_success.dart index 3f720da..28ea1f5 100644 --- a/packages/flutter_user/lib/src/screens/forgot_password_success.dart +++ b/packages/flutter_user/lib/src/screens/forgot_password_success.dart @@ -43,7 +43,7 @@ class ForgotPasswordSuccess extends StatelessWidget { child: SafeArea( bottom: true, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), + padding: const EdgeInsets.all(20), child: PrimaryButton( buttonTitle: translations.forgotPasswordSuccessButtonTitle, diff --git a/packages/flutter_user/lib/src/screens/registration_screen.dart b/packages/flutter_user/lib/src/screens/registration_screen.dart index 3bcb47c..7f1a7e6 100644 --- a/packages/flutter_user/lib/src/screens/registration_screen.dart +++ b/packages/flutter_user/lib/src/screens/registration_screen.dart @@ -124,9 +124,8 @@ class _RegistrationScreenState extends State { controller: _pageController, physics: const NeverScrollableScrollPhysics(), children: [ - for (var currentStep = 0; - currentStep < registrationOptions.steps.length; - currentStep++) ...[ + for (var (index, step) + in registrationOptions.steps.indexed) ...[ Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -154,8 +153,7 @@ class _RegistrationScreenState extends State { alignment: Alignment.topCenter, child: Column( children: [ - for (AuthField field in registrationOptions - .steps[currentStep].fields) ...[ + for (AuthField field in step.fields) ...[ if (field.title != null) ...[ wrapWithDefaultStyle( style: theme.textTheme.headlineLarge!, @@ -167,7 +165,7 @@ class _RegistrationScreenState extends State { maxWidth: registrationOptions.maxFormWidth, ), child: field.build(context, () { - _validate(currentStep); + _validate(index); }), ), ], @@ -194,10 +192,10 @@ class _RegistrationScreenState extends State { onPrevious, registrationOptions .translations.previousStepBtn, - currentStep, + index, ) ?? Visibility( - visible: currentStep != 0, + visible: index != 0, child: stepButton( buttonText: registrationOptions .translations.previousStepBtn, @@ -208,8 +206,8 @@ class _RegistrationScreenState extends State { ), const SizedBox(width: 16), registrationOptions.nextButtonBuilder?.call( - onPrevious, - currentStep == + onClickNext, + index == registrationOptions .steps.length - 1 @@ -217,10 +215,10 @@ class _RegistrationScreenState extends State { .translations.registerBtn : registrationOptions .translations.nextStepBtn, - currentStep, + index, ) ?? stepButton( - buttonText: currentStep == + buttonText: index == registrationOptions .steps.length - 1 @@ -236,14 +234,10 @@ class _RegistrationScreenState extends State { ), ), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), if (registrationOptions.loginButton != null) ...[ registrationOptions.loginButton!, - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), ], ], ), diff --git a/packages/flutter_user/lib/src/screens/registration_unsuccessfull.dart b/packages/flutter_user/lib/src/screens/registration_unsuccessfull.dart index 871c507..7960401 100644 --- a/packages/flutter_user/lib/src/screens/registration_unsuccessfull.dart +++ b/packages/flutter_user/lib/src/screens/registration_unsuccessfull.dart @@ -56,10 +56,13 @@ class RegistrationUnsuccessfull extends StatelessWidget { ), child: SafeArea( bottom: true, - child: PrimaryButton( - buttonTitle: registrationOptions - .translations.registrationUnsuccessButtonTitle, - onPressed: onPressed, + child: Padding( + padding: const EdgeInsets.all(20.0), + child: PrimaryButton( + buttonTitle: registrationOptions + .translations.registrationUnsuccessButtonTitle, + onPressed: onPressed, + ), ), ), ), From 7e1b3916d489f72d67e3d637737f4c8ea0110bd1 Mon Sep 17 00:00:00 2001 From: Kiril Tijsma Date: Fri, 6 Mar 2026 11:08:51 +0100 Subject: [PATCH 16/16] fix: await onTokenReceived --- .../rest_user_repository/lib/rest_user_repository.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/rest_user_repository/lib/rest_user_repository.dart b/packages/rest_user_repository/lib/rest_user_repository.dart index 85d3949..e7a32d8 100644 --- a/packages/rest_user_repository/lib/rest_user_repository.dart +++ b/packages/rest_user_repository/lib/rest_user_repository.dart @@ -66,7 +66,7 @@ class RestUserRepository extends HttpApiService final String loggedInUserEndpoint; /// Callback to handle the received token. - final void Function(String? token)? onTokenReceived; + final Future Function(String? token)? onTokenReceived; /// The base endpoint for all API calls within this repository, /// incorporating the [apiPrefix]. @@ -97,7 +97,7 @@ class RestUserRepository extends HttpApiService var userMap = response.result?.userObject as Map?; var token = userMap?["token"] as String?; _authService.token = token; - onTokenReceived?.call(token); + await onTokenReceived?.call(token); return response.result!; } on ApiException catch (e) { @@ -127,7 +127,7 @@ class RestUserRepository extends HttpApiService var userMap = response.result?.userObject as Map?; var token = userMap?["token"] as String?; _authService.token = token; - onTokenReceived?.call(token); + await onTokenReceived?.call(token); return response.result!; } on ApiException catch (e) { @@ -185,7 +185,7 @@ class RestUserRepository extends HttpApiService @override Future logout() async { _authService.clearToken(); - onTokenReceived?.call(null); + await onTokenReceived?.call(null); return true; }