diff --git a/lib/core/email_validator.dart b/lib/core/email_validator.dart new file mode 100644 index 000000000..23910db60 --- /dev/null +++ b/lib/core/email_validator.dart @@ -0,0 +1,11 @@ +import 'package:cake_wallet/core/validator.dart'; +import 'package:cake_wallet/generated/i18n.dart'; + +class EmailValidator extends TextValidator { + EmailValidator() + : super( + errorMessage: 'Invalid email address', + pattern: + '^[^@]+@[^@]+\.[^@]+', + ); +} diff --git a/lib/di.dart b/lib/di.dart index 0ef93709a..1eb835bcb 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -1,6 +1,8 @@ import 'package:cake_wallet/core/yat_service.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/entities/wake_lock.dart'; +import 'package:cake_wallet/ionia/ionia.dart'; +import 'package:cake_wallet/ionia/ionia_api.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/haven/haven.dart'; @@ -8,8 +10,10 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/src/screens/cake_pay/auth/create_account_page.dart'; import 'package:cake_wallet/src/screens/cake_pay/auth/forgot_password_page.dart'; import 'package:cake_wallet/src/screens/cake_pay/auth/login_page.dart'; +import 'package:cake_wallet/src/screens/cake_pay/auth/verify_otp_page.dart'; import 'package:cake_wallet/src/screens/cake_pay/cake_pay.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/balance_page.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_view_model.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cake_wallet/core/backup_service.dart'; import 'package:cw_core/wallet_service.dart'; @@ -645,13 +649,23 @@ Future setup( getIt.registerFactory(() => AddressResolver(yatService: getIt.get())); - getIt.registerFactory(() => WelcomePage()); - - getIt.registerFactory(() => LoginPage()); - - getIt.registerFactory(() => CreateAccountPage()); - getIt.registerFactory(() => ForgotPassword()); + getIt.registerFactory(() => IoniaApi()); + + getIt.registerFactory( + () => IoniaService(getIt.get(), getIt.get())); + + getIt.registerFactory(() => IoniaViewModel(ioniaService: getIt.get())); + + getIt.registerFactory(() => CreateAccountPage(getIt.get())); + + getIt.registerFactory(() => LoginPage(getIt.get())); + + getIt.registerFactory(() => VerifyIoniaOtp(getIt.get())); + + getIt.registerFactory(() => WelcomePage(getIt.get())); + + _isSetupFinished = true; } diff --git a/lib/ionia/ionia_create_state.dart b/lib/ionia/ionia_create_state.dart new file mode 100644 index 000000000..74f336db3 --- /dev/null +++ b/lib/ionia/ionia_create_state.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +abstract class IoniaCreateState {} + +class IoniaCreateStateSuccess extends IoniaCreateState {} + +class IoniaCreateStateLoading extends IoniaCreateState {} + +class IoniaCreateStateFailure extends IoniaCreateState { + IoniaCreateStateFailure({@required this.error}); + + final String error; +} + +abstract class IoniaOtpState {} + +class IoniaOtpValidating extends IoniaOtpState {} + +class IoniaOtpSuccess extends IoniaOtpState {} + +class IoniaOtpSendDisabled extends IoniaOtpState {} + +class IoniaOtpSendEnabled extends IoniaOtpState {} + +class IoniaOtpFailure extends IoniaOtpState { + IoniaOtpFailure({@required this.error}); + + final String error; +} diff --git a/lib/router.dart b/lib/router.dart index d9de19ef1..5dacb6035 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -8,6 +8,7 @@ import 'package:cake_wallet/src/screens/buy/pre_order_page.dart'; import 'package:cake_wallet/src/screens/cake_pay/auth/create_account_page.dart'; import 'package:cake_wallet/src/screens/cake_pay/auth/forgot_password_page.dart'; import 'package:cake_wallet/src/screens/cake_pay/auth/login_page.dart'; +import 'package:cake_wallet/src/screens/cake_pay/auth/verify_otp_page.dart'; import 'package:cake_wallet/src/screens/cake_pay/cake_pay.dart'; import 'package:cake_wallet/src/screens/order_details/order_details_page.dart'; import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart'; @@ -417,6 +418,9 @@ Route createRoute(RouteSettings settings) { case Routes.cakePayForgotPasswordPage: return CupertinoPageRoute(builder: (_) => getIt.get()); + + case Routes.verifyIoniaOtpPage: + return CupertinoPageRoute(builder: (_) => getIt.get()); default: return MaterialPageRoute( diff --git a/lib/routes.dart b/lib/routes.dart index f604b6df4..1b788ab12 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -64,4 +64,5 @@ class Routes { static const cakePayCreateAccountPage = '/cake_pay_create_account_page'; static const cakePayLoginPage = '/cake_pay_login_page'; static const cakePayForgotPasswordPage = '/cake_pay_forgot_password_page'; + static const verifyIoniaOtpPage = '/cake_pay_verify_otp_page'; } diff --git a/lib/src/screens/cake_pay/auth/create_account_page.dart b/lib/src/screens/cake_pay/auth/create_account_page.dart index 32ec228e8..0fdbf868c 100644 --- a/lib/src/screens/cake_pay/auth/create_account_page.dart +++ b/lib/src/screens/cake_pay/auth/create_account_page.dart @@ -1,15 +1,34 @@ +import 'package:cake_wallet/core/email_validator.dart'; +import 'package:cake_wallet/ionia/ionia_create_state.dart'; +import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_view_model.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:mobx/mobx.dart'; class CreateAccountPage extends BasePage { - CreateAccountPage() : _formKey = GlobalKey(); + final IoniaViewModel _ioniaViewModel; + + CreateAccountPage(this._ioniaViewModel) + : _emailFocus = FocusNode(), + _emailController = TextEditingController(), + _formKey = GlobalKey() { + _emailController.text = _ioniaViewModel.email; + _emailController.addListener(() => _ioniaViewModel.email = _emailController.text); + } final GlobalKey _formKey; + final FocusNode _emailFocus; + final TextEditingController _emailController; + @override Widget middle(BuildContext context) { return Text( @@ -24,21 +43,24 @@ class CreateAccountPage extends BasePage { @override Widget body(BuildContext context) { + reaction((_) => _ioniaViewModel.createUserState, (IoniaCreateState state) { + if (state is IoniaCreateStateFailure) { + _onCreateUserFailure(context, state.error); + } + if (state is IoniaCreateStateSuccess) { + _onCreateSuccessful(context); + } + }); + return ScrollableWithBottomSection( contentPadding: EdgeInsets.all(24), content: Form( key: _formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - BaseTextFormField( - hintText: 'Email Address *', - ), - SizedBox(height: 20), - BaseTextFormField( - hintText: 'Password *', - ), - ], + child: BaseTextFormField( + hintText: 'Email Address', + focusNode: _emailFocus, + validator: EmailValidator(), + controller: _emailController, ), ), bottomSectionPadding: EdgeInsets.symmetric(vertical: 36, horizontal: 24), @@ -47,11 +69,19 @@ class CreateAccountPage extends BasePage { Column( mainAxisAlignment: MainAxisAlignment.end, children: [ - PrimaryButton( - text: S.of(context).create_account, - onPressed: () {}, - color: Theme.of(context).accentTextTheme.body2.color, - textColor: Colors.white, + Observer( + builder: (_) => LoadingPrimaryButton( + text: S.of(context).create_account, + onPressed: () async { + if (!_formKey.currentState.validate()) { + return; + } + await _ioniaViewModel.createUser(_emailController.text); + }, + isLoading: _ioniaViewModel.createUserState is IoniaCreateStateLoading, + color: Theme.of(context).accentTextTheme.body2.color, + textColor: Colors.white, + ), ), SizedBox( height: 20, @@ -92,3 +122,17 @@ class CreateAccountPage extends BasePage { ); } } + +void _onCreateUserFailure(BuildContext context, String error) { + showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.current.create_account, + alertContent: error, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); +} + +void _onCreateSuccessful(BuildContext context) => Navigator.pushNamed(context, Routes.verifyIoniaOtpPage); diff --git a/lib/src/screens/cake_pay/auth/login_page.dart b/lib/src/screens/cake_pay/auth/login_page.dart index e427799a3..f2bfa194a 100644 --- a/lib/src/screens/cake_pay/auth/login_page.dart +++ b/lib/src/screens/cake_pay/auth/login_page.dart @@ -1,19 +1,36 @@ +import 'package:cake_wallet/core/email_validator.dart'; +import 'package:cake_wallet/ionia/ionia_create_state.dart'; import 'package:cake_wallet/palette.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_view_model.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:mobx/mobx.dart'; class LoginPage extends BasePage { - LoginPage() : _formKey = GlobalKey(); + final IoniaViewModel _ioniaViewModel; + LoginPage(this._ioniaViewModel) + : _formKey = GlobalKey(), + _emailFocus = FocusNode(), + _emailController = TextEditingController() { + _emailController.text = _ioniaViewModel.email; + _emailController.addListener(() => _ioniaViewModel.email = _emailController.text); + } final GlobalKey _formKey; @override Color get titleColor => Colors.black; + final FocusNode _emailFocus; + final TextEditingController _emailController; + @override Widget middle(BuildContext context) { return Text( @@ -28,21 +45,22 @@ class LoginPage extends BasePage { @override Widget body(BuildContext context) { + reaction((_) => _ioniaViewModel.createUserState, (IoniaCreateState state) { + if (state is IoniaCreateStateFailure) { + _onLoginUserFailure(context, state.error); + } + if (state is IoniaCreateStateSuccess) { + _onLoginSuccessful(context); + } + }); return ScrollableWithBottomSection( contentPadding: EdgeInsets.all(24), content: Form( key: _formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - BaseTextFormField( - hintText: 'Email Address', - ), - SizedBox(height: 20), - BaseTextFormField( - hintText: 'Password', - ), - ], + child: BaseTextFormField( + hintText: 'Email Address', + validator: EmailValidator(), + controller: _emailController, ), ), bottomSectionPadding: EdgeInsets.symmetric(vertical: 36, horizontal: 24), @@ -51,11 +69,19 @@ class LoginPage extends BasePage { Column( mainAxisAlignment: MainAxisAlignment.end, children: [ - PrimaryButton( - text: S.of(context).login, - onPressed: () {}, - color: Theme.of(context).accentTextTheme.body2.color, - textColor: Colors.white, + Observer( + builder: (_) => LoadingPrimaryButton( + text: S.of(context).login, + onPressed: () async { + if (!_formKey.currentState.validate()) { + return; + } + await _ioniaViewModel.createUser(_emailController.text); + }, + isLoading: _ioniaViewModel.createUserState is IoniaCreateStateLoading, + color: Theme.of(context).accentTextTheme.body2.color, + textColor: Colors.white, + ), ), SizedBox( height: 20, @@ -78,3 +104,17 @@ class LoginPage extends BasePage { ); } } + +void _onLoginUserFailure(BuildContext context, String error) { + showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.current.login, + alertContent: error, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); +} + +void _onLoginSuccessful(BuildContext context) => Navigator.pushNamed(context, Routes.verifyIoniaOtpPage); diff --git a/lib/src/screens/cake_pay/auth/verify_otp_page.dart b/lib/src/screens/cake_pay/auth/verify_otp_page.dart new file mode 100644 index 000000000..9a1b3f846 --- /dev/null +++ b/lib/src/screens/cake_pay/auth/verify_otp_page.dart @@ -0,0 +1,103 @@ +import 'package:cake_wallet/ionia/ionia_create_state.dart'; +import 'package:cake_wallet/palette.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +class VerifyIoniaOtp extends BasePage { + final IoniaViewModel _ioniaViewModel; + + VerifyIoniaOtp(this._ioniaViewModel) + : _codeController = TextEditingController(), + _codeFocus = FocusNode() { + _codeController.addListener(() { + final otp = _codeController.text; + _ioniaViewModel.otp = otp; + if (otp.length > 3) { + _ioniaViewModel.otpState = IoniaOtpSendEnabled(); + } else { + _ioniaViewModel.otpState = IoniaOtpSendDisabled(); + } + }); + } + + @override + Widget middle(BuildContext context) { + return Text( + S.current.verification, + style: TextStyle( + fontSize: 22, + fontFamily: 'Lato', + fontWeight: FontWeight.w900, + ), + ); + } + + final TextEditingController _codeController; + final FocusNode _codeFocus; + + @override + Widget body(BuildContext context) { + return ScrollableWithBottomSection( + contentPadding: EdgeInsets.all(24), + content: Column( + children: [ + BaseTextFormField( + hintText: 'Enter code', + focusNode: _codeFocus, + controller: _codeController, + ), + SizedBox(height: 14), + Text( + S.of(context).fill_code, + style: TextStyle(color: Color(0xff7A93BA), fontSize: 12), + ), + SizedBox(height: 34), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(S.of(context).dont_get_code), + SizedBox(width: 20), + Text( + S.of(context).resend_code, + style: TextStyle( + color: Palette.blueCraiola, + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ], + ), + bottomSectionPadding: EdgeInsets.symmetric(vertical: 36, horizontal: 24), + bottomSection: Column( + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Observer( + builder: (_) => LoadingPrimaryButton( + text: S.of(context).continue_text, + onPressed: () async => await _ioniaViewModel.verifyEmail(_codeController.text), + isDisabled: _ioniaViewModel.otpState is IoniaOtpSendDisabled, + isLoading: _ioniaViewModel.otpState is IoniaOtpValidating, + color: Theme.of(context).accentTextTheme.body2.color, + textColor: Colors.white, + ), + ), + SizedBox( + height: 20, + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/src/screens/cake_pay/auth/welcome_page.dart b/lib/src/screens/cake_pay/auth/welcome_page.dart index 234abb887..d75025c82 100644 --- a/lib/src/screens/cake_pay/auth/welcome_page.dart +++ b/lib/src/screens/cake_pay/auth/welcome_page.dart @@ -2,11 +2,17 @@ import 'package:cake_wallet/palette.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_view_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter/src/widgets/framework.dart'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:mobx/mobx.dart'; class WelcomePage extends BasePage { + final IoniaViewModel _ioniaViewModel; + + WelcomePage(this._ioniaViewModel); + @override Color get titleColor => Colors.black; @@ -24,6 +30,11 @@ class WelcomePage extends BasePage { @override Widget body(BuildContext context) { + reaction((_) => _ioniaViewModel.isLoggedIn, (bool state) { + if (state) { + // TODO:: Navigate to main page + } + }); return Padding( padding: const EdgeInsets.all(24.0), child: Column( diff --git a/lib/view_model/ionia/ionia_view_model.dart b/lib/view_model/ionia/ionia_view_model.dart new file mode 100644 index 000000000..04eaba6d8 --- /dev/null +++ b/lib/view_model/ionia/ionia_view_model.dart @@ -0,0 +1,58 @@ +import 'package:cake_wallet/ionia/ionia.dart'; +import 'package:cake_wallet/ionia/ionia_create_state.dart'; +import 'package:mobx/mobx.dart'; +part 'ionia_view_model.g.dart'; + +class IoniaViewModel = IoniaViewModelBase with _$IoniaViewModel; + +abstract class IoniaViewModelBase with Store { + IoniaViewModelBase({this.ioniaService}) + : createUserState = IoniaCreateStateSuccess(), + otpState = IoniaOtpSendDisabled() { + _getAuthStatus().then((value) => isLoggedIn = value); + } + + final IoniaService ioniaService; + + @observable + IoniaCreateState createUserState; + + @observable + IoniaOtpState otpState; + + @observable + String email; + + @observable + String otp; + + @observable + bool isLoggedIn; + + @action + Future createUser(String email) async { + createUserState = IoniaCreateStateLoading(); + try { + await ioniaService.createUser(email); + + createUserState = IoniaCreateStateSuccess(); + } catch (e) { + createUserState = IoniaCreateStateFailure(error: 'Something went wrong!'); + } + } + + @action + Future verifyEmail(String code) async { + try { + otpState = IoniaOtpValidating(); + await ioniaService.verifyEmail(code); + otpState = IoniaOtpSuccess(); + } catch (_) { + otpState = IoniaOtpFailure(error: 'Invalid OTP. Try again'); + } + } + + Future _getAuthStatus() async { + return await ioniaService.isLogined(); + } +} diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 02d1efc27..f8b2d407d 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -546,5 +546,9 @@ "manage_cards": "Manage Cards", "setup_your_debit_card": "Set up your debit card", "no_id_required": "No ID required. Top up and spend anywhere", - "how_to_use_card": "How to use this card" + "how_to_use_card": "How to use this card", + "verification": "Verification", + "fill_code": "Please fill in the verification code provided to your email", + "dont_get_code": "Don't get code", + "resend_code": "Please resend it" }