diff --git a/CHANGELOG.md b/CHANGELOG.md index 494523a..2a9cbe6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 6.4.0 + +- Added proper use of exceptions for all auth methods. +- Added the ability to add translations for all auth exceptions including two standard provided languages (nl, en) + ## 6.3.2 - Fixed infinite loading when closing the forgotPasswordSucces screen and ForgotPasswordUnsuccessfull screen. diff --git a/packages/firebase_user_repository/lib/src/firebase_user_repository.dart b/packages/firebase_user_repository/lib/src/firebase_user_repository.dart index 0c6da56..67e1b07 100644 --- a/packages/firebase_user_repository/lib/src/firebase_user_repository.dart +++ b/packages/firebase_user_repository/lib/src/firebase_user_repository.dart @@ -12,7 +12,7 @@ class FirebaseUserRepository implements UserRepositoryInterface { final String userCollecton; @override - Future loginWithEmailAndPassword({ + Future loginWithEmailAndPassword({ required String email, required String password, }) async { @@ -22,24 +22,16 @@ class FirebaseUserRepository implements UserRepositoryInterface { password: password, ); - return LoginResponse( - loginSuccessful: true, + return AuthResponse( userObject: userCredential.user, ); } on FirebaseAuthException catch (e) { - return LoginResponse( - loginSuccessful: false, - userObject: null, - loginError: UserError( - title: e.code, - message: e.message ?? "An error occurred", - ), - ); + throw _mapFirebaseAuthException(e); } } @override - Future register({ + Future register({ required Map values, }) async { try { @@ -54,19 +46,11 @@ class FirebaseUserRepository implements UserRepositoryInterface { .doc(userCredential.user!.uid) .set(values); - return RegistrationResponse( - registrationSuccessful: true, + return AuthResponse( userObject: userCredential.user, ); } on FirebaseAuthException catch (e) { - return RegistrationResponse( - registrationSuccessful: false, - userObject: null, - registrationError: UserError( - title: e.code, - message: e.message ?? "An error occurred", - ), - ); + throw _mapFirebaseAuthException(e); } } @@ -80,13 +64,7 @@ class FirebaseUserRepository implements UserRepositoryInterface { requestSuccesfull: true, ); } on FirebaseAuthException catch (e) { - return RequestPasswordResponse( - requestSuccesfull: false, - requestPasswordError: UserError( - title: e.code, - message: e.message ?? "An error occurred", - ), - ); + throw _mapFirebaseAuthException(e); } } @@ -101,4 +79,37 @@ class FirebaseUserRepository implements UserRepositoryInterface { @override Future isLoggedIn() async => _firebaseAuth.currentUser != null; + + /// Maps a [FirebaseAuthException] to a custom [AuthException]. + AuthException _mapFirebaseAuthException(FirebaseAuthException e) => + switch (e.code) { + "invalid-email" => InvalidEmailError(code: e.code, message: e.message), + "user-disabled" => UserDisabledError(code: e.code, message: e.message), + "user-not-found" => UserNotFoundError(code: e.code, message: e.message), + "wrong-password" => + WrongPasswordError(code: e.code, message: e.message), + "email-already-in-use" => + EmailAlreadyInUseError(code: e.code, message: e.message), + "operation-not-allowed" => + OperationNotAllowedError(code: e.code, message: e.message), + "weak-password" => WeakPasswordError(code: e.code, message: e.message), + "too-many-requests" => + TooManyRequestsError(code: e.code, message: e.message), + "network-request-failed" => + NetworkError(code: e.code, message: e.message), + "invalid-credential" => + InvalidCredentialError(code: e.code, message: e.message), + "account-exists-with-different-credential" => + AccountExistsWithDifferentCredentialError( + code: e.code, + message: e.message, + ), + "invalid-verification-code" => + InvalidVerificationCodeError(code: e.code, message: e.message), + "invalid-verification-id" => + InvalidVerificationIdError(code: e.code, message: e.message), + "requires-recent-login" => + RequiresRecentLoginError(code: e.code, message: e.message), + _ => GenericAuthError(code: e.code, message: e.message), + }; } diff --git a/packages/firebase_user_repository/pubspec.yaml b/packages/firebase_user_repository/pubspec.yaml index 5b9cd7a..fd2ec27 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.3.2 +version: 6.4.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.3.2 + version: ^6.4.0 cloud_firestore: ^5.4.2 firebase_auth: ^5.3.0 diff --git a/packages/flutter_user/lib/flutter_user.dart b/packages/flutter_user/lib/flutter_user.dart index e59a347..5fae4f6 100644 --- a/packages/flutter_user/lib/flutter_user.dart +++ b/packages/flutter_user/lib/flutter_user.dart @@ -6,6 +6,7 @@ library flutter_user; export "package:user_repository_interface/user_repository_interface.dart"; export "src/flutter_user_navigator_userstory.dart"; +export "src/models/auth_exception_formatter.dart"; export "src/models/flutter_user_options.dart"; export "src/models/forgot_password/forgot_password_options.dart"; export "src/models/forgot_password/forgot_password_spacer_options.dart"; @@ -18,7 +19,6 @@ 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_exception.dart"; export "src/models/registration/auth_field.dart"; export "src/models/registration/auth_pass_field.dart"; export "src/models/registration/auth_step.dart"; 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 1a048b8..d9bb945 100644 --- a/packages/flutter_user/lib/src/flutter_user_navigator_userstory.dart +++ b/packages/flutter_user/lib/src/flutter_user_navigator_userstory.dart @@ -2,6 +2,7 @@ import "dart:async"; import "package:flutter/material.dart"; import "package:flutter_user/flutter_user.dart"; +import "package:flutter_user/src/models/auth_error_details.dart"; class FlutterUserNavigatorUserstory extends StatefulWidget { const FlutterUserNavigatorUserstory({ @@ -88,47 +89,45 @@ class _FlutterUserNavigatorUserstoryState 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)); - var loginResponse = await userService!.loginWithEmailAndPassword( - email: email, - password: password, - ); - - if (!loginResponse.loginSuccessful) { + try { + await userService?.loginWithEmailAndPassword( + email: email, + password: password, + ); + } on AuthException catch (e) { if (!mounted) return; Navigator.of(context, rootNavigator: true).pop(); if (!context.mounted) return; - await errorScaffoldMessenger(context, loginResponse); + var authErrorDetails = options!.authExceptionFormatter.format(e); + await errorScaffoldMessenger(context, authErrorDetails); return; } - await options!.afterLogin?.call(); - if (loginResponse.loginSuccessful) { - var onboardingUser = await options!.onBoardedUser?.call(); - if (!mounted) return; - Navigator.of(context, rootNavigator: true).pop(); - if (options!.useOnboarding && onboardingUser?.onboarded == false) { - await push( - Onboarding( - onboardingFinished: (results) async { - await options!.onOnboardingComplete?.call(results); - if (!mounted || !context.mounted) return; - Navigator.of(context).pop(); - await pushReplacement(widget.afterLoginScreen); - }, - ), - ); - } else { - if (!context.mounted) { - return; - } - await pushReplacement(widget.afterLoginScreen); - } + await options?.afterLogin?.call(); + + var onboardingUser = await options?.onBoardedUser?.call(); + if (!mounted) return; + Navigator.of(context, rootNavigator: true).pop(); + if (options!.useOnboarding && onboardingUser?.onboarded == false) { + await push( + Onboarding( + onboardingFinished: (results) async { + await options?.onOnboardingComplete?.call(results); + if (!mounted || !context.mounted) return; + Navigator.of(context).pop(); + await pushReplacement(widget.afterLoginScreen); + }, + ), + ); + } else { + if (!context.mounted) return; + await pushReplacement(widget.afterLoginScreen); } } @@ -138,11 +137,11 @@ class _FlutterUserNavigatorUserstoryState 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()); }, ); @@ -164,24 +163,27 @@ class _FlutterUserNavigatorUserstoryState ); FutureOr onRequestForgotPassword(String email) async { - if (options!.onRequestForgotPassword != null) { + if (options?.onRequestForgotPassword != null) { await options!.onRequestForgotPassword!(email); return; } unawaited(showLoadingIndicator(context)); - var requestPasswordReponse = - await userService!.requestChangePassword(email: email); - - if (!mounted) return; - Navigator.of(context).pop(); - - if (!requestPasswordReponse.requestSuccesfull) { + try { + var response = await userService!.requestChangePassword(email: email); + if (!mounted) return; + Navigator.of(context).pop(); + if (response.requestSuccesfull) { + await pushReplacement(_forgotPasswordSuccessScreen()); + } else { + await push(_forgotPasswordUnsuccessfullScreen()); + } + } on AuthException catch (_) { + if (!mounted) return; + Navigator.of(context).pop(); await push(_forgotPasswordUnsuccessfullScreen()); return; } - - await pushReplacement(_forgotPasswordSuccessScreen()); } return ForgotPasswordForm( @@ -196,7 +198,7 @@ class _FlutterUserNavigatorUserstoryState Widget _forgotPasswordSuccessScreen() => ForgotPasswordSuccess( translations: options!.forgotPasswordTranslations, onRequestForgotPassword: () async { - await options!.onForgotPasswordSuccess?.call() ?? + await options?.onForgotPasswordSuccess?.call() ?? // ignore: use_build_context_synchronously Navigator.of(context).pop(); }, @@ -205,7 +207,7 @@ class _FlutterUserNavigatorUserstoryState Widget _forgotPasswordUnsuccessfullScreen() => ForgotPasswordUnsuccessfull( translations: forgotPasswordTranslations!, onPressed: () async { - await options!.onForgotPasswordUnsuccessful?.call() ?? + await options?.onForgotPasswordUnsuccessful?.call() ?? // ignore: use_build_context_synchronously Navigator.of(context).pop(); }, @@ -215,28 +217,25 @@ class _FlutterUserNavigatorUserstoryState registrationOptions: registrationOptions!, userService: userService!, onError: (error) async { - if (options!.onRegistrationError != null) { - return options! - .onRegistrationError!(error ?? "Something went wrong"); + var errorDetails = options!.authExceptionFormatter.format(error); + + if (options?.onRegistrationError != null) { + return options!.onRegistrationError!(error, errorDetails); } await push( _registrationUnsuccessfullScreen( - error ?? "Something went wrong", + errorDetails, ), ); - var isPasswordError = error?.contains("weak-password") ?? false; - var isEmailError = error?.contains("email-already-in-use") ?? false; - if (isPasswordError) { - return 1; - } - if (isEmailError) { - return 0; - } + + if (error is WeakPasswordError) return 1; + + if (error is EmailAlreadyInUseError) return 0; return null; }, afterRegistration: () async { - options!.afterRegistration?.call() ?? + options?.afterRegistration?.call() ?? await pushReplacement(_registrationSuccessScreen()); }, ); @@ -244,13 +243,13 @@ class _FlutterUserNavigatorUserstoryState Widget _registrationSuccessScreen() => RegistrationSuccess( registrationOptions: registrationOptions!, onPressed: () async { - await options!.afterRegistrationSuccess?.call() ?? + await options?.afterRegistrationSuccess?.call() ?? // ignore: use_build_context_synchronously Navigator.of(context).pop(); }, ); - Widget _registrationUnsuccessfullScreen(String error) => + Widget _registrationUnsuccessfullScreen(AuthErrorDetails errorDetails) => RegistrationUnsuccessfull( registrationOptions: registrationOptions!, onPressed: () async { @@ -258,7 +257,7 @@ class _FlutterUserNavigatorUserstoryState // ignore: use_build_context_synchronously Navigator.of(context).pop(); }, - error: error, + errorDetails: errorDetails, ); Future push(Widget screen) async { diff --git a/packages/flutter_user/lib/src/models/auth_error_details.dart b/packages/flutter_user/lib/src/models/auth_error_details.dart new file mode 100644 index 0000000..cf65c10 --- /dev/null +++ b/packages/flutter_user/lib/src/models/auth_error_details.dart @@ -0,0 +1,9 @@ +class AuthErrorDetails { + const AuthErrorDetails({ + required this.title, + required this.message, + }); + + final String title; + final String message; +} diff --git a/packages/flutter_user/lib/src/models/auth_exception_formatter.dart b/packages/flutter_user/lib/src/models/auth_exception_formatter.dart new file mode 100644 index 0000000..de35e9e --- /dev/null +++ b/packages/flutter_user/lib/src/models/auth_exception_formatter.dart @@ -0,0 +1,158 @@ +import "package:flutter_user/flutter_user.dart"; +import "package:flutter_user/src/models/auth_error_details.dart"; + +/// A class to format [AuthException] into user-friendly [AuthErrorDetails]. +/// +/// This allows for easy localization and customization of authentication error +/// messages presented to the user. +class AuthExceptionFormatter { + /// Creates a formatter with custom error messages. + const AuthExceptionFormatter({ + this.userNotFound, + this.wrongPassword, + this.emailAlreadyInUse, + this.weakPassword, + this.invalidEmail, + this.userDisabled, + this.tooManyRequests, + this.networkError, + this.generic, + }); + + /// Dutch implementation of auth exception texts + factory AuthExceptionFormatter.dutch() => const AuthExceptionFormatter( + userNotFound: AuthErrorDetails( + title: "Gebruiker niet gevonden", + message: "We hebben geen account gevonden met dit e-mailadres.", + ), + wrongPassword: AuthErrorDetails( + title: "Verkeerd wachtwoord", + message: "Het ingevoerde wachtwoord is onjuist.", + ), + emailAlreadyInUse: AuthErrorDetails( + title: "E-mailadres in gebruik", + message: "Er bestaat al een account met dit e-mailadres.", + ), + weakPassword: AuthErrorDetails( + title: "Zwak wachtwoord", + message: "Het wachtwoord is te zwak. Gebruik minstens 6 tekens.", + ), + invalidEmail: AuthErrorDetails( + title: "Ongeldig e-mailadres", + message: "Het ingevoerde e-mailadres is ongeldig.", + ), + userDisabled: AuthErrorDetails( + title: "Gebruiker gedeactiveerd", + message: "Deze gebruiker is gedeactiveerd door een beheerder.", + ), + tooManyRequests: AuthErrorDetails( + title: "Te veel pogingen", + message: "Te veel foutieve pogingen. Probeer het later opnieuw.", + ), + networkError: AuthErrorDetails( + title: "Netwerkfout", + message: "Kan geen verbinding maken. Controleer je internet.", + ), + generic: AuthErrorDetails( + title: "Authenticatiefout", + message: "Er is een fout opgetreden. Probeer het later opnieuw.", + ), + ); + + /// English implementation of auth exception texts + factory AuthExceptionFormatter.english() => const AuthExceptionFormatter( + userNotFound: AuthErrorDetails( + title: "User not found", + message: "No account found with this email address.", + ), + wrongPassword: AuthErrorDetails( + title: "Incorrect password", + message: "The entered password is incorrect.", + ), + emailAlreadyInUse: AuthErrorDetails( + title: "Email already in use", + message: "An account already exists with this email address.", + ), + weakPassword: AuthErrorDetails( + title: "Weak password", + message: "The password is too weak. Use at least 6 characters.", + ), + invalidEmail: AuthErrorDetails( + title: "Invalid email", + message: "The entered email address is invalid.", + ), + userDisabled: AuthErrorDetails( + title: "User disabled", + message: "This account has been disabled by an administrator.", + ), + tooManyRequests: AuthErrorDetails( + title: "Too many attempts", + message: "Too many failed attempts. Please try again later.", + ), + networkError: AuthErrorDetails( + title: "Network error", + message: "Could not connect. Please check your internet connection.", + ), + generic: AuthErrorDetails( + title: "Authentication error", + message: "An unexpected error occurred. Please try again later.", + ), + ); + + final AuthErrorDetails? userNotFound; + final AuthErrorDetails? wrongPassword; + final AuthErrorDetails? emailAlreadyInUse; + final AuthErrorDetails? weakPassword; + final AuthErrorDetails? invalidEmail; + final AuthErrorDetails? userDisabled; + final AuthErrorDetails? tooManyRequests; + final AuthErrorDetails? networkError; + final AuthErrorDetails? generic; + + /// Creates a new formatter by overriding existing error details. + AuthExceptionFormatter copyWith({ + AuthErrorDetails? userNotFound, + AuthErrorDetails? wrongPassword, + AuthErrorDetails? emailAlreadyInUse, + AuthErrorDetails? weakPassword, + AuthErrorDetails? invalidEmail, + AuthErrorDetails? userDisabled, + AuthErrorDetails? tooManyRequests, + AuthErrorDetails? networkError, + AuthErrorDetails? generic, + }) => + AuthExceptionFormatter( + userNotFound: userNotFound ?? this.userNotFound, + wrongPassword: wrongPassword ?? this.wrongPassword, + emailAlreadyInUse: emailAlreadyInUse ?? this.emailAlreadyInUse, + weakPassword: weakPassword ?? this.weakPassword, + invalidEmail: invalidEmail ?? this.invalidEmail, + userDisabled: userDisabled ?? this.userDisabled, + tooManyRequests: tooManyRequests ?? this.tooManyRequests, + networkError: networkError ?? this.networkError, + generic: generic ?? this.generic, + ); + + /// Formats the given [AuthException] into human-readable details. + /// + /// This method uses pattern matching to find the appropriate error message. + /// If a specific message isn't provided for an exception type, it falls + /// back to the generic message, and finally to a default message using + /// the raw error code and message. + AuthErrorDetails format(AuthException exception) => switch (exception) { + UserNotFoundError() when userNotFound != null => userNotFound!, + WrongPasswordError() when wrongPassword != null => wrongPassword!, + EmailAlreadyInUseError() when emailAlreadyInUse != null => + emailAlreadyInUse!, + WeakPasswordError() when weakPassword != null => weakPassword!, + InvalidEmailError() when invalidEmail != null => invalidEmail!, + UserDisabledError() when userDisabled != null => userDisabled!, + TooManyRequestsError() when tooManyRequests != null => tooManyRequests!, + NetworkError() when networkError != null => networkError!, + _ => generic ?? + AuthErrorDetails( + title: exception.code ?? "Authentication failed", + message: exception.message ?? "An unknown error occurred.", + ), + }; +} 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 ff0f49d..fb3b05e 100644 --- a/packages/flutter_user/lib/src/models/flutter_user_options.dart +++ b/packages/flutter_user/lib/src/models/flutter_user_options.dart @@ -1,11 +1,13 @@ import "package:flutter/material.dart"; import "package:flutter_user/flutter_user.dart"; +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.authExceptionFormatter = const AuthExceptionFormatter(), this.beforeLogin, this.afterLogin, this.onBoardedUser, @@ -27,6 +29,7 @@ class FlutterUserOptions { final LoginTranslations loginTranslations; final RegistrationOptions? registrationOptions; final ForgotPasswordTranslations forgotPasswordTranslations; + final AuthExceptionFormatter authExceptionFormatter; final Future Function(String email, String password)? beforeLogin; final Future Function()? afterLogin; final Future Function()? onBoardedUser; @@ -39,7 +42,10 @@ class FlutterUserOptions { final Future Function(String email)? onRequestForgotPassword; final Future Function()? onForgotPasswordSuccess; final Future Function()? onForgotPasswordUnsuccessful; - final Future Function(String error)? onRegistrationError; + final Future Function( + AuthException exception, + AuthErrorDetails formattedErrorDetails, + )? onRegistrationError; final Future Function()? afterRegistration; final Future Function()? afterRegistrationSuccess; final Future Function()? afterRegistrationUnsuccessful; diff --git a/packages/flutter_user/lib/src/models/registration/auth_action.dart b/packages/flutter_user/lib/src/models/registration/auth_action.dart index 5a65bca..b00a46c 100644 --- a/packages/flutter_user/lib/src/models/registration/auth_action.dart +++ b/packages/flutter_user/lib/src/models/registration/auth_action.dart @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2022 Iconica +// SPDX-FileCopyrightText: 2025 Iconica // // SPDX-License-Identifier: BSD-3-Clause diff --git a/packages/flutter_user/lib/src/models/registration/auth_bool_field.dart b/packages/flutter_user/lib/src/models/registration/auth_bool_field.dart index 9eb2e27..bf90373 100644 --- a/packages/flutter_user/lib/src/models/registration/auth_bool_field.dart +++ b/packages/flutter_user/lib/src/models/registration/auth_bool_field.dart @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2022 Iconica +// SPDX-FileCopyrightText: 2025 Iconica // // SPDX-License-Identifier: BSD-3-Clause diff --git a/packages/flutter_user/lib/src/models/registration/auth_exception.dart b/packages/flutter_user/lib/src/models/registration/auth_exception.dart deleted file mode 100644 index ef7476e..0000000 --- a/packages/flutter_user/lib/src/models/registration/auth_exception.dart +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause - -/// An exception thrown when an authentication error occurs. -class AuthException implements Exception { - /// Constructs an [AuthException] object. - AuthException(this.message); - - /// The error message. - final String message; - - @override - String toString() => message; -} diff --git a/packages/flutter_user/lib/src/models/registration/auth_field.dart b/packages/flutter_user/lib/src/models/registration/auth_field.dart index 27fcfaf..9b0bc4f 100644 --- a/packages/flutter_user/lib/src/models/registration/auth_field.dart +++ b/packages/flutter_user/lib/src/models/registration/auth_field.dart @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2022 Iconica +// SPDX-FileCopyrightText: 2025 Iconica // // SPDX-License-Identifier: BSD-3-Clause diff --git a/packages/flutter_user/lib/src/models/registration/auth_step.dart b/packages/flutter_user/lib/src/models/registration/auth_step.dart index d162abb..6ef85f7 100644 --- a/packages/flutter_user/lib/src/models/registration/auth_step.dart +++ b/packages/flutter_user/lib/src/models/registration/auth_step.dart @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2022 Iconica +// SPDX-FileCopyrightText: 2025 Iconica // // SPDX-License-Identifier: BSD-3-Clause diff --git a/packages/flutter_user/lib/src/models/registration/auth_text_field.dart b/packages/flutter_user/lib/src/models/registration/auth_text_field.dart index 9615bc9..c4d00de 100644 --- a/packages/flutter_user/lib/src/models/registration/auth_text_field.dart +++ b/packages/flutter_user/lib/src/models/registration/auth_text_field.dart @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2022 Iconica +// SPDX-FileCopyrightText: 2025 Iconica // // SPDX-License-Identifier: BSD-3-Clause 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 9ab37d3..d36a24e 100644 --- a/packages/flutter_user/lib/src/models/registration/registration_translations.dart +++ b/packages/flutter_user/lib/src/models/registration/registration_translations.dart @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2022 Iconica +// SPDX-FileCopyrightText: 2025 Iconica // // SPDX-License-Identifier: BSD-3-Clause @@ -24,7 +24,6 @@ class RegistrationTranslations { required this.defaultPasswordToShortValidatorMessage, required this.registrationSuccessTitle, required this.registrationSuccessButtonTitle, - required this.registrationUnsuccessfullTitle, required this.registrationEmailUnsuccessfullDescription, required this.registrationPasswordUnsuccessfullDescription, required this.registrationUnsuccessButtonTitle, @@ -61,7 +60,6 @@ class RegistrationTranslations { "Password needs to be at least 6 characters long", registrationSuccessTitle = "your registration was successful", registrationSuccessButtonTitle = "Finish", - registrationUnsuccessfullTitle = "something went wrong", registrationEmailUnsuccessfullDescription = "This email address is already" " associated with an account. Please try again.", @@ -121,9 +119,6 @@ class RegistrationTranslations { /// The title for the registration success button. final String registrationSuccessButtonTitle; - /// The title for the registration unsuccessfull screen. - final String registrationUnsuccessfullTitle; - /// The description for the registration email unsuccessfull screen. final String registrationEmailUnsuccessfullDescription; @@ -152,7 +147,6 @@ class RegistrationTranslations { String? defaultPasswordToShortValidatorMessage, String? registrationSuccessTitle, String? registrationSuccessButtonTitle, - String? registrationUnsuccessfullTitle, String? registrationEmailUnsuccessfullDescription, String? registrationPasswordUnsuccessfullDescription, String? registrationUnsuccessButtonTitle, @@ -181,8 +175,6 @@ class RegistrationTranslations { registrationSuccessTitle ?? this.registrationSuccessTitle, registrationSuccessButtonTitle: registrationSuccessButtonTitle ?? this.registrationSuccessButtonTitle, - registrationUnsuccessfullTitle: registrationUnsuccessfullTitle ?? - this.registrationUnsuccessfullTitle, registrationEmailUnsuccessfullDescription: registrationEmailUnsuccessfullDescription ?? this.registrationEmailUnsuccessfullDescription, diff --git a/packages/flutter_user/lib/src/screens/registration_screen.dart b/packages/flutter_user/lib/src/screens/registration_screen.dart index f5b547e..6fe85cf 100644 --- a/packages/flutter_user/lib/src/screens/registration_screen.dart +++ b/packages/flutter_user/lib/src/screens/registration_screen.dart @@ -1,9 +1,7 @@ import "dart:async"; import "package:flutter/material.dart"; -import "package:flutter_user/src/models/registration/auth_field.dart"; -import "package:flutter_user/src/models/registration/registration_options.dart"; -import "package:user_repository_interface/user_repository_interface.dart"; +import "package:flutter_user/flutter_user.dart"; class RegistrationScreen extends StatefulWidget { const RegistrationScreen({ @@ -16,7 +14,7 @@ class RegistrationScreen extends StatefulWidget { final UserService userService; final RegistrationOptions registrationOptions; - final Future Function(String?) onError; + final Future Function(AuthException authException) onError; final Future Function() afterRegistration; @override @@ -89,12 +87,10 @@ class _RegistrationScreenState extends State { } } - var registrationReponse = - await widget.userService.register(values: values); - - if (!registrationReponse.registrationSuccessful) { - var pageToReturn = await widget.onError - .call(registrationReponse.registrationError?.title); + try { + await widget.userService.register(values: values); + } on AuthException catch (e) { + var pageToReturn = await widget.onError.call(e); if (pageToReturn != null) { if (pageToReturn == _pageController.page!.toInt()) { @@ -107,9 +103,10 @@ class _RegistrationScreenState extends State { ); return true; } - } else { - await widget.afterRegistration.call(); } + + await widget.afterRegistration.call(); + return true; } return false; diff --git a/packages/flutter_user/lib/src/screens/registration_unsuccessfull.dart b/packages/flutter_user/lib/src/screens/registration_unsuccessfull.dart index b15c8b6..871c507 100644 --- a/packages/flutter_user/lib/src/screens/registration_unsuccessfull.dart +++ b/packages/flutter_user/lib/src/screens/registration_unsuccessfull.dart @@ -1,4 +1,5 @@ import "package:flutter/material.dart"; +import "package:flutter_user/src/models/auth_error_details.dart"; import "package:flutter_user/src/models/registration/registration_options.dart"; import "package:flutter_user/src/widgets/primary_button.dart"; @@ -7,7 +8,7 @@ class RegistrationUnsuccessfull extends StatelessWidget { /// Registration Unsuccessfull Screen constructor const RegistrationUnsuccessfull({ required this.onPressed, - required this.error, + required this.errorDetails, required this.registrationOptions, super.key, }); @@ -17,61 +18,53 @@ class RegistrationUnsuccessfull extends StatelessWidget { final RegistrationOptions registrationOptions; - /// Error message - final String error; + /// Error details + final AuthErrorDetails errorDetails; @override - Widget build(BuildContext context) { - var isEmailError = error.contains("email-already-in-use"); - return Scaffold( - body: Stack( - children: [ - Align( - alignment: Alignment.center, - child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: registrationOptions.maxFormWidth, - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - registrationOptions - .translations.registrationUnsuccessfullTitle, - style: Theme.of(context).textTheme.headlineLarge, - textAlign: TextAlign.center, - ), - const SizedBox(height: 20), - Text( - isEmailError - ? registrationOptions.translations - .registrationEmailUnsuccessfullDescription - : registrationOptions.translations - .registrationPasswordUnsuccessfullDescription, - style: Theme.of(context).textTheme.bodyMedium, - textAlign: TextAlign.center, - ), - ], + Widget build(BuildContext context) => Scaffold( + body: Stack( + children: [ + Align( + alignment: Alignment.center, + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: registrationOptions.maxFormWidth, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + errorDetails.title, + style: Theme.of(context).textTheme.headlineLarge, + textAlign: TextAlign.center, + ), + const SizedBox(height: 20), + Text( + errorDetails.message, + style: Theme.of(context).textTheme.bodyMedium, + textAlign: TextAlign.center, + ), + ], + ), ), ), - ), - Align( - alignment: Alignment.bottomCenter, - child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: registrationOptions.maxFormWidth, - ), - child: SafeArea( - bottom: true, - child: PrimaryButton( - buttonTitle: registrationOptions - .translations.registrationUnsuccessButtonTitle, - onPressed: onPressed, + Align( + alignment: Alignment.bottomCenter, + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: registrationOptions.maxFormWidth, + ), + child: SafeArea( + bottom: true, + child: PrimaryButton( + buttonTitle: registrationOptions + .translations.registrationUnsuccessButtonTitle, + onPressed: onPressed, + ), ), ), ), - ), - ], - ), - ); - } + ], + ), + ); } diff --git a/packages/flutter_user/lib/src/widgets/error_scaffold_messenger.dart b/packages/flutter_user/lib/src/widgets/error_scaffold_messenger.dart index 01cd6a8..0e62e5d 100644 --- a/packages/flutter_user/lib/src/widgets/error_scaffold_messenger.dart +++ b/packages/flutter_user/lib/src/widgets/error_scaffold_messenger.dart @@ -1,10 +1,10 @@ import "package:flutter/material.dart"; -import "package:user_repository_interface/user_repository_interface.dart"; +import "package:flutter_user/src/models/auth_error_details.dart"; /// Show a [SnackBar] with the error message from the [LoginResponse]. Future errorScaffoldMessenger( BuildContext context, - LoginResponse result, + AuthErrorDetails errorDetails, ) async { var theme = Theme.of(context); ScaffoldMessenger.of(context).showSnackBar( @@ -14,12 +14,12 @@ Future errorScaffoldMessenger( mainAxisSize: MainAxisSize.min, children: [ Text( - result.loginError!.title, + errorDetails.title, style: theme.textTheme.titleMedium, ), const SizedBox(height: 8), Text( - result.loginError!.message, + errorDetails.message, style: theme.textTheme.bodyMedium, textAlign: TextAlign.center, ), diff --git a/packages/flutter_user/pubspec.yaml b/packages/flutter_user/pubspec.yaml index 91a8dee..17e1247 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.3.2 +version: 6.4.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.3.2 + version: ^6.4.0 flutter_accessibility: hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub version: ^0.0.3 diff --git a/packages/user_repository_interface/lib/src/interfaces/user_repository_interface.dart b/packages/user_repository_interface/lib/src/interfaces/user_repository_interface.dart index ba49223..350e86c 100644 --- a/packages/user_repository_interface/lib/src/interfaces/user_repository_interface.dart +++ b/packages/user_repository_interface/lib/src/interfaces/user_repository_interface.dart @@ -1,9 +1,8 @@ -import "package:user_repository_interface/src/models/login_response.dart"; -import "package:user_repository_interface/src/models/registration_reponse.dart"; +import "package:user_repository_interface/src/models/auth_response.dart"; import "package:user_repository_interface/src/models/request_forgot_password_response.dart"; abstract class UserRepositoryInterface { - Future loginWithEmailAndPassword({ + Future loginWithEmailAndPassword({ required String email, required String password, }); @@ -12,7 +11,7 @@ abstract class UserRepositoryInterface { required String email, }); - Future register({ + Future register({ required Map values, }); diff --git a/packages/user_repository_interface/lib/src/local/local_user_repository.dart b/packages/user_repository_interface/lib/src/local/local_user_repository.dart index 43f7608..2162da8 100644 --- a/packages/user_repository_interface/lib/src/local/local_user_repository.dart +++ b/packages/user_repository_interface/lib/src/local/local_user_repository.dart @@ -7,11 +7,10 @@ class LocalUserRepository implements UserRepositoryInterface { dynamic userObject; @override - Future register({ + Future register({ required Map values, }) async => - RegistrationResponse( - registrationSuccessful: true, + AuthResponse( userObject: userObject, ); @@ -24,7 +23,7 @@ class LocalUserRepository implements UserRepositoryInterface { ); @override - Future loginWithEmailAndPassword({ + Future loginWithEmailAndPassword({ required String email, required String password, }) async { @@ -34,8 +33,7 @@ class LocalUserRepository implements UserRepositoryInterface { "email": email, "password": password, }; - return LoginResponse( - loginSuccessful: true, + return AuthResponse( userObject: userObject, ); } diff --git a/packages/user_repository_interface/lib/src/models/auth_exceptions.dart b/packages/user_repository_interface/lib/src/models/auth_exceptions.dart new file mode 100644 index 0000000..44f70b7 --- /dev/null +++ b/packages/user_repository_interface/lib/src/models/auth_exceptions.dart @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2025 Iconica +// +// SPDX-License-Identifier: BSD-3-Clause + +abstract class AuthException implements Exception { + const AuthException({ + this.code, + this.message, + }); + final String? code; + final String? message; + + @override + String toString() => "$code: $message"; +} + +class InvalidEmailError extends AuthException { + const InvalidEmailError({super.code, super.message}); +} + +class UserDisabledError extends AuthException { + const UserDisabledError({super.code, super.message}); +} + +class UserNotFoundError extends AuthException { + const UserNotFoundError({super.code, super.message}); +} + +class WrongPasswordError extends AuthException { + const WrongPasswordError({super.code, super.message}); +} + +class EmailAlreadyInUseError extends AuthException { + const EmailAlreadyInUseError({super.code, super.message}); +} + +class OperationNotAllowedError extends AuthException { + const OperationNotAllowedError({super.code, super.message}); +} + +class WeakPasswordError extends AuthException { + const WeakPasswordError({super.code, super.message}); +} + +class TooManyRequestsError extends AuthException { + const TooManyRequestsError({super.code, super.message}); +} + +class NetworkError extends AuthException { + const NetworkError({super.code, super.message}); +} + +class InvalidCredentialError extends AuthException { + const InvalidCredentialError({super.code, super.message}); +} + +class AccountExistsWithDifferentCredentialError extends AuthException { + const AccountExistsWithDifferentCredentialError({super.code, super.message}); +} + +class InvalidVerificationCodeError extends AuthException { + const InvalidVerificationCodeError({super.code, super.message}); +} + +class InvalidVerificationIdError extends AuthException { + const InvalidVerificationIdError({super.code, super.message}); +} + +class RequiresRecentLoginError extends AuthException { + const RequiresRecentLoginError({super.code, super.message}); +} + +class GenericAuthError extends AuthException { + const GenericAuthError({super.code, super.message}); +} diff --git a/packages/user_repository_interface/lib/src/models/auth_response.dart b/packages/user_repository_interface/lib/src/models/auth_response.dart new file mode 100644 index 0000000..41be879 --- /dev/null +++ b/packages/user_repository_interface/lib/src/models/auth_response.dart @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2025 Iconica +// +// SPDX-License-Identifier: BSD-3-Clause + +/// A [AuthResponse] object is returned with the user object +class AuthResponse { + const AuthResponse({ + required this.userObject, + }); + final T? userObject; +} diff --git a/packages/user_repository_interface/lib/src/models/login_response.dart b/packages/user_repository_interface/lib/src/models/login_response.dart deleted file mode 100644 index 453c62e..0000000 --- a/packages/user_repository_interface/lib/src/models/login_response.dart +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause - -import "package:user_repository_interface/user_repository_interface.dart"; - -/// A [LoginResponse] object is returned from a login attempt. -/// If the login was successful, [loginSuccessful] will be true and -/// [userObject] will contain the user object. If the login was not -/// successful, [loginSuccessful] will be false and [loginError] will -/// contain an [Error] object with the error title and message. -class LoginResponse { - const LoginResponse({ - required this.loginSuccessful, - required this.userObject, - this.loginError, - }); - final bool loginSuccessful; - - final T? userObject; - final UserError? loginError; -} diff --git a/packages/user_repository_interface/lib/src/models/registration_reponse.dart b/packages/user_repository_interface/lib/src/models/registration_reponse.dart deleted file mode 100644 index 70ae7ab..0000000 --- a/packages/user_repository_interface/lib/src/models/registration_reponse.dart +++ /dev/null @@ -1,13 +0,0 @@ -import "package:user_repository_interface/user_repository_interface.dart"; - -class RegistrationResponse { - const RegistrationResponse({ - required this.registrationSuccessful, - required this.userObject, - this.registrationError, - }); - final bool registrationSuccessful; - - final T? userObject; - final UserError? registrationError; -} diff --git a/packages/user_repository_interface/lib/src/models/request_forgot_password_response.dart b/packages/user_repository_interface/lib/src/models/request_forgot_password_response.dart index 363b8fb..da43869 100644 --- a/packages/user_repository_interface/lib/src/models/request_forgot_password_response.dart +++ b/packages/user_repository_interface/lib/src/models/request_forgot_password_response.dart @@ -1,16 +1,9 @@ -import "package:user_repository_interface/user_repository_interface.dart"; - /// A [RequestPasswordResponse] object is returned from a password /// reset request. If the request was successful, [requestSuccesfull] -/// will be true. If the request was not successful, [requestSuccesfull] -/// will be false and [requestPasswordError] will contain an [Error] -/// object with the error title and message. +/// will be true. class RequestPasswordResponse { const RequestPasswordResponse({ required this.requestSuccesfull, - this.requestPasswordError, }); final bool requestSuccesfull; - - final UserError? requestPasswordError; } diff --git a/packages/user_repository_interface/lib/src/services/user_service.dart b/packages/user_repository_interface/lib/src/services/user_service.dart index ec1914c..c126d28 100644 --- a/packages/user_repository_interface/lib/src/services/user_service.dart +++ b/packages/user_repository_interface/lib/src/services/user_service.dart @@ -1,8 +1,4 @@ -import "package:user_repository_interface/src/interfaces/user_repository_interface.dart"; -import "package:user_repository_interface/src/local/local_user_repository.dart"; -import "package:user_repository_interface/src/models/login_response.dart"; -import "package:user_repository_interface/src/models/registration_reponse.dart"; -import "package:user_repository_interface/src/models/request_forgot_password_response.dart"; +import "package:user_repository_interface/user_repository_interface.dart"; class UserService { UserService({ @@ -11,7 +7,7 @@ class UserService { final UserRepositoryInterface userRepository; - Future loginWithEmailAndPassword({ + Future loginWithEmailAndPassword({ required String email, required String password, }) => @@ -25,7 +21,7 @@ class UserService { }) => userRepository.requestChangePassword(email: email); - Future register({ + Future register({ required Map values, }) => userRepository.register(values: values); diff --git a/packages/user_repository_interface/lib/user_repository_interface.dart b/packages/user_repository_interface/lib/user_repository_interface.dart index 9f8e64a..c62e8ce 100644 --- a/packages/user_repository_interface/lib/user_repository_interface.dart +++ b/packages/user_repository_interface/lib/user_repository_interface.dart @@ -3,8 +3,8 @@ library user_repository_interface; export "src/interfaces/user_repository_interface.dart"; export "src/local/local_user_repository.dart"; +export "src/models/auth_exceptions.dart"; +export "src/models/auth_response.dart"; export "src/models/error.dart"; -export "src/models/login_response.dart"; -export "src/models/registration_reponse.dart"; export "src/models/request_forgot_password_response.dart"; export "src/services/user_service.dart"; diff --git a/packages/user_repository_interface/pubspec.yaml b/packages/user_repository_interface/pubspec.yaml index 0ed5832..14b9f77 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.3.2 +version: 6.4.0 repository: https://github.com/Iconica-Development/flutter_user publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub