From 9ab2be35d297325a04ec4f4383dd20fc6923ce82 Mon Sep 17 00:00:00 2001 From: OmarHatem28 Date: Fri, 8 Jul 2022 16:49:22 +0200 Subject: [PATCH] Create CakePhone provider to handle cake phone APIs Add CakePhone Auth View Model to manage the authentication flow --- lib/di.dart | 8 ++ lib/entities/preferences_key.dart | 1 + lib/providers/cake_phone_provider.dart | 54 ++++++++++++++ lib/router.dart | 6 +- .../cake_phone/cake_phone_auth_page.dart | 73 ++++++++++++------- .../cake_phone_auth_view_model.dart | 47 ++++++++++++ 6 files changed, 160 insertions(+), 29 deletions(-) create mode 100644 lib/providers/cake_phone_provider.dart create mode 100644 lib/view_model/cake_phone/cake_phone_auth_view_model.dart diff --git a/lib/di.dart b/lib/di.dart index 7a2175aec..bf15ceb8c 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -5,10 +5,12 @@ import 'package:cake_wallet/entities/wake_lock.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/providers/cake_phone_provider.dart'; import 'package:cake_wallet/src/screens/cake_phone/phone_number_service/auto_renew_settings_page.dart'; import 'package:cake_wallet/src/screens/cake_phone/phone_number_service/number_settings_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/balance_page.dart'; import 'package:cake_wallet/view_model/cake_phone/add_balance_view_model.dart'; +import 'package:cake_wallet/view_model/cake_phone/cake_phone_auth_view_model.dart'; import 'package:cake_wallet/view_model/cake_phone/phone_plan_view_model.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cake_wallet/core/backup_service.dart'; @@ -672,5 +674,11 @@ Future setup( return AddBalancePage(addBalanceViewModel: getIt.get()); }); + getIt.registerFactory(() { + return CakePhoneAuthViewModel(getIt.get(), getIt.get()); + }); + + getIt.registerLazySingleton(() => CakePhoneProvider()); + _isSetupFinished = true; } diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 82e100c42..d86874b7e 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -22,4 +22,5 @@ class PreferencesKey { static const bitcoinTransactionPriority = 'current_fee_priority_bitcoin'; static const shouldShowReceiveWarning = 'should_show_receive_warning'; static const shouldShowYatPopup = 'should_show_yat_popup'; + static const cakePhoneTokenKey = 'cake_phone_token_key'; } diff --git a/lib/providers/cake_phone_provider.dart b/lib/providers/cake_phone_provider.dart new file mode 100644 index 000000000..0502dc374 --- /dev/null +++ b/lib/providers/cake_phone_provider.dart @@ -0,0 +1,54 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; + +class CakePhoneProvider { + final _baseUrl = 'cake-phone.cakewallet.com'; + + Future authenticate(String email) async { + try { + final headers = {'Content-Type': 'application/json'}; + final body = {"email": email}; + + final uri = Uri.https(_baseUrl, '/email'); + final response = await http.post(uri, headers: headers, body: json.encode(body)); + + if (response.statusCode != 200) { + debugPrint(response.body); + return false; + } + + return true; + } catch (err) { + debugPrint(err.toString()); + return false; + } + } + + Future verifyEmail({@required String email, @required String code}) async { + try { + final headers = {'Content-Type': 'application/json'}; + final body = { + "code": code, + "email": email, + }; + + final uri = Uri.https(_baseUrl, '/email/verify'); + final response = await http.post(uri, headers: headers, body: json.encode(body)); + + if (response.statusCode != 200) { + debugPrint(response.body); + return null; + } + + final responseJSON = json.decode(response.body) as Map; + final String token = responseJSON['token'] as String; + + return token; + } catch (err) { + debugPrint(err.toString()); + return null; + } + } +} diff --git a/lib/router.dart b/lib/router.dart index 731cde357..38770f148 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -15,6 +15,7 @@ import 'package:cake_wallet/src/screens/seed/pre_seed_page.dart'; import 'package:cake_wallet/src/screens/support/support_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart'; +import 'package:cake_wallet/view_model/cake_phone/cake_phone_auth_view_model.dart'; import 'package:cake_wallet/view_model/cake_phone/phone_plan_view_model.dart'; import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart'; import 'package:flutter/cupertino.dart'; @@ -351,7 +352,10 @@ Route createRoute(RouteSettings settings) { return MaterialPageRoute( settings: RouteSettings(name: Routes.cakePhoneAuth), - builder: (_) => CakePhoneAuthPage(isLogin: isLogin), + builder: (_) => CakePhoneAuthPage( + isLogin: isLogin, + cakePhoneAuthViewModel: getIt.get(), + ), ); case Routes.cakePhoneVerification: diff --git a/lib/src/screens/cake_phone/cake_phone_auth_page.dart b/lib/src/screens/cake_phone/cake_phone_auth_page.dart index dec780081..ceafeb2d6 100644 --- a/lib/src/screens/cake_phone/cake_phone_auth_page.dart +++ b/lib/src/screens/cake_phone/cake_phone_auth_page.dart @@ -1,5 +1,7 @@ import 'package:cake_wallet/palette.dart'; import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; +import 'package:cake_wallet/utils/show_bar.dart'; +import 'package:flushbar/flushbar.dart'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:cake_wallet/routes.dart'; @@ -7,14 +9,18 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/view_model/cake_phone/cake_phone_auth_view_model.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/core/execution_state.dart'; class CakePhoneAuthPage extends BasePage { - CakePhoneAuthPage({@required this.isLogin}); + CakePhoneAuthPage({@required this.isLogin, @required this.cakePhoneAuthViewModel}); final bool isLogin; + final CakePhoneAuthViewModel cakePhoneAuthViewModel; @override - Widget body(BuildContext context) => CakePhoneAuthBody(isLogin); + Widget body(BuildContext context) => CakePhoneAuthBody(isLogin, cakePhoneAuthViewModel); @override Widget middle(BuildContext context) { @@ -30,9 +36,10 @@ class CakePhoneAuthPage extends BasePage { } class CakePhoneAuthBody extends StatefulWidget { - CakePhoneAuthBody(this.isLogin); + CakePhoneAuthBody(this.isLogin, this.cakePhoneAuthViewModel); final bool isLogin; + final CakePhoneAuthViewModel cakePhoneAuthViewModel; @override CakePhoneAuthBodyState createState() => CakePhoneAuthBodyState(); @@ -44,6 +51,36 @@ class CakePhoneAuthBodyState extends State { AutovalidateMode _autoValidate = AutovalidateMode.disabled; + ReactionDisposer _reaction; + Flushbar _authBar; + + @override + void initState() { + super.initState(); + + _reaction ??= reaction((_) => widget.cakePhoneAuthViewModel.state, (ExecutionState state) { + if (state is ExecutedSuccessfullyState) { + _authBar?.dismiss(); + WidgetsBinding.instance.addPostFrameCallback((_) { + Navigator.pushNamed(context, Routes.cakePhoneVerification, arguments: _emailController.text); + }); + } + + if (state is IsExecutingState) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _authBar = createBar(S.of(context).authentication, duration: null)..show(context); + }); + } + + if (state is FailureState) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _authBar?.dismiss(); + showBar(context, S.of(context).failed_authentication(state.error)); + }); + } + }); + } + @override Widget build(BuildContext context) { return Container( @@ -73,10 +110,12 @@ class CakePhoneAuthBodyState extends State { children: [ PrimaryButton( onPressed: () { - if (widget.isLogin) { - _loginCakePhone(); + if (_formKey.currentState.validate()) { + widget.cakePhoneAuthViewModel.auth(_emailController.text); } else { - _registerCakePhone(); + setState(() { + _autoValidate = AutovalidateMode.always; + }); } }, text: widget.isLogin ? S.of(context).login : S.of(context).create_account, @@ -127,26 +166,4 @@ class CakePhoneAuthBodyState extends State { ), ); } - - void _registerCakePhone() { - // TODO: Add Registration logic - if (_formKey.currentState.validate()) { - Navigator.pushNamed(context, Routes.cakePhoneVerification); - } else { - setState(() { - _autoValidate = AutovalidateMode.always; - }); - } - } - - void _loginCakePhone() { - // TODO: Add Login logic - if (_formKey.currentState.validate()) { - Navigator.pushNamed(context, Routes.cakePhoneVerification); - } else { - setState(() { - _autoValidate = AutovalidateMode.always; - }); - } - } } diff --git a/lib/view_model/cake_phone/cake_phone_auth_view_model.dart b/lib/view_model/cake_phone/cake_phone_auth_view_model.dart new file mode 100644 index 000000000..ee5507988 --- /dev/null +++ b/lib/view_model/cake_phone/cake_phone_auth_view_model.dart @@ -0,0 +1,47 @@ +import 'dart:async'; +import 'package:cake_wallet/entities/preferences_key.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/providers/cake_phone_provider.dart'; +import 'package:cake_wallet/core/execution_state.dart'; + +part 'cake_phone_auth_view_model.g.dart'; + +class CakePhoneAuthViewModel = CakePhoneAuthViewModelBase with _$CakePhoneAuthViewModel; + +abstract class CakePhoneAuthViewModelBase with Store { + CakePhoneAuthViewModelBase(this._cakePhoneProvider, this._secureStorage) { + state = InitialExecutionState(); + } + + @observable + ExecutionState state; + + final CakePhoneProvider _cakePhoneProvider; + final FlutterSecureStorage _secureStorage; + + @action + Future auth(String email) async { + state = IsExecutingState(); + + final isSuccessfullyAuthenticated = await _cakePhoneProvider.authenticate(email); + + if (isSuccessfullyAuthenticated) { + state = ExecutedSuccessfullyState(); + } else { + state = FailureState(""); + } + } + + @action + Future verify(String email, String code) async { + final String token = await _cakePhoneProvider.verifyEmail(email: email, code: code); + + if (token != null) { + state = ExecutedSuccessfullyState(); + await _secureStorage.write(key: PreferencesKey.cakePhoneTokenKey, value: token); + } else { + state = FailureState(""); + } + } +}