diff --git a/packages/deriv_auth_ui/README.md b/packages/deriv_auth_ui/README.md index 504ab5166..c98babcc0 100644 --- a/packages/deriv_auth_ui/README.md +++ b/packages/deriv_auth_ui/README.md @@ -47,6 +47,63 @@ MaterialApp( ) ``` +## AuthErrorHandler + +Since `DerivAuthCubit` influences many features like - login, signup, change password, etc. To be ease the error handling by making it needed to implement in only one place and to make sure all the auth error cases has been handled we have created a base class which client app can extend if they want changes in the default error handling. + +```dart +base class AuthErrorStateHandler { + /// {@macro default_auth_error_state_handler} + AuthErrorStateHandler({ + required this.context, + }); + + /// The [BuildContext] of the widget that is using this handler. + final BuildContext context; + + /// On invalid 2FA code. + void invalid2faCode(DerivAuthErrorState state) { + showErrorDialog( + context: context, + errorMessage: context.localization.informInvalid2FACode, + actionLabel: context.localization.actionTryAgain, + ); + } + + /// On invalid email or password. + void invalidEmailOrPassword(DerivAuthErrorState state) { + //.... + } + + // ... +} +``` +If client app wants to customize the error handling they can extend the `AuthErrorStateHandler` and override the methods they want to customize. + +```dart + +final class CustomAuthErrorStateHandler extends AuthErrorStateHandler { + CustomAuthErrorStateHandler({ + required BuildContext context, + }) : super(context: context); + + @override + void invalid2faCode(DerivAuthErrorState state) { + //... + } +} +``` +The client app can pass the custom error handler to the layout's constructor. + +```dart +DerivLoginLayout( + // ... + authErrorStateHandler: CustomAuthErrorStateHandler(context: context), + // ... +) +``` +The package handles the mapping of the error state to the corresponding method in the `AuthErrorStateHandler` class within the layout. + ## Layouts provided: ### - Get Started Flow @@ -60,6 +117,9 @@ MaterialApp( backgroundImagePath: backgroundImagePath, onLoginTapped: () {}, onSignupTapped: () {}, + onTapNavigation: (){ + // Callback to be called when pressed on the screen seven times + }, ); ``` ### - Login Flow @@ -69,6 +129,7 @@ MaterialApp( welcomeLabel: 'Welcome back!', greetingLabel: 'Log in to your Deriv account to start trading and investing.', + authErrorStateHandler: AuthErrorStateHandler(context: context), onResetPassTapped: () { // Navigate to reset password page }, @@ -99,6 +160,7 @@ MaterialApp( ``` dart DerivSignupLayout( signupPageLabel: 'Start trading with Deriv', + authErrorStateHandler: AuthErrorStateHandler(context: context), signupPageDescription: 'Join over 1 million traders worldwide who loves trading at Deriv.', onSocialAuthButtonPressed: (SocialAuthProvider provider) {}, @@ -140,6 +202,7 @@ MaterialApp( ``` dart DerivSetPasswordLayout( onDerivAuthState: (BuildContext, DerivAuthState) {}, + authErrorStateHandler: AuthErrorStateHandler(context: context), onDerivSignupState: (BuildContext, DerivSignupState) {}, onPreviousPressed: () {}, verificationCode: '123456', @@ -164,3 +227,72 @@ MaterialApp( ), ``` +- **Reset Password Success Layout** + ``` dart + DerivSuccessPassChangeLayout(); + ``` + + +## Additional: + +### AuthListener + +`AuthListener` is a widget that listens to the `DerivAuthCubit` state and calls the corresponding callback. This widget is created for ease of using `AuthErrorStateHandler` by handling the mapping of the error state to the corresponding method in the `AuthErrorStateHandler` class. + + +```dart +class DerivAuthStateListener extends StatelessWidget { + /// {@macro auth_state_listener} + const DerivAuthStateListener({ + required this.child, + super.key, + this.onLoggedIn, + this.onLoggedOut, + this.onLoading, + this.onError, + this.authErrorStateHandler, + }); + + /// The [Widget] that is using this [DerivAuthStateListener]. + final Widget child; + + /// Callback to be called when user is logged in. + final Function(DerivAuthLoggedInState)? onLoggedIn; + + /// Callback to be called when user is logged out. + final VoidCallback? onLoggedOut; + + /// Callback to be called when user is logging in. + final VoidCallback? onLoading; + + /// Callback to be called when an error occurs. + final Function(DerivAuthErrorState)? onError; + + /// Extension of base [AuthErrorStateHandler]. If not provided, base implementation will be used. + final AuthErrorStateHandler? authErrorStateHandler; + + @override + Widget build(BuildContext context) => + BlocListener( + listener: (BuildContext context, DerivAuthState state) { + if (state is DerivAuthLoggedInState) { + onLoggedIn?.call(state); + } else if (state is DerivAuthLoggedOutState) { + onLoggedOut?.call(); + } else if (state is DerivAuthLoadingState) { + onLoading?.call(); + } else if (state is DerivAuthErrorState) { + onError?.call(state); + + authErrorStateMapper( + authErrorState: state, + authErrorStateHandler: authErrorStateHandler ?? + AuthErrorStateHandler(context: context), + ); + } + }, + child: child, + ); +} + +``` \ No newline at end of file diff --git a/packages/deriv_auth_ui/analysis_options.yaml b/packages/deriv_auth_ui/analysis_options.yaml index 3d0c05ee4..2681030b5 100644 --- a/packages/deriv_auth_ui/analysis_options.yaml +++ b/packages/deriv_auth_ui/analysis_options.yaml @@ -58,12 +58,10 @@ linter: - flutter_style_todos - hash_and_equals - implementation_imports - - iterable_contains_unrelated_type + - collection_methods_unrelated_type - join_return_with_assignment - library_names - library_prefixes - # - lines_longer_than_80_chars - - list_remove_unrelated_type - no_adjacent_strings_in_list - no_duplicate_case_values - no_leading_underscores_for_local_identifiers diff --git a/packages/deriv_auth_ui/example/lib/core/example_auth_error_state_handler.dart b/packages/deriv_auth_ui/example/lib/core/example_auth_error_state_handler.dart new file mode 100644 index 000000000..478000d8e --- /dev/null +++ b/packages/deriv_auth_ui/example/lib/core/example_auth_error_state_handler.dart @@ -0,0 +1,16 @@ +import 'package:deriv_auth/deriv_auth.dart'; +import 'package:deriv_auth_ui/deriv_auth_ui.dart'; +import 'package:flutter/material.dart'; + +final class ExampleAuthErrorStateHandler extends AuthErrorStateHandler { + ExampleAuthErrorStateHandler({required super.context}); + + @override + void onInvalidCredentials(DerivAuthErrorState state) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.message), + ), + ); + } +} diff --git a/packages/deriv_auth_ui/example/lib/features/get_started/pages/get_started_page.dart b/packages/deriv_auth_ui/example/lib/features/get_started/pages/get_started_page.dart index d4ea64fe0..a89b5e67f 100644 --- a/packages/deriv_auth_ui/example/lib/features/get_started/pages/get_started_page.dart +++ b/packages/deriv_auth_ui/example/lib/features/get_started/pages/get_started_page.dart @@ -30,5 +30,6 @@ class GetStartedPage extends StatelessWidget { builder: (context) => const SignupPage(), ), ), + onTapNavigation: () {}, ); } diff --git a/packages/deriv_auth_ui/example/lib/features/login/pages/login_page.dart b/packages/deriv_auth_ui/example/lib/features/login/pages/login_page.dart index 7d1e66a2a..2c6ad3518 100644 --- a/packages/deriv_auth_ui/example/lib/features/login/pages/login_page.dart +++ b/packages/deriv_auth_ui/example/lib/features/login/pages/login_page.dart @@ -1,5 +1,6 @@ import 'package:deriv_auth/features/auth/cubit/deriv_auth_cubit.dart'; import 'package:deriv_auth_ui/deriv_auth_ui.dart'; +import 'package:example/core/example_auth_error_state_handler.dart'; import 'package:example/features/home/pages/home_page.dart'; import 'package:example/features/reset_pass/pages/reset_pass_page.dart'; import 'package:example/features/signup/pages/signup_page.dart'; @@ -32,6 +33,7 @@ class _LoginPageState extends State { builder: (context) => const HomePage(), ), ), + authErrorStateHandler: ExampleAuthErrorStateHandler(context: context), onLoginError: (_) {}, onResetPassTapped: () => Navigator.push( context, diff --git a/packages/deriv_auth_ui/example/lib/features/reset_pass/pages/choose_new_password_page.dart b/packages/deriv_auth_ui/example/lib/features/reset_pass/pages/choose_new_password_page.dart index 6ce9f1198..49dbbd6c9 100644 --- a/packages/deriv_auth_ui/example/lib/features/reset_pass/pages/choose_new_password_page.dart +++ b/packages/deriv_auth_ui/example/lib/features/reset_pass/pages/choose_new_password_page.dart @@ -1,6 +1,6 @@ import 'package:deriv_auth/features/reset_password/cubit/reset_password_cubit.dart'; import 'package:deriv_auth_ui/deriv_auth_ui.dart'; -import 'package:example/features/get_started/pages/get_started_page.dart'; +import 'package:example/features/reset_pass/pages/reset_pass_success_page.dart'; import 'package:flutter/material.dart'; class ChooseNewPasswordPage extends StatelessWidget { @@ -13,11 +13,10 @@ class ChooseNewPasswordPage extends StatelessWidget { return DerivChooseNewPassLayout( token: 'token', onResetPassSucceed: () { - Navigator.of(context).pushAndRemoveUntil( + Navigator.of(context).push( MaterialPageRoute( - builder: (context) => const GetStartedPage(), + builder: (context) => const ResetPassSuccessPage(), ), - (Route route) => false, ); }, onResetPassError: (_) {}, diff --git a/packages/deriv_auth_ui/example/lib/features/reset_pass/pages/reset_pass_success_page.dart b/packages/deriv_auth_ui/example/lib/features/reset_pass/pages/reset_pass_success_page.dart new file mode 100644 index 000000000..a0de6729d --- /dev/null +++ b/packages/deriv_auth_ui/example/lib/features/reset_pass/pages/reset_pass_success_page.dart @@ -0,0 +1,51 @@ +import 'package:deriv_auth/deriv_auth.dart'; +import 'package:deriv_auth_ui/deriv_auth_ui.dart'; +import 'package:example/features/get_started/pages/get_started_page.dart'; +import 'package:example/features/login/pages/login_page.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class ResetPassSuccessPage extends StatefulWidget { + const ResetPassSuccessPage({super.key}); + + @override + State createState() => _ResetPassSuccessPageState(); +} + +class _ResetPassSuccessPageState extends State { + static const Duration _successPageHoldDuration = Duration(seconds: 2); + + @override + void initState() { + super.initState(); + + // wait for either [_successPageHoldDuration] or logout to finish + // then navigate to loginPage + Future.wait( + >[ + Future.delayed(_successPageHoldDuration), + BlocProvider.of(context).logout(), + ], + ).then( + (_) { + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute( + builder: (context) => const GetStartedPage(), + ), + (route) => false, + ); + + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const LoginPage(), + ), + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return const DerivSuccessPassChangeLayout(); + } +} diff --git a/packages/deriv_auth_ui/example/lib/features/signup/pages/set_password_page.dart b/packages/deriv_auth_ui/example/lib/features/signup/pages/set_password_page.dart index e009e632e..32b6b7cfa 100644 --- a/packages/deriv_auth_ui/example/lib/features/signup/pages/set_password_page.dart +++ b/packages/deriv_auth_ui/example/lib/features/signup/pages/set_password_page.dart @@ -1,6 +1,6 @@ import 'package:deriv_auth/deriv_auth.dart'; import 'package:deriv_auth_ui/deriv_auth_ui.dart'; -import 'package:example/features/home/pages/home_page.dart'; +import 'package:example/core/example_auth_error_state_handler.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -12,16 +12,7 @@ class SetPasswordPage extends StatelessWidget { @override Widget build(BuildContext context) { return DerivSetPasswordLayout( - onDerivAuthState: (context, state) { - if (state is DerivAuthLoggedInState) { - Navigator.of(context).pushAndRemoveUntil( - MaterialPageRoute( - builder: (context) => const HomePage(), - ), - (Route route) => false, - ); - } - }, + authErrorStateHandler: ExampleAuthErrorStateHandler(context: context), onDerivSignupState: (context, state) { if (state is DerivSignupDoneState) { context diff --git a/packages/deriv_auth_ui/example/lib/features/signup/pages/signup_page.dart b/packages/deriv_auth_ui/example/lib/features/signup/pages/signup_page.dart index c4667ba7f..c275b0dc7 100644 --- a/packages/deriv_auth_ui/example/lib/features/signup/pages/signup_page.dart +++ b/packages/deriv_auth_ui/example/lib/features/signup/pages/signup_page.dart @@ -1,4 +1,5 @@ import 'package:deriv_auth_ui/deriv_auth_ui.dart'; +import 'package:example/core/example_auth_error_state_handler.dart'; import 'package:example/features/login/pages/login_page.dart'; import 'package:example/features/signup/pages/verify_email_page.dart'; import 'package:flutter/material.dart'; @@ -13,6 +14,7 @@ class SignupPage extends StatefulWidget { class _SignupPageState extends State { @override Widget build(BuildContext context) => DerivSignupLayout( + authErrorStateHandler: ExampleAuthErrorStateHandler(context: context), signupPageLabel: 'Start trading with Deriv', signupPageDescription: 'Join over 1 million traders worldwide who loves trading at Deriv.', diff --git a/packages/deriv_auth_ui/example/pubspec.yaml b/packages/deriv_auth_ui/example/pubspec.yaml index 4c200936c..d495c6853 100644 --- a/packages/deriv_auth_ui/example/pubspec.yaml +++ b/packages/deriv_auth_ui/example/pubspec.yaml @@ -16,15 +16,15 @@ dependencies: path: ../ deriv_auth: git: - url: git@github.com:regentmarkets/flutter-deriv-packages.git + url: git@github.com:sahani-deriv/flutter-deriv-packages.git path: packages/deriv_auth - ref: dev + ref: auth-ui-update + deriv_theme: - path: ../../deriv_theme - # git: - # url: git@github.com:regentmarkets/flutter-deriv-packages.git - # path: packages/deriv_theme - # ref: dev + git: + url: git@github.com:regentmarkets/flutter-deriv-packages.git + path: packages/deriv_theme + ref: dev flutter_bloc: ^8.1.3 http: ^0.13.6 diff --git a/packages/deriv_auth_ui/lib/deriv_auth_ui.dart b/packages/deriv_auth_ui/lib/deriv_auth_ui.dart index c03e33998..20c4c1f3e 100644 --- a/packages/deriv_auth_ui/lib/deriv_auth_ui.dart +++ b/packages/deriv_auth_ui/lib/deriv_auth_ui.dart @@ -5,6 +5,7 @@ export 'src/features/login/layouts/deriv_2fa_layout.dart'; export 'src/features/login/layouts/deriv_login_layout.dart'; export 'src/features/reset_pass/layouts/deriv_choose_new_pass_layout.dart'; export 'src/features/reset_pass/layouts/deriv_reset_pass_layout.dart'; +export 'src/features/reset_pass/layouts/deriv_success_pass_change_layout.dart'; export 'src/features/signup/cubits/deriv_country_selection_cubit.dart'; export 'src/features/signup/layouts/deriv_country_selection_layout.dart'; export 'src/features/signup/layouts/deriv_email_not_received_layout.dart'; @@ -15,3 +16,4 @@ export 'src/features/signup/layouts/deriv_verify_email_layout.dart'; export 'src/features/signup/models/deriv_auth_utm_model.dart'; export 'src/features/signup/models/deriv_password_policy_model.dart'; export 'src/features/signup/models/deriv_residence_model.dart'; +export 'src/core/states/states.dart'; diff --git a/packages/deriv_auth_ui/lib/generated/intl/messages_en.dart b/packages/deriv_auth_ui/lib/generated/intl/messages_en.dart index 927d0cfc3..c45e05536 100644 --- a/packages/deriv_auth_ui/lib/generated/intl/messages_en.dart +++ b/packages/deriv_auth_ui/lib/generated/intl/messages_en.dart @@ -43,6 +43,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Get a free account"), "actionLogin": MessageLookupByLibrary.simpleMessage("Log in"), "actionNext": MessageLookupByLibrary.simpleMessage("Next"), + "actionOk": MessageLookupByLibrary.simpleMessage("OK"), "actionPrevious": MessageLookupByLibrary.simpleMessage("Previous"), "actionProceed": MessageLookupByLibrary.simpleMessage("Proceed"), "actionReenterEmail": MessageLookupByLibrary.simpleMessage( @@ -51,20 +52,39 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Reset my password"), "actionStartTrading": MessageLookupByLibrary.simpleMessage("Start trading"), + "actionTryAgain": MessageLookupByLibrary.simpleMessage("Try Again"), "infoReferralInfoDescription": MessageLookupByLibrary.simpleMessage( "An alphanumeric code provided by a Deriv affiliate, applicable for email sign-ups only."), + "informConnectionError": MessageLookupByLibrary.simpleMessage( + "Connection error. Please try again later."), + "informDeactivatedAccount": MessageLookupByLibrary.simpleMessage( + "Your account is deactivated."), "informEnterTwoFactorAuthCode": MessageLookupByLibrary.simpleMessage( "Enter the 6-digit code from the authenticator app on your phone."), + "informExpiredAccount": + MessageLookupByLibrary.simpleMessage("Your account is expired"), + "informFailedAuthentication": MessageLookupByLibrary.simpleMessage( + "Your email or password may be incorrect. Did you sign up with a social account? Check and try again."), + "informFailedAuthorization": + MessageLookupByLibrary.simpleMessage("Authorization failed."), + "informInvalid2FACode": MessageLookupByLibrary.simpleMessage( + "The code you entered is invalid. Check and try again."), + "informInvalidCredentials": + MessageLookupByLibrary.simpleMessage("Invalid credentials."), "informInvalidEmailFormat": MessageLookupByLibrary.simpleMessage("Enter a valid email address"), "informInvalidPasswordFormat": MessageLookupByLibrary.simpleMessage( "Please enter a valid password format"), "informInvalidReferralCode": MessageLookupByLibrary.simpleMessage( "The referral code you entered is invalid. Check and try again."), + "informInvalidResidence": + MessageLookupByLibrary.simpleMessage("Invalid residence."), "informLetsContinue": MessageLookupByLibrary.simpleMessage("Let\'s continue."), "informLoginOptions": MessageLookupByLibrary.simpleMessage("Or log in with"), + "informMissingOtp": + MessageLookupByLibrary.simpleMessage("Missing one-time password."), "informPasswordPolicy": MessageLookupByLibrary.simpleMessage("Your password must have:"), "informPasswordPolicyLength": @@ -78,7 +98,15 @@ class MessageLookup extends MessageLookupByLibrary { "You’ll need to log in with your new password. Hang on, we’re redirecting you."), "informResetPassByEmail": MessageLookupByLibrary.simpleMessage( "We\'ll email you instructions to reset your password."), + "informSelfClosed": MessageLookupByLibrary.simpleMessage( + "Your account has been closed."), "informSendResetPasswordEmail": m0, + "informSwitchAccountError": MessageLookupByLibrary.simpleMessage( + "Switch account error. Please try again later."), + "informUnexpectedError": MessageLookupByLibrary.simpleMessage( + "An unexpected error occurred."), + "informUnsupportedCountry": MessageLookupByLibrary.simpleMessage( + "Your country is not supported."), "informVerificationEmailSent": m1, "informYourPassHasBeenReset": MessageLookupByLibrary.simpleMessage( "Your password has been reset"), @@ -88,6 +116,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Choose country"), "labelChooseNewPass": MessageLookupByLibrary.simpleMessage("Choose a new password"), + "labelCountryConsentBrazil": MessageLookupByLibrary.simpleMessage( + "I hereby confirm that my request for opening an account with Deriv to trade OTC products issued and offered exclusively outside Brazil was initiated by me. I fully understand that Deriv is not regulated by CVM and by approaching Deriv I intend to set up a relation with a foreign company."), "labelCreatePass": MessageLookupByLibrary.simpleMessage("Create a password"), "labelCreatePassword": diff --git a/packages/deriv_auth_ui/lib/generated/l10n.dart b/packages/deriv_auth_ui/lib/generated/l10n.dart index 7d4ef2308..9426718ce 100644 --- a/packages/deriv_auth_ui/lib/generated/l10n.dart +++ b/packages/deriv_auth_ui/lib/generated/l10n.dart @@ -71,6 +71,16 @@ class DerivAuthUILocalization { ); } + /// `OK` + String get actionOk { + return Intl.message( + 'OK', + name: 'actionOk', + desc: '', + args: [], + ); + } + /// `If you have any questions, contact us via ` String get warnNotAvailableCountries { return Intl.message( @@ -690,6 +700,156 @@ class DerivAuthUILocalization { args: [], ); } + + /// `Try Again` + String get actionTryAgain { + return Intl.message( + 'Try Again', + name: 'actionTryAgain', + desc: '', + args: [], + ); + } + + /// `The code you entered is invalid. Check and try again.` + String get informInvalid2FACode { + return Intl.message( + 'The code you entered is invalid. Check and try again.', + name: 'informInvalid2FACode', + desc: '', + args: [], + ); + } + + /// `Your email or password may be incorrect. Did you sign up with a social account? Check and try again.` + String get informFailedAuthentication { + return Intl.message( + 'Your email or password may be incorrect. Did you sign up with a social account? Check and try again.', + name: 'informFailedAuthentication', + desc: '', + args: [], + ); + } + + /// `Your account is deactivated.` + String get informDeactivatedAccount { + return Intl.message( + 'Your account is deactivated.', + name: 'informDeactivatedAccount', + desc: '', + args: [], + ); + } + + /// `Authorization failed.` + String get informFailedAuthorization { + return Intl.message( + 'Authorization failed.', + name: 'informFailedAuthorization', + desc: '', + args: [], + ); + } + + /// `Invalid residence.` + String get informInvalidResidence { + return Intl.message( + 'Invalid residence.', + name: 'informInvalidResidence', + desc: '', + args: [], + ); + } + + /// `Invalid credentials.` + String get informInvalidCredentials { + return Intl.message( + 'Invalid credentials.', + name: 'informInvalidCredentials', + desc: '', + args: [], + ); + } + + /// `Missing one-time password.` + String get informMissingOtp { + return Intl.message( + 'Missing one-time password.', + name: 'informMissingOtp', + desc: '', + args: [], + ); + } + + /// `Your account has been closed.` + String get informSelfClosed { + return Intl.message( + 'Your account has been closed.', + name: 'informSelfClosed', + desc: '', + args: [], + ); + } + + /// `An unexpected error occurred.` + String get informUnexpectedError { + return Intl.message( + 'An unexpected error occurred.', + name: 'informUnexpectedError', + desc: '', + args: [], + ); + } + + /// `Your country is not supported.` + String get informUnsupportedCountry { + return Intl.message( + 'Your country is not supported.', + name: 'informUnsupportedCountry', + desc: '', + args: [], + ); + } + + /// `Your account is expired` + String get informExpiredAccount { + return Intl.message( + 'Your account is expired', + name: 'informExpiredAccount', + desc: '', + args: [], + ); + } + + /// `I hereby confirm that my request for opening an account with Deriv to trade OTC products issued and offered exclusively outside Brazil was initiated by me. I fully understand that Deriv is not regulated by CVM and by approaching Deriv I intend to set up a relation with a foreign company.` + String get labelCountryConsentBrazil { + return Intl.message( + 'I hereby confirm that my request for opening an account with Deriv to trade OTC products issued and offered exclusively outside Brazil was initiated by me. I fully understand that Deriv is not regulated by CVM and by approaching Deriv I intend to set up a relation with a foreign company.', + name: 'labelCountryConsentBrazil', + desc: '', + args: [], + ); + } + + /// `Connection error. Please try again later.` + String get informConnectionError { + return Intl.message( + 'Connection error. Please try again later.', + name: 'informConnectionError', + desc: '', + args: [], + ); + } + + /// `Switch account error. Please try again later.` + String get informSwitchAccountError { + return Intl.message( + 'Switch account error. Please try again later.', + name: 'informSwitchAccountError', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate diff --git a/packages/deriv_auth_ui/lib/l10n/intl_en.arb b/packages/deriv_auth_ui/lib/l10n/intl_en.arb index 337c4193e..bb220ebc7 100644 --- a/packages/deriv_auth_ui/lib/l10n/intl_en.arb +++ b/packages/deriv_auth_ui/lib/l10n/intl_en.arb @@ -10,6 +10,7 @@ } } }, + "actionOk": "OK", "warnNotAvailableCountries": "If you have any questions, contact us via ", "labelLiveChat": "Live chat", "actionGetAFreeAccount": "Get a free account", @@ -89,5 +90,20 @@ "informPasswordPolicyLowerAndUpper": "Upper and lower case letters", "informPasswordPolicyNumber": "At least one number", "warnPasswordContainsSymbol": "Use symbols for strong password.", - "labelReferralCode": "Referral Code" -} \ No newline at end of file + "labelReferralCode": "Referral Code", + "actionTryAgain": "Try Again", + "informInvalid2FACode": "The code you entered is invalid. Check and try again.", + "informFailedAuthentication": "Your email or password may be incorrect. Did you sign up with a social account? Check and try again.", + "informDeactivatedAccount": "Your account is deactivated.", + "informFailedAuthorization": "Authorization failed.", + "informInvalidResidence": "Invalid residence.", + "informInvalidCredentials": "Invalid credentials.", + "informMissingOtp": "Missing one-time password.", + "informSelfClosed": "Your account has been closed.", + "informUnexpectedError": "An unexpected error occurred.", + "informUnsupportedCountry": "Your country is not supported.", + "informExpiredAccount": "Your account is expired", + "labelCountryConsentBrazil": "I hereby confirm that my request for opening an account with Deriv to trade OTC products issued and offered exclusively outside Brazil was initiated by me. I fully understand that Deriv is not regulated by CVM and by approaching Deriv I intend to set up a relation with a foreign company.", + "informConnectionError": "Connection error. Please try again later.", + "informSwitchAccountError": "Switch account error. Please try again later." +} diff --git a/packages/deriv_auth_ui/lib/src/core/helpers/country_selection_helper.dart b/packages/deriv_auth_ui/lib/src/core/helpers/country_selection_helper.dart new file mode 100644 index 000000000..5af80cb6a --- /dev/null +++ b/packages/deriv_auth_ui/lib/src/core/helpers/country_selection_helper.dart @@ -0,0 +1,86 @@ +import 'package:deriv_auth_ui/src/core/extensions/context_extension.dart'; +import 'package:flutter/material.dart'; + +/// Returns `true` if the country is not in the [notAllowedCountryCodes] set, +/// otherwise returns `false`. +bool isAllowedCountry({required String? countryCode}) => + !notAllowedCountryCodes.contains(countryCode); + +/// Returns `true` if consent is required for the given [countryCode], +/// otherwise returns `false`. +bool isConsentRequired({required String? countryCode}) { + if (countryCode == null) { + return false; + } + + return countriesRequiringConsent.contains(countryCode.toLowerCase()); +} + +/// Retrieves the consent message for a given country code. +/// +/// If the country requires consent, the corresponding consent message is returned. +/// Otherwise, an empty string is returned. +/// +/// When a new country is added to the set of [countriesRequiringConsent], +/// update the [consentMessages] map. +String getCountryConsentMessage( + BuildContext context, { + required String? countryCode, +}) { + if (countryCode == null) { + return ''; + } + + final Map consentMessages = { + 'br': context.localization.labelCountryConsentBrazil, + // Add more countries and consent messages here. + }; + + return consentMessages[countryCode.toLowerCase()] ?? ''; +} + +/// Set of country codes that are not allowed to create an account. +const Set notAllowedCountryCodes = { + // MF country codes. + 'de', + 'es', + 'fr', + 'gr', + 'it', + 'lu', + 'mf', + // MLT country codes. + 'at', + 'be', + 'bg', + 'cy', + 'cz', + 'dk', + 'ee', + 'fi', + 'hr', + 'hu', + 'ie', + 'lt', + 'lv', + 'nl', + 'pl', + 'pt', + 'ro', + 'se', + 'si', + 'sk', + // MX country codes. + 'gb', + 'im', +}; + +/// [countriesRequiringConsent] is a set of country codes that are required +/// to show consent. +/// +/// If any country is required to show consent, add the country code +/// in lowercase to the set. +const Set countriesRequiringConsent = { + 'br', + // Add countries here. +}; diff --git a/packages/deriv_auth_ui/lib/src/core/states/auth_error_state_handler.dart b/packages/deriv_auth_ui/lib/src/core/states/auth_error_state_handler.dart new file mode 100644 index 000000000..f7daa9184 --- /dev/null +++ b/packages/deriv_auth_ui/lib/src/core/states/auth_error_state_handler.dart @@ -0,0 +1,127 @@ +import 'package:deriv_auth/deriv_auth.dart'; +import 'package:deriv_auth_ui/src/core/extensions/context_extension.dart'; +import 'package:deriv_ui/deriv_ui.dart'; +import 'package:flutter/material.dart'; + +/// {@template default_auth_error_state_handler} +/// Base class for handling [DerivAuthErrorState]s. Client app can extend this +/// class and override the methods to handle the error states based on their +/// customization. +/// {@endtemplate} +base class AuthErrorStateHandler { + /// {@macro default_auth_error_state_handler} + AuthErrorStateHandler({ + required this.context, + }); + + /// The [BuildContext] of the widget that is using this handler. + final BuildContext context; + + /// On invalid 2FA code. + void invalid2faCode(DerivAuthErrorState state) { + showErrorDialog( + context: context, + errorMessage: context.localization.informInvalid2FACode, + actionLabel: context.localization.actionTryAgain, + ); + } + + /// On account is not activated. + void onAccountUnavailable(DerivAuthErrorState state) { + showErrorDialog( + context: context, + errorMessage: context.localization.informDeactivatedAccount, + actionLabel: context.localization.actionTryAgain, + ); + } + + /// On expired account. + void onExpiredAccount(DerivAuthErrorState state) { + showErrorDialog( + context: context, + errorMessage: context.localization.informExpiredAccount, + actionLabel: context.localization.actionTryAgain, + ); + } + + /// On failed authorization. + void onFailedAuthorization(DerivAuthErrorState state) { + showErrorDialog( + context: context, + errorMessage: context.localization.informFailedAuthorization, + actionLabel: context.localization.actionTryAgain, + ); + } + + /// User is trying to authenticate from an unsupported residence. + void onInavlidResidence(DerivAuthErrorState state) { + showErrorDialog( + context: context, + errorMessage: context.localization.informInvalidResidence, + actionLabel: context.localization.actionTryAgain, + ); + } + + /// On invalid credentials. + void onInvalidCredentials(DerivAuthErrorState state) { + showErrorDialog( + context: context, + errorMessage: context.localization.informInvalidCredentials, + actionLabel: context.localization.actionTryAgain, + ); + } + + /// User has set up 2FA and needs to enter 2FA. + void onMissingOtp(DerivAuthErrorState state) { + showErrorDialog( + context: context, + errorMessage: context.localization.informMissingOtp, + actionLabel: context.localization.actionTryAgain, + ); + } + + /// On self closed account. + void onSelfClosed(DerivAuthErrorState state) { + showErrorDialog( + context: context, + errorMessage: context.localization.informSelfClosed, + actionLabel: context.localization.actionTryAgain, + ); + } + + /// On unexpected error. + void onUnexpectedError(DerivAuthErrorState state) { + showErrorDialog( + context: context, + errorMessage: context.localization.informUnexpectedError, + actionLabel: context.localization.actionTryAgain, + ); + } + + /// Account is not supported in the country. + void onUnsupportedCountry(DerivAuthErrorState state) { + showErrorDialog( + context: context, + errorMessage: context.localization.informUnsupportedCountry, + actionLabel: context.localization.actionTryAgain, + ); + } + + /// On connection error. + void onConnectionError(DerivAuthErrorState state) { + showErrorDialog( + context: context, + errorMessage: context.localization.informConnectionError, + actionLabel: context.localization.actionTryAgain, + ); + } + + /// On switch account error. + void onSwitchAccountError(DerivAuthErrorState state) { + showErrorDialog( + context: context, + errorMessage: context.localization.informSwitchAccountError, + actionLabel: context.localization.actionTryAgain, + ); + } +} diff --git a/packages/deriv_auth_ui/lib/src/core/states/auth_error_state_mapper.dart b/packages/deriv_auth_ui/lib/src/core/states/auth_error_state_mapper.dart new file mode 100644 index 000000000..ba6fb739e --- /dev/null +++ b/packages/deriv_auth_ui/lib/src/core/states/auth_error_state_mapper.dart @@ -0,0 +1,51 @@ +import 'package:deriv_auth/deriv_auth.dart'; +import 'package:deriv_auth_ui/deriv_auth_ui.dart'; + +/// Maps the [DerivAuthErrorState] to the corresponding [AuthErrorStateHandler]. +void authErrorStateMapper({ + required DerivAuthErrorState authErrorState, + required AuthErrorStateHandler authErrorStateHandler, + List? ignoredExceptions, +}) { + if (ignoredExceptions?.contains(authErrorState.type) ?? false) { + return; + } + switch (authErrorState.type) { + case AuthErrorType.missingOtp: + authErrorStateHandler.onMissingOtp(authErrorState); + return; + case AuthErrorType.selfClosed: + authErrorStateHandler.onSelfClosed(authErrorState); + return; + case AuthErrorType.unsupportedCountry: + authErrorStateHandler.onUnsupportedCountry(authErrorState); + return; + case AuthErrorType.accountUnavailable: + authErrorStateHandler.onAccountUnavailable(authErrorState); + return; + case AuthErrorType.invalidCredential: + authErrorStateHandler.onInvalidCredentials(authErrorState); + return; + case AuthErrorType.invalid2faCode: + authErrorStateHandler.invalid2faCode(authErrorState); + return; + case AuthErrorType.failedAuthorization: + authErrorStateHandler.onFailedAuthorization(authErrorState); + return; + case AuthErrorType.invalidResidence: + authErrorStateHandler.onInavlidResidence(authErrorState); + return; + case AuthErrorType.expiredAccount: + authErrorStateHandler.onExpiredAccount(authErrorState); + return; + case AuthErrorType.connectionError: + authErrorStateHandler.onConnectionError(authErrorState); + return; + case AuthErrorType.switchAccountError: + authErrorStateHandler.onSwitchAccountError(authErrorState); + return; + default: + authErrorStateHandler.onUnexpectedError(authErrorState); + return; + } +} diff --git a/packages/deriv_auth_ui/lib/src/core/states/auth_state_listener.dart b/packages/deriv_auth_ui/lib/src/core/states/auth_state_listener.dart new file mode 100644 index 000000000..eee0ac6d6 --- /dev/null +++ b/packages/deriv_auth_ui/lib/src/core/states/auth_state_listener.dart @@ -0,0 +1,63 @@ +import 'package:deriv_auth/deriv_auth.dart'; +import 'package:deriv_auth_ui/deriv_auth_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +/// {@template auth_state_listener} +/// A [Widget] that listens to the [DerivAuthCubit] state changes. +/// This was created to make it easier to use [AuthErrorStateHandler] by +/// handling the mapping of auth error states with [AuthErrorStateHandler]'s method'. +/// {@endtemplate} +class DerivAuthStateListener extends StatelessWidget { + /// {@macro auth_state_listener} + const DerivAuthStateListener({ + required this.child, + super.key, + this.onLoggedIn, + this.onLoggedOut, + this.onLoading, + this.onError, + this.authErrorStateHandler, + }); + + /// The [Widget] that is using this [DerivAuthStateListener]. + final Widget child; + + /// Callback to be called when user is logged in. + final Function(DerivAuthLoggedInState)? onLoggedIn; + + /// Callback to be called when user is logged out. + final VoidCallback? onLoggedOut; + + /// Callback to be called when user is logging in. + final VoidCallback? onLoading; + + /// Callback to be called when an error occurs. + final Function(DerivAuthErrorState)? onError; + + /// Extension of base [AuthErrorStateHandler]. If not provided, base implementation will be used. + final AuthErrorStateHandler? authErrorStateHandler; + + @override + Widget build(BuildContext context) => + BlocListener( + listener: (BuildContext context, DerivAuthState state) { + if (state is DerivAuthLoggedInState) { + onLoggedIn?.call(state); + } else if (state is DerivAuthLoggedOutState) { + onLoggedOut?.call(); + } else if (state is DerivAuthLoadingState) { + onLoading?.call(); + } else if (state is DerivAuthErrorState) { + onError?.call(state); + + authErrorStateMapper( + authErrorState: state, + authErrorStateHandler: authErrorStateHandler ?? + AuthErrorStateHandler(context: context), + ); + } + }, + child: child, + ); +} diff --git a/packages/deriv_auth_ui/lib/src/core/states/states.dart b/packages/deriv_auth_ui/lib/src/core/states/states.dart new file mode 100644 index 000000000..7545c9d18 --- /dev/null +++ b/packages/deriv_auth_ui/lib/src/core/states/states.dart @@ -0,0 +1,2 @@ +export 'auth_error_state_mapper.dart'; +export 'auth_error_state_handler.dart'; diff --git a/packages/deriv_auth_ui/lib/src/features/get_started/layouts/deriv_get_started_layout.dart b/packages/deriv_auth_ui/lib/src/features/get_started/layouts/deriv_get_started_layout.dart index 160d26910..c074202e0 100644 --- a/packages/deriv_auth_ui/lib/src/features/get_started/layouts/deriv_get_started_layout.dart +++ b/packages/deriv_auth_ui/lib/src/features/get_started/layouts/deriv_get_started_layout.dart @@ -20,6 +20,7 @@ class DerivGetStartedLayout extends StatefulWidget { required this.backgroundImagePath, required this.onLoginTapped, required this.onSignupTapped, + required this.onTapNavigation, Key? key, }) : super(key: key); @@ -38,6 +39,9 @@ class DerivGetStartedLayout extends StatefulWidget { /// Callback to be called when signup button is tapped. final VoidCallback onSignupTapped; + /// Navigation to be called when screen is tapped seven times. + final VoidCallback onTapNavigation; + @override State createState() => _DerivGetStartedLayoutState(); } @@ -102,7 +106,9 @@ class _DerivGetStartedLayoutState extends State { PreferredSizeWidget _buildAppBar(BuildContext context) => AppBar( backgroundColor: context.theme.colors.secondary, centerTitle: false, - title: SvgPicture.asset(widget.appLogoIconPath), + title: AppSettingGestureDetector( + onTapNavigation: widget.onTapNavigation, + child: SvgPicture.asset(widget.appLogoIconPath)), ); Timer _buildNewScrollTimer() => Timer.periodic( @@ -221,7 +227,7 @@ class _DerivGetStartedLayoutState extends State { supportingText, style: context.theme.textStyle( textStyle: TextStyles.title, - color: context.theme.colors.lessProminent, + color: context.theme.colors.general, ), textAlign: TextAlign.center, ), diff --git a/packages/deriv_auth_ui/lib/src/features/login/layouts/deriv_2fa_layout.dart b/packages/deriv_auth_ui/lib/src/features/login/layouts/deriv_2fa_layout.dart index 00009622c..4634aaef3 100644 --- a/packages/deriv_auth_ui/lib/src/features/login/layouts/deriv_2fa_layout.dart +++ b/packages/deriv_auth_ui/lib/src/features/login/layouts/deriv_2fa_layout.dart @@ -63,7 +63,7 @@ class _Deriv2FALayoutState extends State { context.localization.informEnterTwoFactorAuthCode, style: context.theme.textStyle( textStyle: TextStyles.body1, - color: context.theme.colors.lessProminent, + color: context.theme.colors.general, ), textAlign: TextAlign.center, ), diff --git a/packages/deriv_auth_ui/lib/src/features/login/layouts/deriv_login_layout.dart b/packages/deriv_auth_ui/lib/src/features/login/layouts/deriv_login_layout.dart index 4f89a1763..0bdb7ae3d 100644 --- a/packages/deriv_auth_ui/lib/src/features/login/layouts/deriv_login_layout.dart +++ b/packages/deriv_auth_ui/lib/src/features/login/layouts/deriv_login_layout.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:deriv_auth/deriv_auth.dart'; +import 'package:deriv_auth_ui/deriv_auth_ui.dart'; import 'package:deriv_auth_ui/src/core/extensions/context_extension.dart'; import 'package:deriv_auth_ui/src/core/extensions/string_extension.dart'; import 'package:deriv_auth_ui/src/features/login/widgets/deriv_social_auth_divider.dart'; @@ -16,11 +17,13 @@ class DerivLoginLayout extends StatefulWidget { const DerivLoginLayout({ required this.onResetPassTapped, required this.onSignupTapped, - required this.onLoginError, required this.onLoggedIn, required this.onSocialAuthButtonPressed, required this.welcomeLabel, required this.greetingLabel, + this.isSocialAuthEnabled = true, + this.authErrorStateHandler, + this.onLoginError, this.onLoginTapped, Key? key, }) : super(key: key); @@ -31,8 +34,11 @@ class DerivLoginLayout extends StatefulWidget { /// Callback to be called when signup button is tapped. final VoidCallback onSignupTapped; - /// Callback to be called when login button is tapped. - final Function(DerivAuthErrorState) onLoginError; + /// Extension of base [AuthErrorStateHandler]. If not provided, base implementation will be used. + final AuthErrorStateHandler? authErrorStateHandler; + + /// Callback to be called when login error occurs. + final Function(DerivAuthErrorState)? onLoginError; /// Callback to be called when user is logged in. final Function(DerivAuthLoggedInState) onLoggedIn; @@ -49,6 +55,9 @@ class DerivLoginLayout extends StatefulWidget { /// Greeting text to be displayed on login page. final String greetingLabel; + /// Whether to display social auth buttons. + final bool isSocialAuthEnabled; + @override State createState() => _DerivLoginLayoutState(); } @@ -108,13 +117,17 @@ class _DerivLoginLayoutState extends State { const SizedBox(height: ThemeProvider.margin24), DerivSocialAuthDivider( label: context.localization.informLoginOptions, + isVisible: widget.isSocialAuthEnabled, ), - const SizedBox(height: ThemeProvider.margin24), + if (widget.isSocialAuthEnabled) + const SizedBox(height: ThemeProvider.margin24), DerivSocialAuthPanel( onSocialAuthButtonPressed: widget.onSocialAuthButtonPressed, + isVisible: widget.isSocialAuthEnabled, ), - const SizedBox(height: ThemeProvider.margin24), + if (widget.isSocialAuthEnabled) + const SizedBox(height: ThemeProvider.margin24), _buildFooterSection(), ], ), @@ -138,7 +151,7 @@ class _DerivLoginLayoutState extends State { widget.greetingLabel, style: context.theme.textStyle( textStyle: TextStyles.body1, - color: context.theme.colors.lessProminent, + color: context.theme.colors.general, ), ), ]; @@ -240,7 +253,7 @@ class _DerivLoginLayoutState extends State { context.localization.labelDontHaveAnAccountYet, style: context.theme.textStyle( textStyle: TextStyles.body1, - color: context.theme.colors.lessProminent, + color: context.theme.colors.general, ), ), InkWell( @@ -262,7 +275,13 @@ class _DerivLoginLayoutState extends State { void _onAuthState(BuildContext context, DerivAuthState state) { if (state is DerivAuthErrorState) { - widget.onLoginError.call(state); + widget.onLoginError?.call(state); + + authErrorStateMapper( + authErrorState: state, + authErrorStateHandler: widget.authErrorStateHandler ?? + AuthErrorStateHandler(context: context), + ); } if (state is DerivAuthLoggedInState) { diff --git a/packages/deriv_auth_ui/lib/src/features/login/widgets/deriv_social_auth_divider.dart b/packages/deriv_auth_ui/lib/src/features/login/widgets/deriv_social_auth_divider.dart index 64d14f557..1f9411517 100644 --- a/packages/deriv_auth_ui/lib/src/features/login/widgets/deriv_social_auth_divider.dart +++ b/packages/deriv_auth_ui/lib/src/features/login/widgets/deriv_social_auth_divider.dart @@ -6,29 +6,37 @@ class DerivSocialAuthDivider extends StatelessWidget { /// Initializes the class. const DerivSocialAuthDivider({ required this.label, + this.isVisible = true, Key? key, }) : super(key: key); /// The label that displayed in the divider. final String label; + /// Whether the buttons are visible. + /// Defaults to `true`. Acts as a flag to hide the buttons. + final bool isVisible; + @override - Widget build(BuildContext context) => Row( - children: [ - _buildDivider(context), - Padding( - padding: - const EdgeInsets.symmetric(horizontal: ThemeProvider.margin08), - child: Text( - label, - style: context.theme.textStyle( - textStyle: TextStyles.body1, - color: context.theme.colors.lessProminent, + Widget build(BuildContext context) => Visibility( + visible: isVisible, + child: Row( + children: [ + _buildDivider(context), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: ThemeProvider.margin08), + child: Text( + label, + style: context.theme.textStyle( + textStyle: TextStyles.body1, + color: context.theme.colors.general, + ), ), ), - ), - _buildDivider(context), - ], + _buildDivider(context), + ], + ), ); Widget _buildDivider(BuildContext context) => Expanded( diff --git a/packages/deriv_auth_ui/lib/src/features/login/widgets/deriv_social_auth_panel.dart b/packages/deriv_auth_ui/lib/src/features/login/widgets/deriv_social_auth_panel.dart index 91298a3d4..6100b7bbf 100644 --- a/packages/deriv_auth_ui/lib/src/features/login/widgets/deriv_social_auth_panel.dart +++ b/packages/deriv_auth_ui/lib/src/features/login/widgets/deriv_social_auth_panel.dart @@ -10,6 +10,7 @@ class DerivSocialAuthPanel extends StatelessWidget { const DerivSocialAuthPanel({ required this.onSocialAuthButtonPressed, this.isEnabled = true, + this.isVisible = true, Key? key, }) : super(key: key); @@ -19,19 +20,26 @@ class DerivSocialAuthPanel extends StatelessWidget { /// Defaults to `true`. final bool isEnabled; + /// Whether the buttons are visible. + /// Defaults to `true`. Acts as a flag to hide the buttons. + final bool isVisible; + /// onPressed callback for social auth buttons. final void Function(SocialAuthProvider) onSocialAuthButtonPressed; @override - Widget build(BuildContext context) => Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildSocialAuthButton(SocialAuthProvider.apple), - const SizedBox(width: ThemeProvider.margin24), - _buildSocialAuthButton(SocialAuthProvider.google), - const SizedBox(width: ThemeProvider.margin24), - _buildSocialAuthButton(SocialAuthProvider.facebook), - ], + Widget build(BuildContext context) => Visibility( + visible: isVisible, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildSocialAuthButton(SocialAuthProvider.apple), + const SizedBox(width: ThemeProvider.margin24), + _buildSocialAuthButton(SocialAuthProvider.google), + const SizedBox(width: ThemeProvider.margin24), + _buildSocialAuthButton(SocialAuthProvider.facebook), + ], + ), ); Widget _buildSocialAuthButton(SocialAuthProvider socialAuthProvider) => diff --git a/packages/deriv_auth_ui/lib/src/features/reset_pass/layouts/deriv_choose_new_pass_layout.dart b/packages/deriv_auth_ui/lib/src/features/reset_pass/layouts/deriv_choose_new_pass_layout.dart index 787a508d9..5785745f8 100644 --- a/packages/deriv_auth_ui/lib/src/features/reset_pass/layouts/deriv_choose_new_pass_layout.dart +++ b/packages/deriv_auth_ui/lib/src/features/reset_pass/layouts/deriv_choose_new_pass_layout.dart @@ -36,12 +36,9 @@ class DerivChooseNewPassLayout extends StatefulWidget { } class _DerivChooseNewPassLayoutState extends State { - static const Duration _successPageHoldDuration = Duration(seconds: 2); - final GlobalKey _formKey = GlobalKey(); final TextEditingController _passController = TextEditingController(); final FocusNode _passFocusNode = FocusNode(); - final PageController _pageController = PageController(); bool _isBusy = false; bool _isPasswordVisible = false; @@ -59,25 +56,12 @@ class _DerivChooseNewPassLayoutState extends State { body: BlocListener( listener: (BuildContext context, DerivResetPassState state) { if (state is DerivResetPassPasswordChangedState) { - _pageController.animateToPage( - 1, - duration: slidingPageChangeDuration, - curve: Curves.easeInOut, - ); - - Timer(_successPageHoldDuration, widget.onResetPassSucceed); + widget.onResetPassSucceed(); } else if (state is DerivResetPassErrorState) { widget.onResetPassError(state.errorMessage); } }, - child: PageView( - controller: _pageController, - physics: const NeverScrollableScrollPhysics(), - children: [ - _buildChooseNewPassSection(context), - _buildSuccessPassChangeSection(context) - ], - ), + child: _buildChooseNewPassSection(context), ), ); @@ -92,7 +76,7 @@ class _DerivChooseNewPassLayoutState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ const SizedBox(height: ThemeProvider.margin72), - Expanded(child: _buildContent()), + _buildContent(), const SizedBox(height: ThemeProvider.margin24), _buildSubmitPassButton() ], @@ -158,44 +142,6 @@ class _DerivChooseNewPassLayoutState extends State { ), ); - Widget _buildSuccessPassChangeSection(BuildContext context) => Center( - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: ThemeProvider.margin16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox( - height: ThemeProvider.margin16, - width: ThemeProvider.margin16, - child: LoadingIndicator( - valueColor: Colors.white, - strokeWidth: 2.5, - ), - ), - const SizedBox( - height: ThemeProvider.margin16, - ), - Text( - context.localization.informYourPassHasBeenReset, - style: TextStyles.title, - ), - const SizedBox( - height: ThemeProvider.margin08, - ), - Text( - context.localization.informRedirectLogin, - textAlign: TextAlign.center, - style: context.theme.textStyle( - textStyle: TextStyles.body1, - color: context.theme.colors.lessProminent, - ), - ), - ], - ), - ), - ); - Widget _buildSubmitPassButton() => ElevatedButton( style: ButtonStyle( backgroundColor: MaterialStateProperty.all( @@ -252,7 +198,6 @@ class _DerivChooseNewPassLayoutState extends State { @override void dispose() { - _pageController.dispose(); _passController.dispose(); _passFocusNode.dispose(); diff --git a/packages/deriv_auth_ui/lib/src/features/reset_pass/layouts/deriv_reset_pass_layout.dart b/packages/deriv_auth_ui/lib/src/features/reset_pass/layouts/deriv_reset_pass_layout.dart index 4614866aa..1182ead53 100644 --- a/packages/deriv_auth_ui/lib/src/features/reset_pass/layouts/deriv_reset_pass_layout.dart +++ b/packages/deriv_auth_ui/lib/src/features/reset_pass/layouts/deriv_reset_pass_layout.dart @@ -95,7 +95,7 @@ class _DerivResetPassLayoutState extends State { textAlign: TextAlign.center, style: context.theme.textStyle( textStyle: TextStyles.body1, - color: context.theme.colors.lessProminent, + color: context.theme.colors.general, ), ), const SizedBox(height: kToolbarHeight), @@ -129,7 +129,7 @@ class _DerivResetPassLayoutState extends State { context.localization.informResetPassByEmail, style: context.theme.textStyle( textStyle: TextStyles.body1, - color: context.theme.colors.lessProminent, + color: context.theme.colors.general, ), ), ), diff --git a/packages/deriv_auth_ui/lib/src/features/reset_pass/layouts/deriv_success_pass_change_layout.dart b/packages/deriv_auth_ui/lib/src/features/reset_pass/layouts/deriv_success_pass_change_layout.dart new file mode 100644 index 000000000..b0af4c580 --- /dev/null +++ b/packages/deriv_auth_ui/lib/src/features/reset_pass/layouts/deriv_success_pass_change_layout.dart @@ -0,0 +1,59 @@ +import 'package:deriv_auth_ui/src/core/extensions/context_extension.dart'; +import 'package:deriv_theme/deriv_theme.dart'; +import 'package:deriv_ui/presentation/widgets/loading_indicator.dart'; +import 'package:flutter/material.dart'; + +/// Success pass change page layout. +class DerivSuccessPassChangeLayout extends StatelessWidget { + /// Initializes success pass change page. + const DerivSuccessPassChangeLayout({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => Scaffold( + backgroundColor: context.theme.colors.primary, + appBar: AppBar( + elevation: ThemeProvider.zeroMargin, + title: Text( + context.localization.labelResetPassword, + style: TextStyles.title, + ), + ), + body: Center( + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: ThemeProvider.margin16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox( + height: ThemeProvider.margin16, + width: ThemeProvider.margin16, + child: LoadingIndicator( + valueColor: Colors.white, + strokeWidth: 2.5, + ), + ), + const SizedBox( + height: ThemeProvider.margin16, + ), + Text( + context.localization.informYourPassHasBeenReset, + style: TextStyles.title, + ), + const SizedBox( + height: ThemeProvider.margin08, + ), + Text( + context.localization.informRedirectLogin, + textAlign: TextAlign.center, + style: context.theme.textStyle( + textStyle: TextStyles.body1, + color: context.theme.colors.general, + ), + ), + ], + ), + ), + ), + ); +} diff --git a/packages/deriv_auth_ui/lib/src/features/signup/cubits/deriv_country_selection_cubit.dart b/packages/deriv_auth_ui/lib/src/features/signup/cubits/deriv_country_selection_cubit.dart index 4ce13603a..49a5d7a9a 100644 --- a/packages/deriv_auth_ui/lib/src/features/signup/cubits/deriv_country_selection_cubit.dart +++ b/packages/deriv_auth_ui/lib/src/features/signup/cubits/deriv_country_selection_cubit.dart @@ -1,3 +1,4 @@ +import 'package:deriv_auth_ui/src/core/helpers/country_selection_helper.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:deriv_auth_ui/src/features/signup/models/deriv_residence_model.dart'; @@ -14,53 +15,53 @@ class DerivCountrySelectionCubit extends Cubit { final Future> residences; /// Fetches residence countries. - Future fetchResidenceCounties() async { + Future fetchResidenceCountries() async { final List countries = await residences; final List filteredCountries = countries .where( - (DerivResidenceModel country) => _isAllowedCountry(country), + (DerivResidenceModel country) => + isAllowedCountry(countryCode: country.code), ) .toList(); emit(DerivCountrySelectionLoadedState(filteredCountries)); } - bool _isAllowedCountry(DerivResidenceModel country) => _notAllowedCountryCodes - .every((String countryCode) => country.code != countryCode); + /// Changes the selected country and updates the state accordingly. + /// + /// If [selectedCountry] is not `null`, this method emits a [DerivCountryChangedState] + /// with the updated selected country. + /// + /// This method does not perform any action if [selectedCountry] is `null`. + Future changeSelectedCountry({ + DerivResidenceModel? selectedCountry, + }) async { + if (selectedCountry != null) { + emit( + DerivCountryChangedState( + state.countries, + selectedCountry: selectedCountry, + selectedCountryRequiresConsent: isConsentRequired( + countryCode: selectedCountry.code, + ), + ), + ); + } + } - static final List _notAllowedCountryCodes = [ - // MF country codes. - 'de', - 'es', - 'fr', - 'gr', - 'it', - 'lu', - 'mf', - // MLT country codes. - 'at', - 'be', - 'bg', - 'cy', - 'cz', - 'dk', - 'ee', - 'fi', - 'hr', - 'hu', - 'ie', - 'lt', - 'lv', - 'nl', - 'pl', - 'pt', - 'ro', - 'se', - 'si', - 'sk', - // MX country codes. - 'gb', - 'im' - ]; + /// Updates the country consent status and triggers a state change. + /// + /// [agreedToTerms]: Whether the user has agreed to the terms for the + /// selected country. + Future updateCountryConsentStatus({bool? agreedToTerms = false}) async { + emit( + DerivCountryConsentChangedState( + state.countries, + selectedCountry: state.selectedCountry, + selectedCountryRequiresConsent: state.selectedCountryRequiresConsent, + agreedToTerms: agreedToTerms, + ), + ); + } } diff --git a/packages/deriv_auth_ui/lib/src/features/signup/cubits/deriv_country_selection_state.dart b/packages/deriv_auth_ui/lib/src/features/signup/cubits/deriv_country_selection_state.dart index 6f67179df..ed48f1844 100644 --- a/packages/deriv_auth_ui/lib/src/features/signup/cubits/deriv_country_selection_state.dart +++ b/packages/deriv_auth_ui/lib/src/features/signup/cubits/deriv_country_selection_state.dart @@ -3,28 +3,82 @@ part of 'deriv_country_selection_cubit.dart'; /// Country selection state abstract class DerivCountrySelectionState extends Equatable { /// Initialize country selection state. - const DerivCountrySelectionState(this.countries); + const DerivCountrySelectionState({ + this.countries = const [], + this.selectedCountry, + this.agreedToTerms = false, + this.selectedCountryRequiresConsent = false, + }); /// List of countries. final List countries; + + /// Selected country. + final DerivResidenceModel? selectedCountry; + + /// If the selected country requires consent, value must be true to continue. + /// Default is null. + /// Example: For Brazil, Brazil requires consent to continue. + /// The user must agree to the terms to continue. + final bool agreedToTerms; + + /// If the selected country requires consent to continue, value is true. + /// Default is false. + final bool selectedCountryRequiresConsent; + + @override + List get props => [ + countries, + selectedCountry, + agreedToTerms, + selectedCountryRequiresConsent, + ]; } /// Initial state. class DerivCountrySelectionInitialState extends DerivCountrySelectionState { - /// Initializes initial state. + /// Initialises initial state. const DerivCountrySelectionInitialState() - : super(const []); - - @override - List get props => []; + : super(countries: const []); } /// Country list loaded state. class DerivCountrySelectionLoadedState extends DerivCountrySelectionState { - /// Initialize country list loaded state - const DerivCountrySelectionLoadedState(List countries) - : super(countries); + /// Initialise country list loaded state + const DerivCountrySelectionLoadedState( + List countries, { + DerivResidenceModel? selectedCountry, + }) : super(countries: countries, selectedCountry: selectedCountry); +} - @override - List get props => [countries]; +/// Country selection changed state. +class DerivCountryChangedState extends DerivCountrySelectionState { + /// Initialise country selection changed state. + const DerivCountryChangedState( + List countries, { + DerivResidenceModel? selectedCountry, + bool? selectedCountryRequiresConsent, + }) : super( + countries: countries, + selectedCountry: selectedCountry, + selectedCountryRequiresConsent: + selectedCountryRequiresConsent ?? false, + ); +} + +/// State to update country selection consent. +class DerivCountryConsentChangedState extends DerivCountrySelectionState { + /// Initialize country list loaded state + const DerivCountryConsentChangedState( + List countries, { + DerivResidenceModel? selectedCountry, + bool? selectedCountryRequiresConsent, + bool? agreedToTerms, + }) : super( + countries: countries, + selectedCountry: selectedCountry, + agreedToTerms: agreedToTerms ?? false, + selectedCountryRequiresConsent: + selectedCountryRequiresConsent ?? false, + ); } diff --git a/packages/deriv_auth_ui/lib/src/features/signup/layouts/deriv_country_selection_layout.dart b/packages/deriv_auth_ui/lib/src/features/signup/layouts/deriv_country_selection_layout.dart index 01ccde127..258cac866 100644 --- a/packages/deriv_auth_ui/lib/src/features/signup/layouts/deriv_country_selection_layout.dart +++ b/packages/deriv_auth_ui/lib/src/features/signup/layouts/deriv_country_selection_layout.dart @@ -1,5 +1,6 @@ import 'package:deriv_auth_ui/src/core/extensions/context_extension.dart'; import 'package:deriv_auth_ui/src/core/helpers/assets.dart'; +import 'package:deriv_auth_ui/src/core/helpers/country_selection_helper.dart'; import 'package:deriv_auth_ui/src/features/signup/cubits/deriv_country_selection_cubit.dart'; import 'package:deriv_auth_ui/src/features/signup/models/deriv_residence_model.dart'; import 'package:deriv_auth_ui/src/features/signup/widgets/country_selection_list_widget.dart'; @@ -17,6 +18,7 @@ class DerivCountrySelectionLayout extends StatefulWidget { required this.residences, required this.onNextPressed, this.affiliateToken, + this.countryConsentMessage, Key? key, }) : super(key: key); @@ -32,6 +34,9 @@ class DerivCountrySelectionLayout extends StatefulWidget { /// Affiliate token. final String? affiliateToken; + /// Message to be shown beside country consent checkbox. + final String? countryConsentMessage; + @override State createState() => _DerivCountrySelectionLayoutState(); @@ -45,14 +50,13 @@ class _DerivCountrySelectionLayoutState late TextEditingController _textController; late final DerivCountrySelectionCubit _countrySelectionCubit; - DerivResidenceModel? _selectedResidence; @override void initState() { super.initState(); _countrySelectionCubit = DerivCountrySelectionCubit(widget.residences) - ..fetchResidenceCounties(); + ..fetchResidenceCountries(); _textController = TextEditingController(); } @@ -61,15 +65,29 @@ class _DerivCountrySelectionLayoutState Widget build(BuildContext context) => Scaffold( backgroundColor: context.theme.colors.primary, body: SafeArea( - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Expanded(child: _buildUpperPage()), - Padding( - padding: const EdgeInsets.all(ThemeProvider.margin16), - child: _buildLowerPage(), - ), - ], + child: BlocListener( + bloc: _countrySelectionCubit, + listener: (BuildContext context, DerivCountrySelectionState state) { + if (state.selectedCountry != null && + state.selectedCountry?.name != null) { + _textController.text = state.selectedCountry?.name ?? ''; + } + + WidgetsBinding.instance.addPostFrameCallback((_) { + _formKey.currentState!.validate(); + }); + }, + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Expanded(child: _buildUpperPage()), + Padding( + padding: const EdgeInsets.all(ThemeProvider.margin16), + child: _buildLowerPage(), + ), + ], + ), ), ), ); @@ -86,7 +104,11 @@ class _DerivCountrySelectionLayoutState style: TextStyles.title, ), const SizedBox(height: ThemeProvider.margin24), - _buildSelectionInput() + _buildSelectionInput(), + const SizedBox(height: ThemeProvider.margin16), + _buildCountryConsentCheckbox( + countryConsentMessage: widget.countryConsentMessage, + ), ], ), ); @@ -114,24 +136,31 @@ class _DerivCountrySelectionLayoutState color: context.theme.colors.prominent, ), readOnly: true, - enabled: state is DerivCountrySelectionLoadedState, - validator: (String? value) => _selectedResidence!.isDisabled - ? context.localization.warnCountryNotAvailable - : null, + enabled: _shouldEnableCountrySelectionField(state), + validator: (String? value) => _countrySelectionValidator(context, + selectedCountry: state.selectedCountry), onTap: () => _onSelectCountryTap(state.countries), ), ), ); - Widget _buildNextButton() => PrimaryButton( - isEnabled: - _selectedResidence != null && !_selectedResidence!.isDisabled, - onPressed: widget.onNextPressed, - child: Center( - child: Text( - context.localization.actionNext, - style: TextStyles.button - .copyWith(color: context.theme.colors.prominent), + Widget _buildNextButton() => + BlocBuilder( + bloc: _countrySelectionCubit, + builder: (BuildContext context, DerivCountrySelectionState state) => + PrimaryButton( + isEnabled: _shouldEnableNextButton( + state.selectedCountry, + isConsentRequired: state.selectedCountryRequiresConsent, + agreedToTerms: state.agreedToTerms, + ), + onPressed: widget.onNextPressed, + child: Center( + child: Text( + context.localization.actionNext, + style: TextStyles.button + .copyWith(color: context.theme.colors.prominent), + ), ), ), ); @@ -150,10 +179,9 @@ class _DerivCountrySelectionLayoutState countries: countries, onChanged: (int index) => setState( () { - _textController.text = countries[index].name; - _selectedResidence = countries[index]; - - _formKey.currentState!.validate(); + _countrySelectionCubit.changeSelectedCountry( + selectedCountry: countries[index], + ); }, ), ), @@ -161,6 +189,88 @@ class _DerivCountrySelectionLayoutState ); } + Widget _buildCountryConsentCheckbox({String? countryConsentMessage}) => + BlocBuilder( + bloc: _countrySelectionCubit, + builder: (BuildContext context, DerivCountrySelectionState state) { + final DerivResidenceModel? selectedCountry = state.selectedCountry; + + if (selectedCountry == null || + !state.selectedCountryRequiresConsent) { + return const SizedBox.shrink(); + } + + return CustomCheckbox( + padding: const EdgeInsets.symmetric( + vertical: ThemeProvider.margin16, + ), + contentsVerticalAlignment: CrossAxisAlignment.start, + value: state.agreedToTerms, + onValueChanged: ({bool? isChecked}) => + _countrySelectionCubit.updateCountryConsentStatus( + agreedToTerms: isChecked, + ), + message: countryConsentMessage ?? + getCountryConsentMessage( + context, + countryCode: selectedCountry.code, + ), + ); + }, + ); + + /// Validates the country selection. + String? _countrySelectionValidator( + BuildContext context, { + DerivResidenceModel? selectedCountry, + }) { + if (selectedCountry != null && selectedCountry.isDisabled) { + return context.localization.warnCountryNotAvailable; + } + + return null; + } + + /// Determines whether the next button should be enabled based on country selection. + /// + /// Returns `true` if the following conditions are met: + /// - A country is selected. + /// - The selected country is not disabled. + /// - If `isConsentRequired` is true, the user must agree to the terms. + /// + /// Otherwise, returns `false`. + bool _shouldEnableNextButton( + DerivResidenceModel? selectedCountry, { + bool agreedToTerms = false, + bool isConsentRequired = false, + }) { + if (selectedCountry == null) { + return false; + } + + bool shouldEnable = !selectedCountry.isDisabled; + + if (isConsentRequired) { + shouldEnable = shouldEnable && agreedToTerms; + } + + return shouldEnable; + } + + /// Determines whether the country selection field should be enabled + /// based on the given state. + bool _shouldEnableCountrySelectionField( + DerivCountrySelectionState countrySelectionState, + ) { + if (countrySelectionState is DerivCountrySelectionLoadedState || + countrySelectionState is DerivCountryChangedState || + countrySelectionState is DerivCountryConsentChangedState) { + return true; + } + + return false; + } + @override void dispose() { _focusNode.dispose(); diff --git a/packages/deriv_auth_ui/lib/src/features/signup/layouts/deriv_email_not_received_layout.dart b/packages/deriv_auth_ui/lib/src/features/signup/layouts/deriv_email_not_received_layout.dart index bad1e2728..a7dc2d4ec 100644 --- a/packages/deriv_auth_ui/lib/src/features/signup/layouts/deriv_email_not_received_layout.dart +++ b/packages/deriv_auth_ui/lib/src/features/signup/layouts/deriv_email_not_received_layout.dart @@ -45,7 +45,7 @@ class DerivEmailNotReceivedLayout extends StatelessWidget { context.localization.labelEmailIssueHeader, style: context.theme.textStyle( textStyle: TextStyles.body2, - color: context.theme.colors.lessProminent, + color: context.theme.colors.general, ), ), const SizedBox(height: ThemeProvider.margin24), @@ -58,7 +58,7 @@ class DerivEmailNotReceivedLayout extends StatelessWidget { context.localization.labelEmailIssueSpam, style: context.theme.textStyle( textStyle: TextStyles.body1, - color: context.theme.colors.lessProminent, + color: context.theme.colors.general, ), ), ), @@ -74,7 +74,7 @@ class DerivEmailNotReceivedLayout extends StatelessWidget { context.localization.labelEmailIssueWrongEmail, style: context.theme.textStyle( textStyle: TextStyles.body1, - color: context.theme.colors.lessProminent, + color: context.theme.colors.general, ), ), ), @@ -90,7 +90,7 @@ class DerivEmailNotReceivedLayout extends StatelessWidget { context.localization.labelEmailIssueTypo, style: context.theme.textStyle( textStyle: TextStyles.body1, - color: context.theme.colors.lessProminent, + color: context.theme.colors.general, ), ), ), @@ -106,7 +106,7 @@ class DerivEmailNotReceivedLayout extends StatelessWidget { context.localization.labelEmailIssueFirewall, style: context.theme.textStyle( textStyle: TextStyles.body1, - color: context.theme.colors.lessProminent, + color: context.theme.colors.general, ), ), ), diff --git a/packages/deriv_auth_ui/lib/src/features/signup/layouts/deriv_set_password_layout.dart b/packages/deriv_auth_ui/lib/src/features/signup/layouts/deriv_set_password_layout.dart index dc4469142..63e5e359f 100644 --- a/packages/deriv_auth_ui/lib/src/features/signup/layouts/deriv_set_password_layout.dart +++ b/packages/deriv_auth_ui/lib/src/features/signup/layouts/deriv_set_password_layout.dart @@ -1,8 +1,9 @@ import 'package:deriv_auth/deriv_auth.dart'; +import 'package:deriv_auth_ui/deriv_auth_ui.dart'; import 'package:deriv_auth_ui/src/core/extensions/context_extension.dart'; import 'package:deriv_auth_ui/src/core/extensions/string_extension.dart'; import 'package:deriv_auth_ui/src/core/helpers/assets.dart'; -import 'package:deriv_auth_ui/src/features/signup/models/deriv_auth_utm_model.dart'; +import 'package:deriv_auth_ui/src/core/states/auth_state_listener.dart'; import 'package:deriv_auth_ui/src/features/signup/widgets/password_policy_checker_widget.dart'; import 'package:deriv_theme/deriv_theme.dart'; import 'package:deriv_ui/deriv_ui.dart'; @@ -14,12 +15,13 @@ import 'package:flutter_svg/flutter_svg.dart'; class DerivSetPasswordLayout extends StatefulWidget { /// constructor of country set password page const DerivSetPasswordLayout({ - required this.onDerivAuthState, required this.onDerivSignupState, required this.onPreviousPressed, required this.verificationCode, required this.residence, + this.authErrorStateHandler, this.utmModel, + this.onAuthError, Key? key, }) : super(key: key); @@ -32,8 +34,11 @@ class DerivSetPasswordLayout extends StatefulWidget { /// Utm model final DerivAuthUtmModel? utmModel; - /// Callback to be called when auth state changes. - final void Function(BuildContext, DerivAuthState) onDerivAuthState; + /// Extension of base [AuthErrorStateHandler]. If not provided, base implementation will be used. + final AuthErrorStateHandler? authErrorStateHandler; + + /// Callback to be called on [DerivAuthErrorState] + final Function(DerivAuthErrorState)? onAuthError; /// Callback to be called when signup state changes. final void Function(BuildContext, DerivSignupState) onDerivSignupState; @@ -63,9 +68,9 @@ class _DerivSetPasswordLayoutState extends State { } @override - Widget build(BuildContext context) => - BlocListener( - listener: widget.onDerivAuthState, + Widget build(BuildContext context) => DerivAuthStateListener( + authErrorStateHandler: widget.authErrorStateHandler, + onError: widget.onAuthError, child: Scaffold( backgroundColor: context.theme.colors.primary, body: SafeArea( diff --git a/packages/deriv_auth_ui/lib/src/features/signup/layouts/deriv_signup_layout.dart b/packages/deriv_auth_ui/lib/src/features/signup/layouts/deriv_signup_layout.dart index 925e3a2a4..2a34b8396 100644 --- a/packages/deriv_auth_ui/lib/src/features/signup/layouts/deriv_signup_layout.dart +++ b/packages/deriv_auth_ui/lib/src/features/signup/layouts/deriv_signup_layout.dart @@ -1,6 +1,7 @@ import 'package:deriv_auth/deriv_auth.dart'; import 'package:deriv_auth_ui/src/core/extensions/context_extension.dart'; import 'package:deriv_auth_ui/src/core/extensions/string_extension.dart'; +import 'package:deriv_auth_ui/src/core/states/auth_state_listener.dart'; import 'package:deriv_auth_ui/src/features/login/widgets/deriv_social_auth_divider.dart'; import 'package:deriv_auth_ui/src/features/login/widgets/deriv_social_auth_panel.dart'; import 'package:deriv_theme/deriv_theme.dart'; @@ -8,6 +9,8 @@ import 'package:deriv_ui/deriv_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:deriv_auth_ui/src/core/states/auth_error_state_handler.dart'; + /// It offers creating demo accounts via email and third-party providers. /// It Also provides optional referral code section which can be disabled /// by setting [enableReferralSection] to false. @@ -21,7 +24,10 @@ class DerivSignupLayout extends StatefulWidget { required this.onLoginTapped, required this.signupPageLabel, required this.signupPageDescription, + this.isSocialAuthEnabled = true, + this.authErrorStateHandler, this.enableReferralSection = true, + this.onAuthError, Key? key, }) : super(key: key); @@ -31,6 +37,13 @@ class DerivSignupLayout extends StatefulWidget { /// Callback to be called when signup error occurs. final Function(DerivSignupErrorState) onSingupError; + /// Extension of base [AuthErrorStateHandler]. If not provided, base implementation will be used. + final AuthErrorStateHandler? authErrorStateHandler; + + /// Callback to be called on [DerivAuthErrorState]. + /// Useful if needed to do anything additional to [authErrorStateHandler]. + final Function(DerivAuthErrorState)? onAuthError; + /// Callback to be called when signup email is sent. final Function(String) onSingupEmailSent; @@ -49,6 +62,9 @@ class DerivSignupLayout extends StatefulWidget { /// Description of signup page. final String signupPageDescription; + /// Whether to display social auth buttons. + final bool isSocialAuthEnabled; + @override State createState() => _DerivSignupLayoutState(); } @@ -77,39 +93,47 @@ class _DerivSignupLayoutState extends State { Text(context.localization.labelSignUp, style: TextStyles.title), backgroundColor: context.theme.colors.secondary, ), - body: BlocConsumer( - listener: _onSignUpState, - builder: (BuildContext context, DerivSignupState state) => Form( - key: formKey, - child: SingleChildScrollView( - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: ThemeProvider.margin16, - vertical: ThemeProvider.margin24, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ..._buildHeaderSection(), - const SizedBox(height: ThemeProvider.margin24), - _buildEmailTextField(), - const SizedBox(height: ThemeProvider.margin36), - if (widget.enableReferralSection) _buildReferralSection(), - const SizedBox(height: ThemeProvider.margin16), - _buildSignUpButton(), - const SizedBox(height: ThemeProvider.margin24), - DerivSocialAuthDivider( - label: context.localization.labelOrSignUpWith, - ), - const SizedBox(height: ThemeProvider.margin24), - DerivSocialAuthPanel( - isEnabled: !isReferralEnabled, - onSocialAuthButtonPressed: - widget.onSocialAuthButtonPressed, - ), - const SizedBox(height: ThemeProvider.margin24), - _buildFooterSection(), - ], + body: DerivAuthStateListener( + authErrorStateHandler: widget.authErrorStateHandler, + onError: widget.onAuthError, + child: BlocConsumer( + listener: _onSignUpState, + builder: (BuildContext context, DerivSignupState state) => Form( + key: formKey, + child: SingleChildScrollView( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: ThemeProvider.margin16, + vertical: ThemeProvider.margin24, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ..._buildHeaderSection(), + const SizedBox(height: ThemeProvider.margin24), + _buildEmailTextField(), + const SizedBox(height: ThemeProvider.margin36), + if (widget.enableReferralSection) _buildReferralSection(), + const SizedBox(height: ThemeProvider.margin16), + _buildSignUpButton(), + const SizedBox(height: ThemeProvider.margin24), + DerivSocialAuthDivider( + label: context.localization.labelOrSignUpWith, + isVisible: widget.isSocialAuthEnabled, + ), + if (widget.isSocialAuthEnabled) + const SizedBox(height: ThemeProvider.margin24), + DerivSocialAuthPanel( + isEnabled: !isReferralEnabled, + onSocialAuthButtonPressed: + widget.onSocialAuthButtonPressed, + isVisible: widget.isSocialAuthEnabled, + ), + if (widget.isSocialAuthEnabled) + const SizedBox(height: ThemeProvider.margin24), + _buildFooterSection(), + ], + ), ), ), ), @@ -136,6 +160,7 @@ class _DerivSignupLayoutState extends State { dialogTitle: context.localization.labelReferralInfoTitle, dialogDescription: context.localization.infoReferralInfoDescription, + positiveActionLabel: context.localization.actionOk, iconSize: ThemeProvider.iconSize24, ), const SizedBox(width: ThemeProvider.margin08), @@ -217,7 +242,7 @@ class _DerivSignupLayoutState extends State { widget.signupPageDescription, style: context.theme.textStyle( textStyle: TextStyles.body1, - color: context.theme.colors.lessProminent, + color: context.theme.colors.general, ), ), ]; @@ -230,7 +255,7 @@ class _DerivSignupLayoutState extends State { context.localization.labelHaveAccount, style: context.theme.textStyle( textStyle: TextStyles.body1, - color: context.theme.colors.lessProminent, + color: context.theme.colors.general, ), ), InkWell( diff --git a/packages/deriv_auth_ui/lib/src/features/signup/layouts/deriv_verification_done_layout.dart b/packages/deriv_auth_ui/lib/src/features/signup/layouts/deriv_verification_done_layout.dart index 4df6eb34b..400d1e1f4 100644 --- a/packages/deriv_auth_ui/lib/src/features/signup/layouts/deriv_verification_done_layout.dart +++ b/packages/deriv_auth_ui/lib/src/features/signup/layouts/deriv_verification_done_layout.dart @@ -64,7 +64,7 @@ class DerivVerificationDoneLayout extends StatelessWidget { context.localization.informLetsContinue, style: context.theme.textStyle( textStyle: TextStyles.body1, - color: context.theme.colors.lessProminent, + color: context.theme.colors.general, ), ), ], diff --git a/packages/deriv_auth_ui/lib/src/features/signup/layouts/deriv_verify_email_layout.dart b/packages/deriv_auth_ui/lib/src/features/signup/layouts/deriv_verify_email_layout.dart index 46a525d8b..637f61365 100644 --- a/packages/deriv_auth_ui/lib/src/features/signup/layouts/deriv_verify_email_layout.dart +++ b/packages/deriv_auth_ui/lib/src/features/signup/layouts/deriv_verify_email_layout.dart @@ -64,7 +64,7 @@ class DerivVerifyEmailLayout extends StatelessWidget { context.localization.informVerificationEmailSent(email!), style: context.theme.textStyle( textStyle: TextStyles.body1, - color: context.theme.colors.lessProminent, + color: context.theme.colors.general, ), textAlign: TextAlign.center, ), diff --git a/packages/deriv_auth_ui/lib/src/features/signup/widgets/country_selection_list_widget.dart b/packages/deriv_auth_ui/lib/src/features/signup/widgets/country_selection_list_widget.dart index 0e4fa9133..294aa9aad 100644 --- a/packages/deriv_auth_ui/lib/src/features/signup/widgets/country_selection_list_widget.dart +++ b/packages/deriv_auth_ui/lib/src/features/signup/widgets/country_selection_list_widget.dart @@ -113,7 +113,7 @@ class _CountrySelectionListWidgetState focusNode: _searchFocusNode, style: context.theme.textStyle( textStyle: TextStyles.subheading, - color: context.theme.colors.lessProminent, + color: context.theme.colors.general, ), decoration: InputDecoration( border: InputBorder.none, @@ -145,7 +145,7 @@ class _CountrySelectionListWidgetState _filteredCountries[index].name, style: context.theme.textStyle( textStyle: TextStyles.body1, - color: context.theme.colors.lessProminent, + color: context.theme.colors.general, ), ), ), diff --git a/packages/deriv_auth_ui/lib/src/features/signup/widgets/password_policy_checker_widget.dart b/packages/deriv_auth_ui/lib/src/features/signup/widgets/password_policy_checker_widget.dart index 68dc90052..508f8eb8b 100644 --- a/packages/deriv_auth_ui/lib/src/features/signup/widgets/password_policy_checker_widget.dart +++ b/packages/deriv_auth_ui/lib/src/features/signup/widgets/password_policy_checker_widget.dart @@ -69,7 +69,7 @@ class PasswordPolicyCheckerWidget extends StatelessWidget { context.localization.informPasswordPolicy, style: context.theme.textStyle( textStyle: TextStyles.body1, - color: context.theme.colors.lessProminent, + color: context.theme.colors.general, ), ), const SizedBox(height: ThemeProvider.margin04), @@ -114,7 +114,7 @@ class PasswordPolicyCheckerWidget extends StatelessWidget { textStyle: TextStyles.body1, color: policy.isMatchWith(password) ? context.theme.colors.hover - : context.theme.colors.lessProminent, + : context.theme.colors.general, ); Widget _buildPolicyIcon({ @@ -129,7 +129,7 @@ class PasswordPolicyCheckerWidget extends StatelessWidget { policy.isMatchWith(password) ? Icons.check : Icons.circle; final Color color = policy.isMatchWith(password) - ? context.theme.colors.lessProminent + ? context.theme.colors.general : context.theme.colors.coral; return Padding( diff --git a/packages/deriv_auth_ui/pubspec.yaml b/packages/deriv_auth_ui/pubspec.yaml index 32f15918a..c15ade0f4 100644 --- a/packages/deriv_auth_ui/pubspec.yaml +++ b/packages/deriv_auth_ui/pubspec.yaml @@ -16,17 +16,22 @@ dependencies: deriv_auth: git: - url: git@github.com:regentmarkets/flutter-deriv-packages.git + url: git@github.com:sahani-deriv/flutter-deriv-packages.git path: packages/deriv_auth - ref: dev + ref: auth-ui-update + deriv_theme: - path: ../deriv_theme - # git: - # url: git@github.com:regentmarkets/flutter-deriv-packages.git - # path: packages/deriv_theme - # ref: dev + git: + url: git@github.com:regentmarkets/flutter-deriv-packages.git + path: packages/deriv_theme + ref: dev + deriv_ui: - path: ../deriv_ui + git: + url: git@github.com:sahani-deriv/flutter-deriv-packages.git + path: packages/deriv_ui + ref: auth-ui-update + flutter_svg: ^1.1.6 smooth_page_indicator: ^1.1.0 intl_utils: ^2.8.3 diff --git a/packages/deriv_auth_ui/test/core/states/auth_error_state_mapper_test.dart b/packages/deriv_auth_ui/test/core/states/auth_error_state_mapper_test.dart new file mode 100644 index 000000000..12d6a5f74 --- /dev/null +++ b/packages/deriv_auth_ui/test/core/states/auth_error_state_mapper_test.dart @@ -0,0 +1,206 @@ +import 'package:flutter/material.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; +import 'package:deriv_auth/deriv_auth.dart'; +import 'package:deriv_auth_ui/deriv_auth_ui.dart'; + +class MockBuildContext extends Mock implements BuildContext {} + +// Create a mock implementation of AuthErrorStateHandler for testing. +final class MockAuthErrorStateHandler extends AuthErrorStateHandler { + MockAuthErrorStateHandler({required super.context}); + + DerivAuthErrorState? lastHandledError; + + @override + void onMissingOtp(DerivAuthErrorState errorState) { + lastHandledError = errorState; + } + + @override + void onSelfClosed(DerivAuthErrorState errorState) { + lastHandledError = errorState; + } + + @override + void onUnsupportedCountry(DerivAuthErrorState errorState) { + lastHandledError = errorState; + } + + @override + void onAccountUnavailable(DerivAuthErrorState errorState) { + lastHandledError = errorState; + } + + @override + void onInvalidCredentials(DerivAuthErrorState errorState) { + lastHandledError = errorState; + } + + @override + void onFailedAuthorization(DerivAuthErrorState errorState) { + lastHandledError = errorState; + } + + @override + void onInavlidResidence(DerivAuthErrorState errorState) { + lastHandledError = errorState; + } + + @override + void onExpiredAccount(DerivAuthErrorState errorState) { + lastHandledError = errorState; + } + + @override + void invalid2faCode(DerivAuthErrorState errorState) { + lastHandledError = errorState; + } + + @override + void onConnectionError(DerivAuthErrorState errorState) { + lastHandledError = errorState; + } + + @override + void onSwitchAccountError(DerivAuthErrorState errorState) { + lastHandledError = errorState; + } +} + +void main() { + // Create an instance of MockAuthErrorStateHandler for testing. + final MockAuthErrorStateHandler mockHandler = MockAuthErrorStateHandler( + context: MockBuildContext(), + ); + + test('authErrorStateMapper handles missing OTP', () { + final DerivAuthErrorState errorState = DerivAuthErrorState( + type: AuthErrorType.missingOtp, isSocialLogin: false, message: ''); + authErrorStateMapper( + authErrorState: errorState, + authErrorStateHandler: mockHandler, + ); + + expect(mockHandler.lastHandledError, equals(errorState)); + }); + + test('authErrorStateMapper handles self closed', () { + final DerivAuthErrorState errorState = DerivAuthErrorState( + type: AuthErrorType.selfClosed, isSocialLogin: false, message: ''); + authErrorStateMapper( + authErrorState: errorState, + authErrorStateHandler: mockHandler, + ); + + expect(mockHandler.lastHandledError, equals(errorState)); + }); + + test('authErrorStateMapper handles unsupported country', () { + final DerivAuthErrorState errorState = DerivAuthErrorState( + type: AuthErrorType.unsupportedCountry, + isSocialLogin: false, + message: ''); + authErrorStateMapper( + authErrorState: errorState, + authErrorStateHandler: mockHandler, + ); + + expect(mockHandler.lastHandledError, equals(errorState)); + }); + + test('authErrorStateMapper handles account unavailable', () { + final DerivAuthErrorState errorState = DerivAuthErrorState( + type: AuthErrorType.accountUnavailable, + isSocialLogin: false, + message: ''); + authErrorStateMapper( + authErrorState: errorState, + authErrorStateHandler: mockHandler, + ); + + expect(mockHandler.lastHandledError, equals(errorState)); + }); + + test('authErrorStateMapper handles invalid credentials', () { + final DerivAuthErrorState errorState = DerivAuthErrorState( + type: AuthErrorType.invalidCredential, + isSocialLogin: false, + message: ''); + authErrorStateMapper( + authErrorState: errorState, + authErrorStateHandler: mockHandler, + ); + + expect(mockHandler.lastHandledError, equals(errorState)); + }); + + test('authErrorStateMapper handles failed authorization', () { + final DerivAuthErrorState errorState = DerivAuthErrorState( + type: AuthErrorType.failedAuthorization, + isSocialLogin: false, + message: ''); + authErrorStateMapper( + authErrorState: errorState, + authErrorStateHandler: mockHandler, + ); + + expect(mockHandler.lastHandledError, equals(errorState)); + }); + + test('authErrorStateMapper handles invalid residence', () { + final DerivAuthErrorState errorState = DerivAuthErrorState( + type: AuthErrorType.invalidResidence, + isSocialLogin: false, + message: ''); + authErrorStateMapper( + authErrorState: errorState, + authErrorStateHandler: mockHandler, + ); + + expect(mockHandler.lastHandledError, equals(errorState)); + }); + + test('authErrorStateMapper handles expired account', () { + final DerivAuthErrorState errorState = DerivAuthErrorState( + type: AuthErrorType.expiredAccount, isSocialLogin: false, message: ''); + authErrorStateMapper( + authErrorState: errorState, + authErrorStateHandler: mockHandler, + ); + + expect(mockHandler.lastHandledError, equals(errorState)); + }); + + test('authErrorStateMapper handles invalid 2FA code', () { + final DerivAuthErrorState errorState = DerivAuthErrorState( + type: AuthErrorType.invalid2faCode, isSocialLogin: false, message: ''); + authErrorStateMapper( + authErrorState: errorState, + authErrorStateHandler: mockHandler, + ); + expect(mockHandler.lastHandledError, equals(errorState)); + }); + + test('authErrorStateMapper handles connection error', () { + final DerivAuthErrorState errorState = DerivAuthErrorState( + type: AuthErrorType.connectionError, isSocialLogin: false, message: ''); + authErrorStateMapper( + authErrorState: errorState, + authErrorStateHandler: mockHandler, + ); + expect(mockHandler.lastHandledError, equals(errorState)); + }); + + test('authErrorStateMapper handles switch account error', () { + final DerivAuthErrorState errorState = DerivAuthErrorState( + type: AuthErrorType.switchAccountError, + isSocialLogin: false, + message: ''); + authErrorStateMapper( + authErrorState: errorState, + authErrorStateHandler: mockHandler, + ); + expect(mockHandler.lastHandledError, equals(errorState)); + }); +} diff --git a/packages/deriv_auth_ui/test/core/states/auth_state_listener_test.dart b/packages/deriv_auth_ui/test/core/states/auth_state_listener_test.dart new file mode 100644 index 000000000..85db400ae --- /dev/null +++ b/packages/deriv_auth_ui/test/core/states/auth_state_listener_test.dart @@ -0,0 +1,134 @@ +import 'package:deriv_auth/core/models/landig_comany_model.dart'; +import 'package:deriv_auth/deriv_auth.dart'; +import 'package:deriv_auth_ui/src/core/states/auth_state_listener.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:patrol_finders/patrol_finders.dart'; + +import '../../pump_app.dart'; + +class MockDerivAuthCubit extends Mock implements DerivAuthCubit {} + +class MockBuildContext extends Mock implements BuildContext {} + +void main() { + late DerivAuthCubit authCubit; + + setUpAll(() { + authCubit = MockDerivAuthCubit(); + + when(() => authCubit.close()).thenAnswer((_) async {}); + }); + + group('AuthStateLister', () { + patrolWidgetTest('onLoggedIn is called based on logged in state', + (PatrolTester $) async { + bool isOnLoggedInCalled = false; + + final DerivAuthLoggedInState mockAuthState = + DerivAuthLoggedInState(const DerivAuthModel( + authorizeEntity: AuthorizeEntity(), + landingCompany: LandingCompanyEntity(), + )); + + when(() => authCubit.state).thenAnswer((_) => mockAuthState); + + when(() => authCubit.stream).thenAnswer((_) => + Stream.fromIterable([mockAuthState])); + + await $.pumpApp( + BlocProvider( + create: (_) => authCubit, + child: DerivAuthStateListener( + onLoggedIn: (_) { + isOnLoggedInCalled = true; + }, + child: Container()), + ), + ); + + expect(isOnLoggedInCalled, true); + }); + + patrolWidgetTest('onLoggedOut is called based on logged out state', + (PatrolTester $) async { + bool isOnLoggedOutCalled = false; + + final DerivAuthLoggedOutState mockAuthState = DerivAuthLoggedOutState(); + + when(() => authCubit.state).thenAnswer((_) => mockAuthState); + + when(() => authCubit.stream).thenAnswer((_) => + Stream.fromIterable([mockAuthState])); + + await $.pumpApp( + BlocProvider( + create: (_) => authCubit, + child: DerivAuthStateListener( + onLoggedOut: () { + isOnLoggedOutCalled = true; + }, + child: Container()), + ), + ); + + expect(isOnLoggedOutCalled, true); + }); + + patrolWidgetTest('onLoading is called based on loading state', + (PatrolTester $) async { + bool isOnLoadingCalled = false; + + final DerivAuthLoadingState mockAuthState = DerivAuthLoadingState(); + + when(() => authCubit.state).thenAnswer((_) => mockAuthState); + + when(() => authCubit.stream).thenAnswer((_) => + Stream.fromIterable([mockAuthState])); + + await $.pumpApp( + BlocProvider( + create: (_) => authCubit, + child: DerivAuthStateListener( + onLoading: () { + isOnLoadingCalled = true; + }, + child: Container()), + ), + ); + + expect(isOnLoadingCalled, true); + }); + + patrolWidgetTest('onError is called based on error state', + (PatrolTester $) async { + bool isOnErrorCalled = false; + + final DerivAuthErrorState mockAuthState = DerivAuthErrorState( + type: AuthErrorType.accountUnavailable, + message: 'error', + isSocialLogin: false, + ); + + when(() => authCubit.state).thenAnswer((_) => mockAuthState); + + when(() => authCubit.stream).thenAnswer((_) => + Stream.fromIterable([mockAuthState])); + + await $.pumpApp( + BlocProvider( + create: (_) => authCubit, + child: DerivAuthStateListener( + onError: (_) { + isOnErrorCalled = true; + }, + child: Container()), + ), + ); + + expect(isOnErrorCalled, true); + }); + }); +} diff --git a/packages/deriv_auth_ui/test/features/get_started/layouts/deriv_get_started_layout_test.dart b/packages/deriv_auth_ui/test/features/get_started/layouts/deriv_get_started_layout_test.dart index 5acb0fd52..a59eb0741 100644 --- a/packages/deriv_auth_ui/test/features/get_started/layouts/deriv_get_started_layout_test.dart +++ b/packages/deriv_auth_ui/test/features/get_started/layouts/deriv_get_started_layout_test.dart @@ -28,6 +28,7 @@ void main() { patrolWidgetTest('should render DerivGetStartedLayout', (PatrolTester $) async { await $.pumpApp(DerivGetStartedLayout( + onTapNavigation: () {}, slides: [mockSlideModel], appLogoIconPath: appLogoIconPath, backgroundImagePath: backgroundImagePath, @@ -46,6 +47,7 @@ void main() { bool loginTapped = false; await $.pumpApp(DerivGetStartedLayout( + onTapNavigation: () {}, slides: [mockSlideModel], appLogoIconPath: appLogoIconPath, backgroundImagePath: backgroundImagePath, @@ -65,6 +67,7 @@ void main() { bool signupTapped = false; await $.pumpApp(DerivGetStartedLayout( + onTapNavigation: () {}, slides: [mockSlideModel], appLogoIconPath: appLogoIconPath, backgroundImagePath: backgroundImagePath, diff --git a/packages/deriv_auth_ui/test/features/login/layouts/deriv_2fa_layout_test.dart b/packages/deriv_auth_ui/test/features/login/layouts/deriv_2fa_layout_test.dart index 6a7aa9256..5907efae0 100644 --- a/packages/deriv_auth_ui/test/features/login/layouts/deriv_2fa_layout_test.dart +++ b/packages/deriv_auth_ui/test/features/login/layouts/deriv_2fa_layout_test.dart @@ -55,8 +55,9 @@ void main() { password: any(named: 'password'), otp: any(named: 'otp'))) .thenAnswer((_) async => DerivAuthLoggedInState(const DerivAuthModel( - authorizeEntity: AuthorizeEntity(), - landingCompany: LandingCompanyEntity()))); + authorizeEntity: AuthorizeEntity(), + landingCompany: LandingCompanyEntity(), + ))); await $.pumpApp( BlocProvider.value( diff --git a/packages/deriv_auth_ui/test/features/login/layouts/deriv_login_layout_test.dart b/packages/deriv_auth_ui/test/features/login/layouts/deriv_login_layout_test.dart index e746f3a6b..83e94330e 100644 --- a/packages/deriv_auth_ui/test/features/login/layouts/deriv_login_layout_test.dart +++ b/packages/deriv_auth_ui/test/features/login/layouts/deriv_login_layout_test.dart @@ -40,9 +40,9 @@ void main() { greetingLabel: greetingLabel, onResetPassTapped: () {}, onSignupTapped: () {}, - onLoginError: (_) {}, onLoggedIn: (_) {}, onSocialAuthButtonPressed: (p0) {}, + onLoginError: (_) {}, ), ), ); @@ -71,9 +71,9 @@ void main() { greetingLabel: greetingLabel, onResetPassTapped: () {}, onSignupTapped: () {}, - onLoginError: (_) {}, onLoggedIn: (_) {}, onSocialAuthButtonPressed: (_) {}, + onLoginError: (_) {}, ), ), ); @@ -104,9 +104,9 @@ void main() { greetingLabel: greetingLabel, onResetPassTapped: () {}, onSignupTapped: () {}, - onLoginError: (_) {}, onLoggedIn: (_) {}, onSocialAuthButtonPressed: (_) {}, + onLoginError: (_) {}, ), )); @@ -133,9 +133,9 @@ void main() { onSignupTapped: () { onSignupTappedCalled = true; }, - onLoginError: (_) {}, onLoggedIn: (_) {}, onSocialAuthButtonPressed: (_) {}, + onLoginError: (_) {}, ), )); @@ -171,11 +171,11 @@ void main() { greetingLabel: greetingLabel, onResetPassTapped: () {}, onSignupTapped: () {}, - onLoginError: (_) {}, onLoggedIn: (_) { onLoggedInCalled = true; }, onSocialAuthButtonPressed: (_) {}, + onLoginError: (_) {}, ), )); @@ -214,6 +214,35 @@ void main() { expect(onLoginErrorCalled, isTrue); }); + patrolWidgetTest('calls [AuthErrorStateHandler] on auth error state.', + (PatrolTester $) async { + final mockAuthState = DerivAuthErrorState( + isSocialLogin: false, + message: 'error', + type: AuthErrorType.failedAuthorization, + ); + + when(() => authCubit.state).thenAnswer((_) => mockAuthState); + + when(() => authCubit.stream) + .thenAnswer((_) => Stream.fromIterable([mockAuthState])); + + await $.pumpApp(BlocProvider.value( + value: authCubit, + child: DerivLoginLayout( + welcomeLabel: welcomeLabel, + greetingLabel: greetingLabel, + onResetPassTapped: () {}, + onSignupTapped: () {}, + onLoginError: (_) {}, + onLoggedIn: (_) {}, + onSocialAuthButtonPressed: (_) {}, + ), + )); + + expect($(PopupAlertDialog).$('Authorization failed.'), findsOneWidget); + }); + patrolWidgetTest('calls resetPassTapped when reset button is pressed.', (PatrolTester $) async { final mockAuthState = DerivAuthLoggedOutState(); @@ -234,9 +263,9 @@ void main() { onResetPassTappedCalled = true; }, onSignupTapped: () {}, - onLoginError: (_) {}, onLoggedIn: (_) {}, onSocialAuthButtonPressed: (_) {}, + onLoginError: (_) {}, ), )); @@ -264,11 +293,11 @@ void main() { greetingLabel: greetingLabel, onResetPassTapped: () {}, onSignupTapped: () {}, - onLoginError: (_) {}, onLoggedIn: (_) {}, onSocialAuthButtonPressed: (_) { onSocialAuthButtonPressedCalled = true; }, + onLoginError: (_) {}, ), )); diff --git a/packages/deriv_auth_ui/test/features/reset_pass/layouts/deriv_success_pass_change_layout_test.dart b/packages/deriv_auth_ui/test/features/reset_pass/layouts/deriv_success_pass_change_layout_test.dart new file mode 100644 index 000000000..9e647d321 --- /dev/null +++ b/packages/deriv_auth_ui/test/features/reset_pass/layouts/deriv_success_pass_change_layout_test.dart @@ -0,0 +1,17 @@ +import 'package:deriv_auth_ui/deriv_auth_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:patrol_finders/patrol_finders.dart'; + +import '../../../pump_app.dart'; + +void main() { + group('DerivSuccessPassChangeLayout', () { + patrolWidgetTest('renders correctly', (PatrolTester $) async { + await $.pumpApp(settle: false, const DerivSuccessPassChangeLayout()); + + expect($(Text).$('Reset Password'), findsOneWidget); + expect($(Text).$('Your password has been reset'), findsOneWidget); + }); + }); +} diff --git a/packages/deriv_auth_ui/test/features/signup/cubits/deriv_country_selection_cubit_test.dart b/packages/deriv_auth_ui/test/features/signup/cubits/deriv_country_selection_cubit_test.dart index 7d854c8bc..ea764dc15 100644 --- a/packages/deriv_auth_ui/test/features/signup/cubits/deriv_country_selection_cubit_test.dart +++ b/packages/deriv_auth_ui/test/features/signup/cubits/deriv_country_selection_cubit_test.dart @@ -6,29 +6,95 @@ import 'package:flutter_test/flutter_test.dart'; void main() { group('DerivCountrySelectionCubit', () { - late DerivCountrySelectionCubit cubit; + late DerivCountrySelectionCubit countrySelectionCubit; + late List countriesList; setUp(() { - final countries = [ + countriesList = [ const DerivResidenceModel( code: 'us', name: 'United States', isDisabled: false), + const DerivResidenceModel( + code: 'br', name: 'Brazil', isDisabled: false), const DerivResidenceModel( code: 'fr', name: 'France', isDisabled: false), const DerivResidenceModel( code: 'de', name: 'Germany', isDisabled: false), ]; - cubit = DerivCountrySelectionCubit(Future.value(countries)); - }); - test('initial state is DerivCountrySelectionInitialState', () { - expect(cubit.state, const DerivCountrySelectionInitialState()); + countrySelectionCubit = + DerivCountrySelectionCubit(Future.value(countriesList)); }); blocTest( - 'emits DerivCountrySelectionLoadedState with filtered countries when fetchResidenceCounties is called', - build: () => cubit, - act: (cubit) => cubit.fetchResidenceCounties(), - expect: () => [isA()], - ); + 'Initial states is [DerivCountrySelectionInitialState]', + build: () => countrySelectionCubit, + verify: (DerivCountrySelectionCubit cubit) { + expect( + cubit.state, + isA(), + ); + }); + + blocTest( + 'Verify cubit emits the right states by loading country list', + build: () => countrySelectionCubit, + act: (DerivCountrySelectionCubit cubit) => + countrySelectionCubit.fetchResidenceCountries(), + verify: (DerivCountrySelectionCubit cubit) { + expect( + cubit.state, + isA(), + ); + }); + + blocTest( + 'Verify cubit emits the right states by changing the country selection.', + build: () => countrySelectionCubit, + seed: () => DerivCountrySelectionLoadedState(countriesList), + act: (DerivCountrySelectionCubit cubit) => cubit.changeSelectedCountry( + selectedCountry: countriesList[0], + ), + verify: (DerivCountrySelectionCubit cubit) { + expect( + countrySelectionCubit.state, + isA(), + ); + + expect( + countrySelectionCubit.state.selectedCountry?.code, + countriesList[0].code, + ); + }); + + blocTest( + 'Verify cubit emits the right states when it changing the consent status(Checkbox).', + build: () => countrySelectionCubit, + seed: () => DerivCountrySelectionLoadedState(countriesList), + act: (DerivCountrySelectionCubit cubit) { + cubit + ..changeSelectedCountry( + selectedCountry: const DerivResidenceModel( + code: 'br', name: 'Brazil', isDisabled: false), + ) + ..updateCountryConsentStatus( + agreedToTerms: !cubit.state.agreedToTerms, + ); + }, + verify: (DerivCountrySelectionCubit cubit) { + expect( + cubit.state, + isA(), + ); + + expect( + cubit.state.selectedCountryRequiresConsent, + true, + ); + + expect( + cubit.state.agreedToTerms, + true, + ); + }); }); } diff --git a/packages/deriv_auth_ui/test/features/signup/cubits/deriv_country_selection_state_test.dart b/packages/deriv_auth_ui/test/features/signup/cubits/deriv_country_selection_state_test.dart deleted file mode 100644 index 146c66014..000000000 --- a/packages/deriv_auth_ui/test/features/signup/cubits/deriv_country_selection_state_test.dart +++ /dev/null @@ -1,39 +0,0 @@ -// ignore_for_file: always_specify_types - -import 'package:bloc_test/bloc_test.dart'; -import 'package:deriv_auth_ui/deriv_auth_ui.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - group('DerivCountrySelectionCubit', () { - late DerivCountrySelectionCubit cubit; - - setUp(() { - final countries = [ - const DerivResidenceModel( - code: 'us', name: 'United States', isDisabled: false), - const DerivResidenceModel( - code: 'fr', name: 'France', isDisabled: false), - const DerivResidenceModel( - code: 'de', name: 'Germany', isDisabled: false), - ]; - cubit = DerivCountrySelectionCubit(Future.value(countries)); - }); - - test('initial state is DerivCountrySelectionInitialState', () { - expect(cubit.state, const DerivCountrySelectionInitialState()); - }); - - blocTest( - 'emits DerivCountrySelectionLoadedState with filtered countries when fetchResidenceCounties is called', - build: () => cubit, - act: (cubit) => cubit.fetchResidenceCounties(), - expect: () => [ - const DerivCountrySelectionLoadedState([ - DerivResidenceModel( - code: 'us', name: 'United States', isDisabled: false), - ]), - ], - ); - }); -} diff --git a/packages/deriv_auth_ui/test/features/signup/layouts/deriv_set_password_layout_test.dart b/packages/deriv_auth_ui/test/features/signup/layouts/deriv_set_password_layout_test.dart index f2d186cad..28047562f 100644 --- a/packages/deriv_auth_ui/test/features/signup/layouts/deriv_set_password_layout_test.dart +++ b/packages/deriv_auth_ui/test/features/signup/layouts/deriv_set_password_layout_test.dart @@ -39,7 +39,6 @@ void main() { BlocProvider.value(value: signupCubit), ], child: DerivSetPasswordLayout( - onDerivAuthState: (_, __) {}, onDerivSignupState: (_, __) {}, onPreviousPressed: () {}, verificationCode: '123456', @@ -52,31 +51,6 @@ void main() { expect($(ElevatedButton), findsNWidgets(2)); }); - patrolWidgetTest('onDerivAuthState is called on DerivAuth state changes', - (PatrolTester $) async { - bool isOnDerivAuthStateCalled = false; - - when(() => authCubit.stream).thenAnswer((_) => Stream.fromIterable([ - DerivAuthLoadingState(), - DerivAuthLoggedOutState(), - ])); - - await $.pumpApp(MultiBlocProvider( - providers: [ - BlocProvider.value(value: authCubit), - BlocProvider.value(value: signupCubit), - ], - child: DerivSetPasswordLayout( - onDerivAuthState: (_, __) => isOnDerivAuthStateCalled = true, - onDerivSignupState: (_, __) {}, - onPreviousPressed: () {}, - verificationCode: 'verificationCode', - residence: 'residence'), - )); - - expect(isOnDerivAuthStateCalled, true); - }); - patrolWidgetTest( 'onDerivSignupState is called on DerivSignup state changes', (PatrolTester $) async { @@ -95,7 +69,6 @@ void main() { BlocProvider.value(value: signupCubit), ], child: DerivSetPasswordLayout( - onDerivAuthState: (_, __) {}, onDerivSignupState: (_, __) => isOnDerivSignupStateCalled = true, onPreviousPressed: () {}, @@ -106,6 +79,65 @@ void main() { expect(isOnDerivSignupStateCalled, true); }); + patrolWidgetTest('calls [AuthErrorStateHandler] on auth error state.', + (PatrolTester $) async { + final mockAuthState = DerivAuthErrorState( + isSocialLogin: false, + message: 'error', + type: AuthErrorType.failedAuthorization, + ); + + when(() => authCubit.state).thenReturn(mockAuthState); + when(() => authCubit.stream) + .thenAnswer((_) => Stream.fromIterable([mockAuthState])); + + await $.pumpApp(MultiBlocProvider( + providers: [ + BlocProvider.value(value: authCubit), + BlocProvider.value(value: signupCubit), + ], + child: DerivSetPasswordLayout( + onDerivSignupState: (_, __) {}, + onPreviousPressed: () {}, + verificationCode: 'verificationCode', + residence: 'residence'), + )); + + expect($(PopupAlertDialog).$('Authorization failed.'), findsOneWidget); + }); + + patrolWidgetTest('onAuthError is called on auth error state.', + (PatrolTester $) async { + final mockAuthState = DerivAuthErrorState( + isSocialLogin: false, + message: 'error', + type: AuthErrorType.failedAuthorization, + ); + + when(() => authCubit.state).thenReturn(mockAuthState); + when(() => authCubit.stream) + .thenAnswer((_) => Stream.fromIterable([mockAuthState])); + + bool isOnAuthErrorCalled = false; + + await $.pumpApp(MultiBlocProvider( + providers: [ + BlocProvider.value(value: authCubit), + BlocProvider.value(value: signupCubit), + ], + child: DerivSetPasswordLayout( + onDerivSignupState: (_, __) {}, + onPreviousPressed: () {}, + onAuthError: (_) { + isOnAuthErrorCalled = true; + }, + verificationCode: 'verificationCode', + residence: 'residence'), + )); + + expect(isOnAuthErrorCalled, true); + }); + patrolWidgetTest('onPreviousPressed is called upon tapping previous button', (PatrolTester $) async { bool isOnPreviousPressedCalled = false; @@ -118,7 +150,6 @@ void main() { BlocProvider.value(value: signupCubit), ], child: DerivSetPasswordLayout( - onDerivAuthState: (_, __) {}, onDerivSignupState: (_, __) {}, onPreviousPressed: () => isOnPreviousPressedCalled = true, verificationCode: 'verificationCode', @@ -141,7 +172,6 @@ void main() { BlocProvider.value(value: signupCubit), ], child: DerivSetPasswordLayout( - onDerivAuthState: (_, __) {}, onDerivSignupState: (_, __) {}, onPreviousPressed: () {}, verificationCode: 'verificationCode', @@ -171,7 +201,6 @@ void main() { BlocProvider.value(value: signupCubit), ], child: DerivSetPasswordLayout( - onDerivAuthState: (_, __) {}, onDerivSignupState: (_, __) {}, onPreviousPressed: () {}, verificationCode: 'verificationCode', diff --git a/packages/deriv_auth_ui/test/features/signup/layouts/deriv_signup_layout_test.dart b/packages/deriv_auth_ui/test/features/signup/layouts/deriv_signup_layout_test.dart index 8a7f59f25..820830810 100644 --- a/packages/deriv_auth_ui/test/features/signup/layouts/deriv_signup_layout_test.dart +++ b/packages/deriv_auth_ui/test/features/signup/layouts/deriv_signup_layout_test.dart @@ -15,33 +15,43 @@ import '../../../pump_app.dart'; void main() { group('DerivSignupLayout', () { late MockSignupCubit signupCubit; + late MockAuthCubit authCubit; const String signupPageLabel = 'Create a free account'; const String signupPageDescription = 'Start trading within minutes.'; setUpAll(() { signupCubit = MockSignupCubit(); + authCubit = MockAuthCubit(); when(() => signupCubit.state) .thenAnswer((_) => const DerivSignupInitialState()); when(() => signupCubit.stream).thenAnswer( (_) => Stream.fromIterable([const DerivSignupInitialState()])); + + when(() => authCubit.state).thenAnswer((_) => DerivAuthLoadingState()); + + when(() => authCubit.stream) + .thenAnswer((_) => Stream.fromIterable([DerivAuthLoadingState()])); }); patrolWidgetTest('renders correctly', (PatrolTester $) async { await $.pumpApp( settle: false, - BlocProvider.value( - value: signupCubit, - child: DerivSignupLayout( - signupPageLabel: signupPageLabel, - signupPageDescription: signupPageDescription, - onSocialAuthButtonPressed: (_) {}, - onSingupError: (_) {}, - onSingupEmailSent: (_) {}, - onSignupPressed: () {}, - onLoginTapped: () {}, + BlocProvider.value( + value: authCubit, + child: BlocProvider.value( + value: signupCubit, + child: DerivSignupLayout( + signupPageLabel: signupPageLabel, + signupPageDescription: signupPageDescription, + onSocialAuthButtonPressed: (_) {}, + onSingupError: (_) {}, + onSingupEmailSent: (_) {}, + onSignupPressed: () {}, + onLoginTapped: () {}, + ), ), )); @@ -56,18 +66,21 @@ void main() { (PatrolTester $) async { bool isOnSocialAuthButtonPressedCalled = false; - await $.pumpApp(BlocProvider.value( - value: signupCubit, - child: DerivSignupLayout( - signupPageLabel: signupPageLabel, - signupPageDescription: signupPageDescription, - onSocialAuthButtonPressed: (_) { - isOnSocialAuthButtonPressedCalled = true; - }, - onSingupError: (_) {}, - onSingupEmailSent: (_) {}, - onSignupPressed: () {}, - onLoginTapped: () {}, + await $.pumpApp(BlocProvider.value( + value: authCubit, + child: BlocProvider.value( + value: signupCubit, + child: DerivSignupLayout( + signupPageLabel: signupPageLabel, + signupPageDescription: signupPageDescription, + onSocialAuthButtonPressed: (_) { + isOnSocialAuthButtonPressedCalled = true; + }, + onSingupError: (_) {}, + onSingupEmailSent: (_) {}, + onSignupPressed: () {}, + onLoginTapped: () {}, + ), ), )); @@ -86,16 +99,18 @@ void main() { when(() => signupCubit.stream).thenAnswer( (_) => Stream.fromIterable([const DerivSignupEmailSentState()])); - await $.pumpApp(BlocProvider.value( - value: signupCubit, - child: DerivSignupLayout( - signupPageLabel: signupPageLabel, - signupPageDescription: signupPageDescription, - onSocialAuthButtonPressed: (_) {}, - onSingupError: (_) {}, - onSingupEmailSent: (_) => isOnSignupEmailSentCalled = true, - onSignupPressed: () {}, - onLoginTapped: () {}, + await $.pumpApp(BlocProvider.value( + value: authCubit, + child: BlocProvider.value( + value: signupCubit, + child: DerivSignupLayout( + signupPageLabel: signupPageLabel, + signupPageDescription: signupPageDescription, + onSocialAuthButtonPressed: (_) {}, + onSingupError: (_) {}, + onSingupEmailSent: (_) => isOnSignupEmailSentCalled = true, + onSignupPressed: () {}, + onLoginTapped: () {}), ), )); @@ -109,16 +124,18 @@ void main() { when(() => signupCubit.sendVerificationEmail('test@gmail.com')) .thenAnswer((_) async => const DerivSignupEmailSentState()); - await $.pumpApp(BlocProvider.value( - value: signupCubit, - child: DerivSignupLayout( - signupPageLabel: signupPageLabel, - signupPageDescription: signupPageDescription, - onSocialAuthButtonPressed: (_) {}, - onSingupError: (_) {}, - onSingupEmailSent: (_) {}, - onSignupPressed: () => isOnSignupPressedCalled = true, - onLoginTapped: () {}, + await $.pumpApp(BlocProvider.value( + value: authCubit, + child: BlocProvider.value( + value: signupCubit, + child: DerivSignupLayout( + signupPageLabel: signupPageLabel, + signupPageDescription: signupPageDescription, + onSocialAuthButtonPressed: (_) {}, + onSingupError: (_) {}, + onSingupEmailSent: (_) {}, + onSignupPressed: () => isOnSignupPressedCalled = true, + onLoginTapped: () {}), ), )); @@ -137,18 +154,20 @@ void main() { (PatrolTester $) async { bool isOnLoginTappedCalled = false; - await $.pumpApp(BlocProvider.value( - value: signupCubit, - child: DerivSignupLayout( - signupPageLabel: signupPageLabel, - signupPageDescription: signupPageDescription, - onSocialAuthButtonPressed: (_) {}, - onSingupError: (_) {}, - onSingupEmailSent: (_) {}, - onSignupPressed: () {}, - onLoginTapped: () { - isOnLoginTappedCalled = true; - }, + await $.pumpApp(BlocProvider.value( + value: authCubit, + child: BlocProvider.value( + value: signupCubit, + child: DerivSignupLayout( + signupPageLabel: signupPageLabel, + signupPageDescription: signupPageDescription, + onSocialAuthButtonPressed: (_) {}, + onSingupError: (_) {}, + onSingupEmailSent: (_) {}, + onSignupPressed: () {}, + onLoginTapped: () { + isOnLoginTappedCalled = true; + }), ), )); @@ -170,20 +189,85 @@ void main() { when(() => signupCubit.stream).thenAnswer( (_) => Stream.fromIterable([const DerivSignupErrorState('')])); - await $.pumpApp(BlocProvider.value( - value: signupCubit, - child: DerivSignupLayout( - signupPageLabel: signupPageLabel, - signupPageDescription: signupPageDescription, - onSocialAuthButtonPressed: (_) {}, - onSingupError: (_) => isOnSignupErrorCalled = true, - onSingupEmailSent: (_) {}, - onSignupPressed: () {}, - onLoginTapped: () {}, + await $.pumpApp(BlocProvider.value( + value: authCubit, + child: BlocProvider.value( + value: signupCubit, + child: DerivSignupLayout( + signupPageLabel: signupPageLabel, + signupPageDescription: signupPageDescription, + onSocialAuthButtonPressed: (_) {}, + onSingupError: (_) => isOnSignupErrorCalled = true, + onSingupEmailSent: (_) {}, + onSignupPressed: () {}, + onLoginTapped: () {}), ), )); expect(isOnSignupErrorCalled, true); }); + patrolWidgetTest('onAuthError is called upon auth error state', + (PatrolTester $) async { + bool isOnAuthErrorCalled = false; + + final mockAuthState = DerivAuthErrorState( + isSocialLogin: false, + message: 'error', + type: AuthErrorType.failedAuthorization, + ); + + when(() => authCubit.state).thenAnswer((_) => mockAuthState); + + when(() => authCubit.stream) + .thenAnswer((_) => Stream.fromIterable([mockAuthState])); + + await $.pumpApp(BlocProvider.value( + value: authCubit, + child: BlocProvider.value( + value: signupCubit, + child: DerivSignupLayout( + signupPageLabel: signupPageLabel, + signupPageDescription: signupPageDescription, + onSocialAuthButtonPressed: (_) {}, + onSingupError: (_) {}, + onSingupEmailSent: (_) {}, + onSignupPressed: () {}, + onAuthError: (_) => isOnAuthErrorCalled = true, + onLoginTapped: () {}), + ), + )); + + expect(isOnAuthErrorCalled, true); + }); + + patrolWidgetTest('calls [AuthErrorStateHandler] on auth error state.', + (PatrolTester $) async { + final mockAuthState = DerivAuthErrorState( + isSocialLogin: false, + message: 'error', + type: AuthErrorType.failedAuthorization, + ); + + when(() => authCubit.state).thenAnswer((_) => mockAuthState); + + when(() => authCubit.stream) + .thenAnswer((_) => Stream.fromIterable([mockAuthState])); + + await $.pumpApp(BlocProvider.value( + value: authCubit, + child: BlocProvider.value( + value: signupCubit, + child: DerivSignupLayout( + signupPageLabel: signupPageLabel, + signupPageDescription: signupPageDescription, + onSocialAuthButtonPressed: (_) {}, + onSingupError: (_) {}, + onSingupEmailSent: (_) {}, + onSignupPressed: () {}, + onLoginTapped: () {}), + ))); + + expect($(PopupAlertDialog).$('Authorization failed.'), findsOneWidget); + }); }); } diff --git a/packages/deriv_bloc_manager/README.md b/packages/deriv_bloc_manager/README.md index fecfc8c77..11b77964b 100644 --- a/packages/deriv_bloc_manager/README.md +++ b/packages/deriv_bloc_manager/README.md @@ -51,7 +51,7 @@ This section shows how to create a new bloc/cubit following the proposed archite ```dart abstract class BaseStateListener {} -abstract class AuthStateListener implements BaseStateListener { +abstract class DerivAuthStateListener implements BaseStateListener { void onLogin(Authorize authorizedAccount); void onLogout(); @@ -64,7 +64,7 @@ abstract class ConnectivityStateListener implements BaseStateListener { } ``` -2- Create a cubit for a feature, let’s call it `FeatureCubit`. The cubit class will implement both `AuthStateListener` and `ConnectivityStateListener` so it can expose the 4 methods in addition to any other feature-specific states. The type of the state `FeatureCubit` is managing in this example, is **Status** with initial value as `initial`; +2- Create a cubit for a feature, let’s call it `FeatureCubit`. The cubit class will implement both `DerivAuthStateListener` and `ConnectivityStateListener` so it can expose the 4 methods in addition to any other feature-specific states. The type of the state `FeatureCubit` is managing in this example, is **Status** with initial value as `initial`; ```dart enum Status { @@ -82,7 +82,7 @@ The `FeatureCubit` will expose the common/share `onConnect`, `onDisconnect`, `on ```dart import 'package:bloc/bloc.dart'; -class FeatureCubit extends Cubit implements ConnectivityStateListener, AuthStateListener { +class FeatureCubit extends Cubit implements ConnectivityStateListener, DerivAuthStateListener { FeatureCubit() : super(Status.initial); void loading() => emit(Status.loading); diff --git a/packages/deriv_date_range_picker/lib/src/date_range_picker.dart b/packages/deriv_date_range_picker/lib/src/date_range_picker.dart index f52a31216..3d5923708 100644 --- a/packages/deriv_date_range_picker/lib/src/date_range_picker.dart +++ b/packages/deriv_date_range_picker/lib/src/date_range_picker.dart @@ -237,7 +237,7 @@ class _DerivDateRangePickerState extends State { context.localization!.labelSelectedDateRange, style: context.theme.textStyle( textStyle: TextStyles.overline, - color: context.theme.colors.lessProminent, + color: context.theme.colors.general, ), ), ), diff --git a/packages/deriv_date_range_picker/lib/src/widgets/date_range_text_field.dart b/packages/deriv_date_range_picker/lib/src/widgets/date_range_text_field.dart index 0aa9a69bc..49440cca3 100644 --- a/packages/deriv_date_range_picker/lib/src/widgets/date_range_text_field.dart +++ b/packages/deriv_date_range_picker/lib/src/widgets/date_range_text_field.dart @@ -109,7 +109,7 @@ class _DateRangeTextFieldState extends State<_DateRangeTextField> { : TextInputAction.done, style: context.theme.textStyle( textStyle: TextStyles.subheading, - color: context.theme.colors.lessProminent, + color: context.theme.colors.general, ), decoration: InputDecoration( enabledBorder: OutlineInputBorder( diff --git a/packages/deriv_date_range_picker/lib/src/widgets/input_date_range.dart b/packages/deriv_date_range_picker/lib/src/widgets/input_date_range.dart index 9676a7bdf..50111d233 100644 --- a/packages/deriv_date_range_picker/lib/src/widgets/input_date_range.dart +++ b/packages/deriv_date_range_picker/lib/src/widgets/input_date_range.dart @@ -124,7 +124,7 @@ class _InputDateRangeState extends State { context.localization!.labelSelectedDateRange, style: context.theme.textStyle( textStyle: TextStyles.overline, - color: context.theme.colors.lessProminent, + color: context.theme.colors.general, ), ), ), diff --git a/packages/deriv_date_range_picker/lib/src/widgets/month_item.dart b/packages/deriv_date_range_picker/lib/src/widgets/month_item.dart index d9e6f5338..07a9167d7 100644 --- a/packages/deriv_date_range_picker/lib/src/widgets/month_item.dart +++ b/packages/deriv_date_range_picker/lib/src/widgets/month_item.dart @@ -157,7 +157,7 @@ class _MonthItemState extends State<_MonthItem> { localizations.formatMonthYear(widget.displayedMonth), style: context.theme.textStyle( textStyle: TextStyles.body1, - color: context.theme.colors.lessProminent, + color: context.theme.colors.general, ), ), ), @@ -197,7 +197,7 @@ class _MonthItemState extends State<_MonthItem> { TextStyle itemStyle = context.theme.textStyle( textStyle: TextStyles.body1, - color: context.theme.colors.lessProminent, + color: context.theme.colors.general, ); BoxDecoration? decoration; diff --git a/packages/deriv_numpad/lib/core/widgets/custom_checkbox.dart b/packages/deriv_numpad/lib/core/widgets/custom_checkbox.dart index 4f0b4938a..5a7476e56 100644 --- a/packages/deriv_numpad/lib/core/widgets/custom_checkbox.dart +++ b/packages/deriv_numpad/lib/core/widgets/custom_checkbox.dart @@ -50,7 +50,7 @@ class CustomCheckbox extends StatelessWidget { textAlign: TextAlign.left, style: context.theme.textStyle( textStyle: TextStyles.body1, - color: context.theme.colors.lessProminent, + color: context.theme.colors.general, ), ), ), diff --git a/packages/deriv_ui/lib/presentation/widgets/base_text_field.dart b/packages/deriv_ui/lib/presentation/widgets/base_text_field.dart index f50a1b9f1..60a9ee1c9 100644 --- a/packages/deriv_ui/lib/presentation/widgets/base_text_field.dart +++ b/packages/deriv_ui/lib/presentation/widgets/base_text_field.dart @@ -156,8 +156,7 @@ class _BaseTextFieldState extends State { ), focusedBorder: OutlineInputBorder( borderSide: BorderSide( - color: - widget.focusedBorderColor ?? context.theme.colors.blue, + color: widget.focusedBorderColor ?? context.theme.colors.blue, ), borderRadius: BorderRadius.circular(ThemeProvider.borderRadius04), ), @@ -174,8 +173,7 @@ class _BaseTextFieldState extends State { color: _hasError ? context.theme.colors.coral : _hasFocus() - ? widget.focusedLabelColor ?? - context.theme.colors.blue + ? widget.focusedLabelColor ?? context.theme.colors.blue : widget.labelColor ?? context.theme.colors.disabled, ), counterText: widget.showCounterText ? null : '', @@ -197,7 +195,7 @@ class _BaseTextFieldState extends State { Color _getTextFieldColor() => widget.enabled && _hasFocus() ? widget.focusedTextColor ?? context.theme.colors.prominent - : widget.textColor ?? context.theme.colors.lessProminent; + : widget.textColor ?? context.theme.colors.general; String? _validator(String? input) { final String? errorMsg = widget.validator?.call(input); diff --git a/packages/deriv_ui/lib/presentation/widgets/custom_checkbox.dart b/packages/deriv_ui/lib/presentation/widgets/custom_checkbox.dart index e47177718..352ec2afb 100644 --- a/packages/deriv_ui/lib/presentation/widgets/custom_checkbox.dart +++ b/packages/deriv_ui/lib/presentation/widgets/custom_checkbox.dart @@ -8,6 +8,8 @@ class CustomCheckbox extends StatelessWidget { required this.message, this.value = false, this.onValueChanged, + this.padding = const EdgeInsets.only(top: ThemeProvider.margin08), + this.contentsVerticalAlignment = CrossAxisAlignment.center, Key? key, }) : super(key: key); @@ -20,11 +22,24 @@ class CustomCheckbox extends StatelessWidget { /// A callback for the listeners of a checkbox, when the checkbox's value changes. final Function({bool? isChecked})? onValueChanged; + /// Padding of the checkbox. + final EdgeInsetsGeometry padding; + + /// The vertical alignment of the check and message within the row. + /// Defaults to [CrossAxisAlignment.center]. + /// + /// Example use case: + /// If the checkbox's message is longer than the width of the screen, + /// the message will be wrapped to the multiple line. Then, the message and + /// the checkbox can be aligned vertically top, center, or bottom of the row. + final CrossAxisAlignment contentsVerticalAlignment; + @override Widget build(BuildContext context) => Container( - padding: const EdgeInsets.only(top: ThemeProvider.margin08), + padding: padding, transform: Matrix4.translationValues(-14, 0, 0), child: Row( + crossAxisAlignment: contentsVerticalAlignment, children: [ Theme( data: ThemeData( @@ -47,7 +62,7 @@ class CustomCheckbox extends StatelessWidget { textAlign: TextAlign.left, style: context.theme.textStyle( textStyle: TextStyles.body1, - color: context.theme.colors.lessProminent, + color: context.theme.colors.general, ), ), onTap: onValueChanged == null diff --git a/packages/deriv_ui/lib/utils/popup_dialogs_helper.dart b/packages/deriv_ui/lib/utils/popup_dialogs_helper.dart index c70e69b7d..bb5e11de1 100644 --- a/packages/deriv_ui/lib/utils/popup_dialogs_helper.dart +++ b/packages/deriv_ui/lib/utils/popup_dialogs_helper.dart @@ -138,7 +138,7 @@ Future showSimpleLoadingDialog( bodyMessage, style: context.theme.textStyle( textStyle: TextStyles.body1, - color: context.theme.colors.lessProminent, + color: context.theme.colors.general, ), textAlign: TextAlign.center, ), @@ -172,7 +172,7 @@ Future showErrorDialog({ errorMessage ?? '', style: context.theme.textStyle( textStyle: TextStyles.body1, - color: context.theme.colors.lessProminent, + color: context.theme.colors.general, ), ), positiveActionLabel: actionLabel, @@ -195,7 +195,7 @@ Future showTokenExpiredDialog({ content, style: context.theme.textStyle( textStyle: TextStyles.body1, - color: context.theme.colors.lessProminent, + color: context.theme.colors.general, ), ), positiveActionLabel: positiveActionLabel, @@ -223,7 +223,7 @@ Future showAccountDeactivatedDialog({ text: content, style: context.theme.textStyle( textStyle: TextStyles.body1, - color: context.theme.colors.lessProminent, + color: context.theme.colors.general, ), children: [ buildTextSpanHyperlink( diff --git a/packages/deriv_ui/lib/utils/regex_helpers.dart b/packages/deriv_ui/lib/utils/regex_helpers.dart index 1a1d6e839..adfdfb75b 100644 --- a/packages/deriv_ui/lib/utils/regex_helpers.dart +++ b/packages/deriv_ui/lib/utils/regex_helpers.dart @@ -20,3 +20,10 @@ RegExp validPasswordLengthRegex = RegExp(r'^.{8,25}$'); /// Valid Password length for login. RegExp validLoginPasswordLengthRegex = RegExp(r'^.{6,25}$'); + +/// Check if [str] input contains only a-z letters and 0-9 numbers +bool hasOnlySmallLettersAndNumberInput(String str) => + RegExp('^[a-z0-9.]+\$').hasMatch(str); + +/// Check if [string] input contains only 0-9 numbers +bool hasOnlyNumberInput(String string) => RegExp('^[0-9]+\$').hasMatch(string); diff --git a/packages/deriv_ui/pubspec.yaml b/packages/deriv_ui/pubspec.yaml index 36734d6d8..a689b2616 100644 --- a/packages/deriv_ui/pubspec.yaml +++ b/packages/deriv_ui/pubspec.yaml @@ -12,12 +12,10 @@ dependencies: sdk: flutter deriv_theme: - path: - ../deriv_theme - # git: - # url: git@github.com:regentmarkets/flutter-deriv-packages.git - # path: packages/deriv_theme - # ref: dev + git: + url: git@github.com:regentmarkets/flutter-deriv-packages.git + path: packages/deriv_theme + ref: dev deriv_web_view: git: