diff --git a/assets/images/badge_discount.png b/assets/images/badge_discount.png new file mode 100644 index 000000000..64c8789c5 Binary files /dev/null and b/assets/images/badge_discount.png differ diff --git a/assets/images/card.png b/assets/images/card.png new file mode 100644 index 000000000..58935bdac Binary files /dev/null and b/assets/images/card.png differ diff --git a/assets/images/filter.png b/assets/images/filter.png new file mode 100644 index 000000000..dc47944c0 Binary files /dev/null and b/assets/images/filter.png differ diff --git a/assets/images/mastercard.png b/assets/images/mastercard.png new file mode 100644 index 000000000..2a80f52fe Binary files /dev/null and b/assets/images/mastercard.png differ diff --git a/assets/images/profile.png b/assets/images/profile.png new file mode 100644 index 000000000..d7dfe2508 Binary files /dev/null and b/assets/images/profile.png differ diff --git a/assets/images/search_icon.png b/assets/images/search_icon.png new file mode 100644 index 000000000..d9d71a4d7 Binary files /dev/null and b/assets/images/search_icon.png differ diff --git a/assets/images/wifi.png b/assets/images/wifi.png new file mode 100644 index 000000000..7834ef1c3 Binary files /dev/null and b/assets/images/wifi.png differ diff --git a/cw_haven/pubspec.lock b/cw_haven/pubspec.lock index 37e6b72b7..f9b6c4890 100644 --- a/cw_haven/pubspec.lock +++ b/cw_haven/pubspec.lock @@ -169,13 +169,6 @@ packages: relative: true source: path version: "0.0.1" - cw_monero: - dependency: "direct main" - description: - path: "../cw_monero" - relative: true - source: path - version: "0.0.1" dart_style: dependency: transitive description: 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 45760f1db..61a2e9fea 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -1,11 +1,15 @@ 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'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/src/screens/ionia/ionia.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'; @@ -641,5 +645,36 @@ Future setup( getIt.registerFactory(() => AddressResolver(yatService: getIt.get())); + getIt.registerFactory(() => IoniaApi()); + + getIt.registerFactory( + () => IoniaService(getIt.get(), getIt.get())); + + getIt.registerFactory(() => IoniaViewModel(ioniaService: getIt.get())); + + getIt.registerFactory(() => IoniaCreateAccountPage(getIt.get())); + + getIt.registerFactory(() => IoniaLoginPage(getIt.get())); + + getIt.registerFactoryParam((List args, _) { + final email = args.first as String; + final ioniaViewModel = args[1] as IoniaViewModel; + + return IoniaVerifyIoniaOtp(ioniaViewModel, email); + }); + + getIt.registerFactory(() => IoniaWelcomePage(getIt.get())); + + getIt.registerFactory(() => IoniaBuyGiftCardPage()); + + getIt.registerFactory(() => IoniaBuyGiftCardDetailPage()); + + getIt.registerFactory(() => IoniaManageCardsPage(getIt.get())); + + getIt.registerFactory(() => IoniaDebitCardPage(getIt.get())); + + getIt.registerFactory(() => IoniaActivateDebitCardPage(getIt.get())); + + _isSetupFinished = true; } diff --git a/lib/ionia/ionia_api.dart b/lib/ionia/ionia_api.dart index 2ef367c36..10169c779 100644 --- a/lib/ionia/ionia_api.dart +++ b/lib/ionia/ionia_api.dart @@ -88,7 +88,7 @@ class IoniaApi { final isSuccessful = bodyJson['Successful'] as bool; if (!isSuccessful) { - throw Exception(data['ErrorMessage'] as String); + throw Exception(data['message'] as String); } final virtualCard = data['VirtualCard'] as Map; @@ -117,7 +117,7 @@ class IoniaApi { final isSuccessful = bodyJson['Successful'] as bool; if (!isSuccessful) { - throw Exception(data['ErrorMessage'] as String); + throw Exception(data['message'] as String); } return IoniaVirtualCard.fromMap(data); diff --git a/lib/ionia/ionia_create_state.dart b/lib/ionia/ionia_create_state.dart new file mode 100644 index 000000000..199d72c73 --- /dev/null +++ b/lib/ionia/ionia_create_state.dart @@ -0,0 +1,56 @@ +import 'package:cake_wallet/ionia/ionia_virtual_card.dart'; +import 'package:flutter/material.dart'; + +abstract class IoniaCreateAccountState {} + +class IoniaCreateStateSuccess extends IoniaCreateAccountState {} + +class IoniaCreateStateLoading extends IoniaCreateAccountState {} + +class IoniaCreateStateFailure extends IoniaCreateAccountState { + 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; +} + +class IoniaCreateCardState {} + +class IoniaCreateCardSuccess extends IoniaCreateCardState {} + +class IoniaCreateCardLoading extends IoniaCreateCardState {} + +class IoniaCreateCardFailure extends IoniaCreateCardState { + IoniaCreateCardFailure({@required this.error}); + + final String error; +} + +class IoniaFetchCardState {} + +class IoniaNoCardState extends IoniaFetchCardState {} + +class IoniaFetchingCard extends IoniaFetchCardState {} + +class IoniaFetchCardFailure extends IoniaFetchCardState {} + +class IoniaCardSuccess extends IoniaFetchCardState { + IoniaCardSuccess({@required this.card}); + + final IoniaVirtualCard card; +} diff --git a/lib/router.dart b/lib/router.dart index acfb18684..930424019 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -70,6 +70,7 @@ import 'package:hive/hive.dart'; import 'package:cake_wallet/wallet_type_utils.dart'; import 'package:cake_wallet/wallet_types.g.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/address_page.dart'; +import 'package:cake_wallet/src/screens/ionia/ionia.dart'; RouteSettings currentRouteSettings; @@ -402,6 +403,34 @@ Route createRoute(RouteSettings settings) { getIt.get( param1: args)); + case Routes.ioniaWelcomePage: + return CupertinoPageRoute(builder: (_) => getIt.get()); + + case Routes.ioniaLoginPage: + return CupertinoPageRoute( builder: (_) => getIt.get()); + + case Routes.ioniaCreateAccountPage: + return CupertinoPageRoute( builder: (_) => getIt.get()); + + case Routes.ioniaManageCardsPage: + return CupertinoPageRoute(builder: (_) => getIt.get()); + + case Routes.ioniaBuyGiftCardPage: + return CupertinoPageRoute(builder: (_) => getIt.get()); + + case Routes.ioniaBuyGiftCardDetailPage: + return CupertinoPageRoute(builder: (_) => getIt.get()); + + case Routes.ioniaVerifyIoniaOtpPage: + final args = settings.arguments as List; + return CupertinoPageRoute(builder: (_) =>getIt.get(param1: args)); + + case Routes.ioniaDebitCardPage: + return CupertinoPageRoute(builder: (_) => getIt.get()); + + case Routes.ioniaActivateDebitCardPage: + return CupertinoPageRoute(builder: (_) => getIt.get()); + default: return MaterialPageRoute( builder: (_) => Scaffold( diff --git a/lib/routes.dart b/lib/routes.dart index 23e236023..4ab4c0b26 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -60,4 +60,14 @@ class Routes { static const moneroRestoreWalletFromWelcome = '/monero_restore_wallet'; static const moneroNewWalletFromWelcome = '/monero_new_wallet'; static const addressPage = '/address_page'; -} \ No newline at end of file + static const ioniaWelcomePage = '/cake_pay_welcome_page'; + static const ioniaCreateAccountPage = '/cake_pay_create_account_page'; + static const ioniaLoginPage = '/cake_pay_login_page'; + static const ioniaManageCardsPage = '/manage_cards_page'; + static const ioniaBuyGiftCardPage = '/buy_gift_card_page'; + static const ioniaBuyGiftCardDetailPage = '/buy_gift_card_detail_page'; + static const ioniaVerifyIoniaOtpPage = '/cake_pay_verify_otp_page'; + static const ioniaDebitCardPage = '/debit_card_page'; + static const ioniaActivateDebitCardPage = '/activate_debit_card_page'; + +} diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index eb8c9ab17..f11398564 100644 --- a/lib/src/screens/dashboard/dashboard_page.dart +++ b/lib/src/screens/dashboard/dashboard_page.dart @@ -1,8 +1,8 @@ import 'dart:async'; +import 'package:cake_wallet/src/screens/dashboard/widgets/market_place_page.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/src/screens/yat/yat_popup.dart'; import 'package:cake_wallet/src/screens/yat_emoji_id.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/themes/theme_base.dart'; @@ -14,19 +14,15 @@ import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/menu_widget.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/action_button.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/balance_page.dart'; -import 'package:cake_wallet/src/screens/dashboard/widgets/address_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/transactions_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:mobx/mobx.dart'; import 'package:smooth_page_indicator/smooth_page_indicator.dart'; -import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:cake_wallet/main.dart'; -import 'package:cake_wallet/router.dart'; import 'package:cake_wallet/buy/moonpay/moonpay_buy_provider.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'package:cake_wallet/wallet_type_utils.dart'; class DashboardPage extends BasePage { DashboardPage({ @@ -85,7 +81,7 @@ class DashboardPage extends BasePage { final DashboardViewModel walletViewModel; final WalletAddressListViewModel addressListViewModel; - final controller = PageController(initialPage: 0); + final controller = PageController(initialPage: 1); var pages = []; bool _isEffectsInstalled = false; @@ -221,7 +217,7 @@ class DashboardPage extends BasePage { if (_isEffectsInstalled) { return; } - + pages.add(MarketPlacePage()); pages.add(balancePage); pages.add(TransactionsPage(dashboardViewModel: walletViewModel)); _isEffectsInstalled = true; diff --git a/lib/src/screens/dashboard/widgets/market_place_page.dart b/lib/src/screens/dashboard/widgets/market_place_page.dart new file mode 100644 index 000000000..b616e955f --- /dev/null +++ b/lib/src/screens/dashboard/widgets/market_place_page.dart @@ -0,0 +1,52 @@ +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/widgets/market_place_item.dart'; +import 'package:flutter/material.dart'; +import 'package:cake_wallet/generated/i18n.dart'; + +class MarketPlacePage extends StatelessWidget { + final _scrollController = ScrollController(); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: RawScrollbar( + thumbColor: Colors.white.withOpacity(0.15), + radius: Radius.circular(20), + isAlwaysShown: true, + thickness: 2, + controller: _scrollController, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 50), + Text( + S.of(context).market_place, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.w500, + color: Theme.of(context).accentTextTheme.display3.backgroundColor, + ), + ), + Expanded( + child: ListView( + controller: _scrollController, + children: [ + SizedBox(height: 20), + MarketPlaceItem( + onTap: () => Navigator.of(context).pushNamed(Routes.ioniaWelcomePage), + title: S.of(context).cake_pay_title, + subTitle: S.of(context).cake_pay_subtitle, + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/screens/ionia/auth/ionia_create_account_page.dart b/lib/src/screens/ionia/auth/ionia_create_account_page.dart new file mode 100644 index 000000000..3fd912404 --- /dev/null +++ b/lib/src/screens/ionia/auth/ionia_create_account_page.dart @@ -0,0 +1,143 @@ +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 IoniaCreateAccountPage extends BasePage { + IoniaCreateAccountPage(this._ioniaViewModel) + : _emailFocus = FocusNode(), + _emailController = TextEditingController(), + _formKey = GlobalKey() { + _emailController.text = _ioniaViewModel.email; + _emailController.addListener(() => _ioniaViewModel.email = _emailController.text); + } + + final IoniaViewModel _ioniaViewModel; + + final GlobalKey _formKey; + + final FocusNode _emailFocus; + final TextEditingController _emailController; + + @override + Widget middle(BuildContext context) { + return Text( + S.current.sign_up, + style: TextStyle( + fontSize: 22, + fontFamily: 'Lato', + color: Theme.of(context).accentTextTheme.display4.backgroundColor, + fontWeight: FontWeight.w900, + ), + ); + } + + @override + Widget body(BuildContext context) { + reaction((_) => _ioniaViewModel.createUserState, (IoniaCreateAccountState state) { + if (state is IoniaCreateStateFailure) { + _onCreateUserFailure(context, state.error); + } + if (state is IoniaCreateStateSuccess) { + _onCreateSuccessful(context, _ioniaViewModel); + } + }); + + return ScrollableWithBottomSection( + contentPadding: EdgeInsets.all(24), + content: Form( + key: _formKey, + child: BaseTextFormField( + hintText: S.of(context).email_address, + focusNode: _emailFocus, + validator: EmailValidator(), + controller: _emailController, + ), + ), + bottomSectionPadding: EdgeInsets.symmetric(vertical: 36, horizontal: 24), + bottomSection: Column( + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + 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, + ), + RichText( + textAlign: TextAlign.center, + text: TextSpan( + text: S.of(context).agree_to, + style: TextStyle( + color: Color(0xff7A93BA), + fontSize: 12, + fontFamily: 'Lato', + ), + children: [ + TextSpan( + text: S.of(context).settings_terms_and_conditions, + style: TextStyle( + color: Theme.of(context).accentTextTheme.body2.color, + fontWeight: FontWeight.w700, + ), + ), + TextSpan(text: ' ${S.of(context).and} '), + TextSpan( + text: S.of(context).privacy_policy, + style: TextStyle( + color: Theme.of(context).accentTextTheme.body2.color, + fontWeight: FontWeight.w700, + ), + ), + TextSpan(text: ' ${S.of(context).by_cake_pay}'), + ], + ), + ), + ], + ), + ], + ), + ); + } + + 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, IoniaViewModel ioniaViewModel) => Navigator.pushNamed( + context, + Routes.ioniaVerifyIoniaOtpPage, + arguments: [ioniaViewModel.email, ioniaViewModel], + ); +} diff --git a/lib/src/screens/ionia/auth/ionia_login_page.dart b/lib/src/screens/ionia/auth/ionia_login_page.dart new file mode 100644 index 000000000..584682341 --- /dev/null +++ b/lib/src/screens/ionia/auth/ionia_login_page.dart @@ -0,0 +1,112 @@ +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 IoniaLoginPage extends BasePage { + IoniaLoginPage(this._ioniaViewModel) + : _formKey = GlobalKey(), + _emailController = TextEditingController() { + _emailController.text = _ioniaViewModel.email; + _emailController.addListener(() => _ioniaViewModel.email = _emailController.text); + } + + final GlobalKey _formKey; + + final IoniaViewModel _ioniaViewModel; + + @override + Color get titleColor => Colors.black; + + final TextEditingController _emailController; + + @override + Widget middle(BuildContext context) { + return Text( + S.current.login, + style: TextStyle( + fontSize: 22, + fontFamily: 'Lato', + fontWeight: FontWeight.w900, + ), + ); + } + + @override + Widget body(BuildContext context) { + reaction((_) => _ioniaViewModel.createUserState, (IoniaCreateAccountState state) { + if (state is IoniaCreateStateFailure) { + _onLoginUserFailure(context, state.error); + } + if (state is IoniaCreateStateSuccess) { + _onLoginSuccessful(context, _ioniaViewModel); + } + }); + return ScrollableWithBottomSection( + contentPadding: EdgeInsets.all(24), + content: Form( + key: _formKey, + child: BaseTextFormField( + hintText: S.of(context).email_address, + validator: EmailValidator(), + controller: _emailController, + ), + ), + bottomSectionPadding: EdgeInsets.symmetric(vertical: 36, horizontal: 24), + bottomSection: Column( + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + 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, + ), + ], + ), + ], + ), + ); + } + + 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, IoniaViewModel ioniaViewModel) => Navigator.pushNamed( + context, + Routes.ioniaVerifyIoniaOtpPage, + arguments: [ioniaViewModel.email, ioniaViewModel], + ); +} diff --git a/lib/src/screens/ionia/auth/ionia_verify_otp_page.dart b/lib/src/screens/ionia/auth/ionia_verify_otp_page.dart new file mode 100644 index 000000000..2af82350a --- /dev/null +++ b/lib/src/screens/ionia/auth/ionia_verify_otp_page.dart @@ -0,0 +1,132 @@ +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/typography.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 IoniaVerifyIoniaOtp extends BasePage { + + IoniaVerifyIoniaOtp(this._ioniaViewModel, this._email) + : _codeController = TextEditingController(), + _codeFocus = FocusNode() { + _codeController.addListener(() { + final otp = _codeController.text; + _ioniaViewModel.otp = otp; + if (otp.length > 3) { + _ioniaViewModel.otpState = IoniaOtpSendEnabled(); + } else { + _ioniaViewModel.otpState = IoniaOtpSendDisabled(); + } + }); + } + + final IoniaViewModel _ioniaViewModel; + + final String _email; + + @override + Widget middle(BuildContext context) { + return Text( + S.current.verification, + style: TextStyle( + fontSize: 22, + fontFamily: 'Lato', + color: Theme.of(context).accentTextTheme.display4.backgroundColor, + fontWeight: FontWeight.w900, + ), + ); + } + + final TextEditingController _codeController; + final FocusNode _codeFocus; + + @override + Widget body(BuildContext context) { + reaction((_) => _ioniaViewModel.otpState, (IoniaOtpState state) { + if (state is IoniaOtpFailure) { + _onOtpFailure(context, state.error); + } + if (state is IoniaOtpSuccess) { + _onOtpSuccessful(context); + } + }); + return ScrollableWithBottomSection( + contentPadding: EdgeInsets.all(24), + content: Column( + children: [ + BaseTextFormField( + hintText: S.of(context).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), + InkWell( + onTap: () => _ioniaViewModel.createUser(_email), + child: Text( + S.of(context).resend_code, + style: textSmallSemiBold(color: Palette.blueCraiola), + ), + ), + ], + ), + ], + ), + 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), + ], + ), + ], + ), + ); + } + + void _onOtpFailure(BuildContext context, String error) { + showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.current.verification, + alertContent: error, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); + } + + void _onOtpSuccessful(BuildContext context) => + Navigator.pushNamedAndRemoveUntil(context, Routes.ioniaManageCardsPage, ModalRoute.withName(Routes.dashboard)); +} diff --git a/lib/src/screens/ionia/auth/ionia_welcome_page.dart b/lib/src/screens/ionia/auth/ionia_welcome_page.dart new file mode 100644 index 000000000..0c4abecab --- /dev/null +++ b/lib/src/screens/ionia/auth/ionia_welcome_page.dart @@ -0,0 +1,104 @@ +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 IoniaWelcomePage extends BasePage { + IoniaWelcomePage(this._ioniaViewModel); + + @override + Widget middle(BuildContext context) { + return Text( + S.current.welcome_to_cakepay, + style: TextStyle( + fontSize: 22, + fontFamily: 'Lato', + color: Theme.of(context).accentTextTheme.display4.backgroundColor, + fontWeight: FontWeight.w900, + ), + ); + } + + final IoniaViewModel _ioniaViewModel; + + @override + Widget body(BuildContext context) { + reaction((_) => _ioniaViewModel.isLoggedIn, (bool state) { + if (state) { + Navigator.pushReplacementNamed(context, Routes.ioniaDebitCardPage); + } + }); + return Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + children: [ + SizedBox(height: 100), + Text( + S.of(context).about_cake_pay, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w400, + fontFamily: 'Lato', + color: Theme.of(context).primaryTextTheme.title.color, + ), + ), + SizedBox(height: 20), + Text( + S.of(context).cake_pay_account_note, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w400, + fontFamily: 'Lato', + color: Theme.of(context).primaryTextTheme.title.color, + ), + ), + ], + ), + Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + PrimaryButton( + text: S.of(context).create_account, + onPressed: () => Navigator.of(context).pushNamed(Routes.ioniaCreateAccountPage), + color: Theme.of(context).accentTextTheme.body2.color, + textColor: Colors.white, + ), + SizedBox( + height: 16, + ), + Text( + S.of(context).already_have_account, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + fontFamily: 'Lato', + color: Theme.of(context).primaryTextTheme.title.color, + ), + ), + SizedBox(height: 8), + InkWell( + onTap: () => Navigator.of(context).pushNamed(Routes.ioniaLoginPage), + child: Text( + S.of(context).login, + style: TextStyle( + color: Palette.blueCraiola, + fontSize: 16, + fontWeight: FontWeight.w900, + ), + ), + ) + ], + ) + ], + ), + ); + } +} diff --git a/lib/src/screens/ionia/cards/ionia_activate_debit_card_page.dart b/lib/src/screens/ionia/cards/ionia_activate_debit_card_page.dart new file mode 100644 index 000000000..91fd8615c --- /dev/null +++ b/lib/src/screens/ionia/cards/ionia_activate_debit_card_page.dart @@ -0,0 +1,116 @@ +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/screens/ionia/widgets/text_icon_button.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.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:mobx/mobx.dart'; + +class IoniaActivateDebitCardPage extends BasePage { + + IoniaActivateDebitCardPage(this._ioniaViewModel); + + final IoniaViewModel _ioniaViewModel; + + @override + Widget middle(BuildContext context) { + return Text( + S.current.debit_card, + style: TextStyle( + fontSize: 22, + color: Theme.of(context).accentTextTheme.display4.backgroundColor, + fontFamily: 'Lato', + fontWeight: FontWeight.w900, + ), + ); + } + + @override + Widget body(BuildContext context) { + reaction((_) => _ioniaViewModel.createCardState, (IoniaCreateCardState state) { + if (state is IoniaCreateCardFailure) { + _onCreateCardFailure(context, state.error); + } + if (state is IoniaCreateCardSuccess) { + _onCreateCardSuccess(context); + } + }); + return ScrollableWithBottomSection( + contentPadding: EdgeInsets.zero, + content: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + SizedBox(height: 16), + Text(S.of(context).debit_card_terms), + SizedBox(height: 24), + Text(S.of(context).please_reference_document), + SizedBox(height: 40), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Column( + children: [ + TextIconButton( + label: S.current.cardholder_agreement, + onTap: () {}, + ), + SizedBox( + height: 24, + ), + TextIconButton( + label: S.current.e_sign_consent, + onTap: () {}, + ), + ], + ), + ), + ], + ), + ), + bottomSection: LoadingPrimaryButton( + onPressed: () { + _ioniaViewModel.createCard(); + }, + isLoading: _ioniaViewModel.createCardState is IoniaCreateCardLoading, + text: S.of(context).agree_and_continue, + color: Theme.of(context).accentTextTheme.body2.color, + textColor: Colors.white, + ), + ); + } + + void _onCreateCardFailure(BuildContext context, String errorMessage) { + showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.current.error, + alertContent: errorMessage, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); + } + + void _onCreateCardSuccess(BuildContext context) { + Navigator.pushNamed( + context, + Routes.ioniaDebitCardPage, + ); + showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.of(context).congratulations, + alertContent: S.of(context).you_now_have_debit_card, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop(), + ); + }, + ); + } +} diff --git a/lib/src/screens/ionia/cards/ionia_buy_card_detail_page.dart b/lib/src/screens/ionia/cards/ionia_buy_card_detail_page.dart new file mode 100644 index 000000000..9f0fa1792 --- /dev/null +++ b/lib/src/screens/ionia/cards/ionia_buy_card_detail_page.dart @@ -0,0 +1,322 @@ +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/palette.dart'; +import 'package:cake_wallet/src/screens/ionia/widgets/confirm_modal.dart'; +import 'package:cake_wallet/src/screens/ionia/widgets/text_icon_button.dart'; +import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; +import 'package:cake_wallet/src/widgets/discount_badge.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/store/settings_store.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:cake_wallet/typography.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:flutter/material.dart'; +import 'package:cake_wallet/generated/i18n.dart'; + +class IoniaBuyGiftCardDetailPage extends StatelessWidget { + ThemeBase get currentTheme => getIt.get().currentTheme; + + Color get backgroundLightColor => Colors.white; + + Color get backgroundDarkColor => PaletteDark.backgroundColor; + + void onClose(BuildContext context) => Navigator.of(context).pop(); + + Widget leading(BuildContext context) { + if (ModalRoute.of(context).isFirst) { + return null; + } + + final _backButton = Icon( + Icons.arrow_back_ios, + color: Theme.of(context).primaryTextTheme.title.color, + size: 16, + ); + return Padding( + padding: const EdgeInsets.only(left: 10.0), + child: SizedBox( + height: 37, + width: 37, + child: ButtonTheme( + minWidth: double.minPositive, + child: FlatButton( + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + padding: EdgeInsets.all(0), + onPressed: () => onClose(context), + child: _backButton), + ), + ), + ); + } + + Widget middle(BuildContext context) { + return Text( + 'AppleBees', + style: TextStyle( + fontSize: 22, + fontFamily: 'Lato', + fontWeight: FontWeight.w900, + ), + ); + } + + @override + Widget build(BuildContext context) { + final _backgroundColor = currentTheme.type == ThemeType.dark ? backgroundDarkColor : backgroundLightColor; + + return Scaffold( + backgroundColor: _backgroundColor, + body: ScrollableWithBottomSection( + contentPadding: EdgeInsets.zero, + content: Column( + children: [ + SizedBox(height: 60), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [leading(context), middle(context), DiscountBadge()], + ), + SizedBox(height: 36), + Container( + padding: EdgeInsets.symmetric(vertical: 24), + margin: EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + gradient: LinearGradient( + colors: [ + Theme.of(context).primaryTextTheme.subhead.color, + Theme.of(context).primaryTextTheme.subhead.decorationColor, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: Column( + children: [ + Text( + S.of(context).gift_card_amount, + style: textSmall(), + ), + SizedBox(height: 4), + Text( + '\$1000.12', + style: textXLargeSemiBold(), + ), + SizedBox(height: 24), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + S.of(context).bill_amount, + style: textSmall(), + ), + SizedBox(height: 4), + Text( + '\$1000.00', + style: textLargeSemiBold(), + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + S.of(context).tip, + style: textSmall(), + ), + SizedBox(height: 4), + Text( + '\$1000.00', + style: textLargeSemiBold(), + ), + ], + ), + ], + ), + ), + SizedBox(height: 16), + Divider(), + SizedBox(height: 16), + Text( + S.of(context).you_pay, + style: textSmall(), + ), + SizedBox(height: 4), + Text( + '22.3435345000 XMR', + style: textLargeSemiBold(), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + S.of(context).tip, + style: TextStyle( + color: Theme.of(context).primaryTextTheme.title.color, + fontWeight: FontWeight.w700, + fontSize: 14, + ), + ), + SizedBox(height: 4), + TipButtonGroup() + ], + ), + ), + SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: TextIconButton( + label: S.of(context).how_to_use_card, + onTap: () {}, + ), + ), + ], + ), + bottomSection: Column( + children: [ + Padding( + padding: EdgeInsets.only(bottom: 12), + child: PrimaryButton( + onPressed: () => purchaseCard(context), + text: S.of(context).purchase_gift_card, + color: Theme.of(context).accentTextTheme.body2.color, + textColor: Colors.white, + ), + ), + SizedBox(height: 8), + Text(S.of(context).settings_terms_and_conditions, + style: textMediumSemiBold( + color: Theme.of(context).primaryTextTheme.body1.color, + ).copyWith(fontSize: 12)), + SizedBox(height: 16) + ], + ), + ), + ); + } + + void purchaseCard(BuildContext context) { + showPopUp( + context: context, + builder: (_) { + return IoniaConfirmModal( + alertTitle: S.of(context).confirm_sending, + alertContent: SizedBox( + //Todo:: substitute this widget with modal content + height: 200, + ), + rightButtonText: S.of(context).ok, + leftButtonText: S.of(context).cancel, + leftActionColor: Color(0xffFF6600), + rightActionColor: Theme.of(context).accentTextTheme.body2.color, + actionRightButton: () async { + Navigator.of(context).pop(); + }, + actionLeftButton: () => Navigator.of(context).pop()); + }); + } +} + +class TipButtonGroup extends StatefulWidget { + const TipButtonGroup({ + Key key, + }) : super(key: key); + + @override + _TipButtonGroupState createState() => _TipButtonGroupState(); +} + +class _TipButtonGroupState extends State { + String selectedTip; + bool _isSelected(String value) { + return selectedTip == value; + } + + @override + Widget build(BuildContext context) { + return Row( + children: [ + TipButton( + isSelected: _isSelected('299'), + caption: '\$10', + subTitle: '%299', + ), + SizedBox(width: 4), + TipButton( + caption: '\$10', + subTitle: '%299', + ), + SizedBox(width: 4), + TipButton( + isSelected: _isSelected('299'), + caption: '\$10', + subTitle: '%299', + ), + SizedBox(width: 4), + TipButton( + isSelected: _isSelected('299'), + caption: S.of(context).custom, + ), + ], + ); + } +} + +class TipButton extends StatelessWidget { + final String caption; + final String subTitle; + final bool isSelected; + final void Function(int, bool) onTap; + + const TipButton({ + @required this.caption, + this.subTitle, + this.onTap, + this.isSelected = false, + }); + + @override + Widget build(BuildContext context) { + return Container( + height: 49, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(caption, style: textSmallSemiBold(color: Theme.of(context).primaryTextTheme.title.color)), + if (subTitle != null) ...[ + SizedBox(height: 4), + Text( + subTitle, + style: textXxSmallSemiBold(color: Palette.gray), + ), + ] + ], + ), + padding: EdgeInsets.symmetric(horizontal: 18, vertical: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Color.fromRGBO(242, 240, 250, 1), + gradient: isSelected + ? LinearGradient( + colors: [ + Theme.of(context).primaryTextTheme.subhead.color, + Theme.of(context).primaryTextTheme.subhead.decorationColor, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ) + : null, + ), + ); + } +} diff --git a/lib/src/screens/ionia/cards/ionia_buy_gift_card.dart b/lib/src/screens/ionia/cards/ionia_buy_gift_card.dart new file mode 100644 index 000000000..1677a9e77 --- /dev/null +++ b/lib/src/screens/ionia/cards/ionia_buy_gift_card.dart @@ -0,0 +1,157 @@ +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/ionia/widgets/card_item.dart'; +import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; +import 'package:cake_wallet/src/widgets/keyboard_done_button.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/themes/theme_base.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:keyboard_actions/keyboard_actions.dart'; +import 'package:cake_wallet/generated/i18n.dart'; + +class IoniaBuyGiftCardPage extends BasePage { + IoniaBuyGiftCardPage() + : _amountFieldFocus = FocusNode(), + _amountController = TextEditingController(); + @override + String get title => S.current.enter_amount; + + @override + Color get titleColor => Colors.white; + + @override + bool get extendBodyBehindAppBar => true; + + @override + AppBarStyle get appBarStyle => AppBarStyle.transparent; + + Color get textColor => currentTheme.type == ThemeType.dark ? Colors.white : Color(0xff393939); + + final TextEditingController _amountController; + final FocusNode _amountFieldFocus; + + @override + Widget body(BuildContext context) { + final _width = MediaQuery.of(context).size.width; + return KeyboardActions( + disableScroll: true, + config: KeyboardActionsConfig( + keyboardActionsPlatform: KeyboardActionsPlatform.IOS, + keyboardBarColor: Theme.of(context).accentTextTheme.body2.backgroundColor, + nextFocus: false, + actions: [ + KeyboardActionsItem( + focusNode: _amountFieldFocus, + toolbarButtons: [(_) => KeyboardDoneButton()], + ), + ]), + child: Container( + color: Theme.of(context).backgroundColor, + child: ScrollableWithBottomSection( + contentPadding: EdgeInsets.zero, + content: Column( + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 25), + decoration: BoxDecoration( + borderRadius: BorderRadius.only(bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)), + gradient: LinearGradient(colors: [ + Theme.of(context).primaryTextTheme.subhead.color, + Theme.of(context).primaryTextTheme.subhead.decorationColor, + ], begin: Alignment.topLeft, end: Alignment.bottomRight), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox(height: 150), + BaseTextFormField( + controller: _amountController, + focusNode: _amountFieldFocus, + keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true), + inputFormatters: [FilteringTextInputFormatter.deny(RegExp('[\-|\ ]'))], + hintText: '1000', + placeholderTextStyle: TextStyle( + color: Theme.of(context).primaryTextTheme.headline.color, + fontWeight: FontWeight.w500, + fontSize: 36, + ), + borderColor: Theme.of(context).primaryTextTheme.headline.color, + textColor: Colors.white, + textStyle: TextStyle( + color: Colors.white, + fontSize: 36, + ), + suffixIcon: SizedBox( + width: _width / 6, + ), + prefixIcon: Padding( + padding: EdgeInsets.only( + top: 5.0, + left: _width / 4, + ), + child: Text( + 'USD: ', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w900, + fontSize: 36, + ), + ), + ), + ), + SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + S.of(context).min_amount('5'), + style: TextStyle( + color: Theme.of(context).primaryTextTheme.headline.color, + ), + ), + Text( + S.of(context).max_amount('20000'), + style: TextStyle( + color: Theme.of(context).primaryTextTheme.headline.color, + ), + ), + ], + ), + SizedBox(height: 24), + ], + ), + ), + Padding( + padding: const EdgeInsets.all(24.0), + child: CardItem( + onTap: () {}, + title: 'Applebee’s', + hasDiscount: true, + subTitle: 'subTitle', + logoUrl: '', + ), + ) + ], + ), + bottomSection: Column( + children: [ + Padding( + padding: EdgeInsets.only(bottom: 12), + child: PrimaryButton( + onPressed: () => Navigator.of(context).pushNamed(Routes.ioniaBuyGiftCardDetailPage), + text: S.of(context).continue_text, + color: Theme.of(context).accentTextTheme.body2.color, + textColor: Colors.white, + ), + ), + SizedBox(height: 30), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/screens/ionia/cards/ionia_debit_card_page.dart b/lib/src/screens/ionia/cards/ionia_debit_card_page.dart new file mode 100644 index 000000000..0d2a6294f --- /dev/null +++ b/lib/src/screens/ionia/cards/ionia_debit_card_page.dart @@ -0,0 +1,382 @@ +import 'package:cake_wallet/ionia/ionia_create_state.dart'; +import 'package:cake_wallet/ionia/ionia_virtual_card.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/ionia/widgets/text_icon_button.dart'; +import 'package:cake_wallet/src/widgets/alert_background.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/typography.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'; + +class IoniaDebitCardPage extends BasePage { + final IoniaViewModel _ioniaViewModel; + + IoniaDebitCardPage(this._ioniaViewModel); + + @override + Widget middle(BuildContext context) { + return Text( + S.current.debit_card, + style: TextStyle( + fontSize: 22, + fontFamily: 'Lato', + color: Theme.of(context).accentTextTheme.display4.backgroundColor, + fontWeight: FontWeight.w900, + ), + ); + } + + @override + Widget body(BuildContext context) { + return Observer( + builder: (_) { + final cardState = _ioniaViewModel.cardState; + if (cardState is IoniaFetchingCard) { + return Center(child: CircularProgressIndicator()); + } + if (cardState is IoniaCardSuccess) { + return ScrollableWithBottomSection( + contentPadding: EdgeInsets.zero, + content: Padding( + padding: const EdgeInsets.all(16.0), + child: _IoniaDebitCard( + cardInfo: cardState.card, + ), + ), + bottomSection: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: Text( + S.of(context).billing_address_info, + style: textSmall(color: Theme.of(context).textTheme.display1.color), + textAlign: TextAlign.center, + ), + ), + SizedBox(height: 24), + PrimaryButton( + text: S.of(context).order_physical_card, + onPressed: () {}, + color: Color(0xffE9F2FC), + textColor: Theme.of(context).textTheme.display2.color, + ), + SizedBox(height: 8), + PrimaryButton( + text: S.of(context).add_value, + onPressed: () {}, + color: Theme.of(context).accentTextTheme.body2.color, + textColor: Colors.white, + ), + SizedBox(height: 16) + ], + ), + ); + } + return ScrollableWithBottomSection( + contentPadding: EdgeInsets.zero, + content: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + _IoniaDebitCard(isCardSample: true), + SizedBox(height: 40), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Column( + children: [ + TextIconButton( + label: S.current.how_to_use_card, + onTap: () => _showHowToUseCard(context), + ), + SizedBox( + height: 24, + ), + TextIconButton( + label: S.current.frequently_asked_questions, + onTap: () {}, + ), + ], + ), + ), + SizedBox(height: 50), + Container( + padding: EdgeInsets.all(20), + margin: EdgeInsets.all(8), + width: double.infinity, + decoration: BoxDecoration( + color: Color.fromRGBO(233, 242, 252, 1), + borderRadius: BorderRadius.circular(20), + ), + child: RichText( + text: TextSpan( + text: S.of(context).get_a, + style: textMedium(color: Theme.of(context).textTheme.display2.color), + children: [ + TextSpan( + text: S.of(context).digital_and_physical_card, + style: textMediumBold(color: Theme.of(context).textTheme.display2.color), + ), + TextSpan( + text: S.of(context).get_card_note, + ) + ], + )), + ), + ], + ), + ), + bottomSectionPadding: EdgeInsets.symmetric( + horizontal: 16, + vertical: 32, + ), + bottomSection: PrimaryButton( + text: S.of(context).activate, + onPressed: () => _showHowToUseCard(context, activate: true), + color: Theme.of(context).accentTextTheme.body2.color, + textColor: Colors.white, + ), + ); + }, + ); + } + + void _showHowToUseCard(BuildContext context, {bool activate = false}) { + showPopUp( + context: context, + builder: (BuildContext context) { + return AlertBackground( + child: Material( + color: Colors.transparent, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox(height: 10), + Container( + padding: EdgeInsets.only(top: 24, left: 24, right: 24), + margin: EdgeInsets.all(24), + decoration: BoxDecoration( + color: Theme.of(context).backgroundColor, + borderRadius: BorderRadius.circular(30), + ), + child: Column( + children: [ + Text( + S.of(context).how_to_use_card, + style: textLargeSemiBold( + color: Theme.of(context).textTheme.body1.color, + ), + ), + SizedBox(height: 24), + Align( + alignment: Alignment.bottomLeft, + child: Text( + S.of(context).signup_for_card_accept_terms, + style: textSmallSemiBold( + color: Theme.of(context).textTheme.display2.color, + ), + ), + ), + SizedBox(height: 24), + _TitleSubtitleTile( + title: S.of(context).add_fund_to_card('1000'), + subtitle: S.of(context).use_card_info_two, + ), + SizedBox(height: 21), + _TitleSubtitleTile( + title: S.of(context).use_card_info_three, + subtitle: S.of(context).optionally_order_card, + ), + SizedBox(height: 35), + PrimaryButton( + onPressed: () => activate + ? Navigator.pushNamed(context, Routes.ioniaActivateDebitCardPage) + : Navigator.pop(context), + text: S.of(context).send_got_it, + color: Color.fromRGBO(233, 242, 252, 1), + textColor: Theme.of(context).textTheme.display2.color, + ), + SizedBox(height: 21), + ], + ), + ), + InkWell( + onTap: () => Navigator.pop(context), + child: Container( + margin: EdgeInsets.only(bottom: 40), + child: CircleAvatar( + child: Icon( + Icons.close, + color: Colors.black, + ), + backgroundColor: Colors.white, + ), + ), + ) + ], + ), + ), + ); + }); + } +} + +class _IoniaDebitCard extends StatefulWidget { + final bool isCardSample; + final IoniaVirtualCard cardInfo; + const _IoniaDebitCard({ + Key key, + this.isCardSample = false, + this.cardInfo, + }) : super(key: key); + + @override + _IoniaDebitCardState createState() => _IoniaDebitCardState(); +} + +class _IoniaDebitCardState extends State<_IoniaDebitCard> { + bool _showDetails = false; + void _toggleVisibility() { + setState(() => _showDetails = !_showDetails); + } + + String _formatPan(String pan) { + if (pan == null) return ''; + return pan.replaceAllMapped(RegExp(r'.{4}'), (match) => '${match.group(0)} '); + } + + String get _getLast4 => widget.isCardSample ? '0000' : widget.cardInfo.pan.substring(widget.cardInfo.pan.length - 5); + + String get _getSpendLimit => widget.isCardSample ? '10000' : widget.cardInfo.spendLimit.toStringAsFixed(2); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 24, vertical: 19), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + gradient: LinearGradient( + colors: [ + Theme.of(context).primaryTextTheme.subhead.color, + Theme.of(context).primaryTextTheme.subhead.decorationColor, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + S.current.cakepay_prepaid_card, + style: textSmall(), + ), + Image.asset( + 'assets/images/mastercard.png', + width: 54, + ), + ], + ), + Text( + widget.isCardSample ? S.of(context).upto(_getSpendLimit) : '\$$_getSpendLimit', + style: textXLargeSemiBold(), + ), + SizedBox(height: 16), + Text( + _showDetails ? _formatPan(widget.cardInfo.pan) : '**** **** **** $_getLast4', + style: textMediumSemiBold(), + ), + SizedBox(height: 32), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (widget.isCardSample) + Text( + S.current.no_id_needed, + style: textMediumBold(), + ) + else ...[ + Column( + children: [ + Text( + 'CVV', + style: textXSmallSemiBold(), + ), + SizedBox(height: 4), + Text( + _showDetails ? widget.cardInfo.cvv : '***', + style: textMediumSemiBold(), + ) + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + S.of(context).expires, + style: textXSmallSemiBold(), + ), + SizedBox(height: 4), + Text( + '${widget.cardInfo.expirationMonth ?? S.of(context).mm}/${widget.cardInfo.expirationYear ?? S.of(context).yy}', + style: textMediumSemiBold(), + ) + ], + ), + ] + ], + ), + if (!widget.isCardSample) ...[ + SizedBox(height: 8), + Center( + child: InkWell( + onTap: () => _toggleVisibility(), + child: Text( + _showDetails ? S.of(context).hide_details : S.of(context).show_details, + style: textSmall(), + ), + ), + ), + ], + ], + ), + ); + } +} + +class _TitleSubtitleTile extends StatelessWidget { + final String title; + final String subtitle; + const _TitleSubtitleTile({ + Key key, + @required this.title, + @required this.subtitle, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: textSmallSemiBold(color: Theme.of(context).textTheme.display2.color), + ), + SizedBox(height: 4), + Text( + subtitle, + style: textSmall(color: Theme.of(context).textTheme.display2.color), + ), + ], + ); + } +} diff --git a/lib/src/screens/ionia/cards/ionia_manage_cards_page.dart b/lib/src/screens/ionia/cards/ionia_manage_cards_page.dart new file mode 100644 index 000000000..49276307d --- /dev/null +++ b/lib/src/screens/ionia/cards/ionia_manage_cards_page.dart @@ -0,0 +1,247 @@ +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/ionia/widgets/card_item.dart'; +import 'package:cake_wallet/src/screens/ionia/widgets/card_menu.dart'; +import 'package:cake_wallet/src/widgets/market_place_item.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:cake_wallet/view_model/ionia/ionia_view_model.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:cake_wallet/generated/i18n.dart'; + +class IoniaManageCardsPage extends BasePage { + IoniaManageCardsPage(this._ioniaViewModel); + + final IoniaViewModel _ioniaViewModel; + + @override + Color get backgroundLightColor => currentTheme.type == ThemeType.bright ? Colors.transparent : Colors.white; + + @override + Color get backgroundDarkColor => Colors.transparent; + + @override + Color get titleColor => currentTheme.type == ThemeType.bright ? Colors.white : Colors.black; + + @override + Widget Function(BuildContext, Widget) get rootWrapper => (BuildContext context, Widget scaffold) => Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Theme.of(context).accentColor, + Theme.of(context).scaffoldBackgroundColor, + Theme.of(context).primaryColor, + ], + begin: Alignment.topRight, + end: Alignment.bottomLeft, + ), + ), + child: scaffold, + ); + + @override + bool get resizeToAvoidBottomInset => false; + + @override + Widget get endDrawer => CardMenu(); + + @override + Widget leading(BuildContext context) { + final _backButton = Icon( + Icons.arrow_back_ios, + color: titleColor ?? Theme.of(context).primaryTextTheme.title.color, + size: 16, + ); + + return SizedBox( + height: 37, + width: 37, + child: ButtonTheme( + minWidth: double.minPositive, + child: FlatButton( + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + padding: EdgeInsets.all(0), + onPressed: () => Navigator.pushReplacementNamed(context, Routes.dashboard), + child: _backButton), + ), + ); + } + + @override + Widget middle(BuildContext context) { + return Text( + S.of(context).manage_cards, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.w500, + color: Theme.of(context).accentTextTheme.display3.backgroundColor, + ), + ); + } + + final ScrollController _scrollController = ScrollController(); + + @override + Widget trailing(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + _TrailingIcon( + asset: 'assets/images/card.png', + onPressed: () => Navigator.pushNamed(context, Routes.ioniaDebitCardPage), + ), + SizedBox(width: 16), + _TrailingIcon( + asset: 'assets/images/profile.png', + onPressed: () {}, + ), + ], + ); + } + + @override + Widget body(BuildContext context) { + final filterIcon = Image.asset( + 'assets/images/filter.png', + color: Theme.of(context).textTheme.caption.decorationColor, + ); + + return Padding( + padding: const EdgeInsets.all(14.0), + child: Column( + children: [ + MarketPlaceItem( + onTap: () {}, + title: S.of(context).setup_your_debit_card, + subTitle: S.of(context).no_id_required, + ), + SizedBox(height: 48), + Container( + padding: EdgeInsets.only(left: 2, right: 22), + height: 32, + child: Row( + children: [ + Expanded(child: _SearchWidget()), + SizedBox(width: 10), + Container( + width: 32, + padding: EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.15), + border: Border.all( + color: Colors.white.withOpacity(0.2), + ), + borderRadius: BorderRadius.circular(10), + ), + child: filterIcon, + ) + ], + ), + ), + SizedBox(height: 8), + Expanded( + child: RawScrollbar( + thumbColor: Colors.white.withOpacity(0.15), + radius: Radius.circular(20), + isAlwaysShown: true, + thickness: 2, + controller: _scrollController, + child: ListView.separated( + padding: EdgeInsets.only(left: 2, right: 22), + controller: _scrollController, + itemCount: 20, + separatorBuilder: (_, __) => SizedBox(height: 4), + itemBuilder: (_, index) { + return CardItem( + logoUrl: '', + onTap: () => Navigator.of(context).pushNamed(Routes.ioniaBuyGiftCardPage), + title: 'Amazon', + subTitle: 'Onlin', + hasDiscount: true, + ); + }, + ), + ), + ), + ], + ), + ); + } +} + +class _SearchWidget extends StatelessWidget { + const _SearchWidget({ + Key key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final searchIcon = Padding( + padding: EdgeInsets.all(8), + child: Image.asset( + 'assets/images/search_icon.png', + color: Theme.of(context).textTheme.caption.decorationColor, + ), + ); + + return TextField( + style: TextStyle(color: Colors.white), + decoration: InputDecoration( + filled: true, + contentPadding: EdgeInsets.only( + top: 10, + left: 10, + ), + fillColor: Colors.white.withOpacity(0.15), + hintText: 'Search', + hintStyle: TextStyle( + color: Colors.white.withOpacity(0.6), + ), + alignLabelWithHint: true, + floatingLabelBehavior: FloatingLabelBehavior.never, + suffixIcon: searchIcon, + border: OutlineInputBorder( + borderSide: BorderSide( + color: Colors.white.withOpacity(0.2), + ), + borderRadius: BorderRadius.circular(10), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Colors.white.withOpacity(0.2), + ), + borderRadius: BorderRadius.circular(10), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.white.withOpacity(0.2)), + borderRadius: BorderRadius.circular(10), + )), + ); + } +} + +class _TrailingIcon extends StatelessWidget { + final String asset; + final VoidCallback onPressed; + + const _TrailingIcon({this.asset, this.onPressed}); + + @override + Widget build(BuildContext context) { + return Container( + alignment: Alignment.centerRight, + width: 25, + child: FlatButton( + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + padding: EdgeInsets.all(0), + onPressed: onPressed, + child: Image.asset( + asset, + color: Theme.of(context).accentTextTheme.display3.backgroundColor, + ), + ), + ); + } +} diff --git a/lib/src/screens/ionia/ionia.dart b/lib/src/screens/ionia/ionia.dart new file mode 100644 index 000000000..bdc2065a9 --- /dev/null +++ b/lib/src/screens/ionia/ionia.dart @@ -0,0 +1,9 @@ +export 'auth/ionia_welcome_page.dart'; +export 'auth/ionia_create_account_page.dart'; +export 'auth/ionia_login_page.dart'; +export 'auth/ionia_verify_otp_page.dart'; +export 'cards/ionia_activate_debit_card_page.dart'; +export 'cards/ionia_buy_card_detail_page.dart'; +export 'cards/ionia_manage_cards_page.dart'; +export 'cards/ionia_debit_card_page.dart'; +export 'cards/ionia_buy_gift_card.dart'; diff --git a/lib/src/screens/ionia/widgets/card_item.dart b/lib/src/screens/ionia/widgets/card_item.dart new file mode 100644 index 000000000..95bf9ee1e --- /dev/null +++ b/lib/src/screens/ionia/widgets/card_item.dart @@ -0,0 +1,122 @@ +import 'package:cake_wallet/palette.dart'; +import 'package:cake_wallet/src/widgets/discount_badge.dart'; +import 'package:flutter/material.dart'; + +class CardItem extends StatelessWidget { + + CardItem({ + @required this.onTap, + @required this.title, + @required this.subTitle, + this.logoUrl, + this.hasDiscount = false, + }); + + final VoidCallback onTap; + final String title; + final String subTitle; + final String logoUrl; + final bool hasDiscount; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Stack( + children: [ + Container( + padding: EdgeInsets.all(12), + width: double.infinity, + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: Colors.white.withOpacity(0.20), + ), + ), + child: Row( + children: [ + if (logoUrl != null) ...[ + ClipOval( + child: Image.network( + logoUrl, + width: 42.0, + height: 42.0, + loadingBuilder: (BuildContext _, Widget child, ImageChunkEvent loadingProgress) { + if (loadingProgress == null) { + return child; + } else { + return _PlaceholderContainer(text: 'Logo'); + } + }, + errorBuilder: (_, __, ___) => _PlaceholderContainer(text: '!'), + ), + ), + SizedBox(width: 5), + ], + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + color: Palette.stateGray, + fontSize: 24, + fontWeight: FontWeight.w900, + ), + ), + SizedBox(height: 5), + Text( + subTitle, + style: TextStyle( + color: Palette.niagara , + fontWeight: FontWeight.w500, + fontFamily: 'Lato'), + ) + ], + ), + ], + ), + ), + if (hasDiscount) + Align( + alignment: Alignment.topRight, + child: Padding( + padding: const EdgeInsets.only(top: 20.0), + child: DiscountBadge(), + ), + ), + ], + ), + ); + } +} + +class _PlaceholderContainer extends StatelessWidget { + + const _PlaceholderContainer({@required this.text}); + + final String text; + + @override + Widget build(BuildContext context) { + return Container( + height: 42, + width: 42, + child: Center( + child: Text( + text, + style: TextStyle( + color: Colors.black, + fontSize: 12, + fontWeight: FontWeight.w900, + ), + ), + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(100), + ), + ); + } +} diff --git a/lib/src/screens/ionia/widgets/card_menu.dart b/lib/src/screens/ionia/widgets/card_menu.dart new file mode 100644 index 000000000..9212c0448 --- /dev/null +++ b/lib/src/screens/ionia/widgets/card_menu.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; + +class CardMenu extends StatelessWidget { + + @override + Widget build(BuildContext context) { + return Container( + + ); + } +} \ No newline at end of file diff --git a/lib/src/screens/ionia/widgets/confirm_modal.dart b/lib/src/screens/ionia/widgets/confirm_modal.dart new file mode 100644 index 000000000..dd513ffa7 --- /dev/null +++ b/lib/src/screens/ionia/widgets/confirm_modal.dart @@ -0,0 +1,146 @@ +import 'dart:ui'; + +import 'package:cake_wallet/palette.dart'; +import 'package:flutter/material.dart'; + +class IoniaConfirmModal extends StatelessWidget { + IoniaConfirmModal({ + @required this.alertTitle, + @required this.alertContent, + @required this.leftButtonText, + @required this.rightButtonText, + @required this.actionLeftButton, + @required this.actionRightButton, + this.leftActionColor, + this.rightActionColor, + }); + + final String alertTitle; + final Widget alertContent; + final String leftButtonText; + final String rightButtonText; + final VoidCallback actionLeftButton; + final VoidCallback actionRightButton; + final Color leftActionColor; + final Color rightActionColor; + + Widget actionButtons(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.max, + children: [ + IoniaActionButton( + buttonText: leftButtonText, + action: actionLeftButton, + backgoundColor: leftActionColor, + ), + Container( + width: 1, + height: 52, + color: Theme.of(context).dividerColor, + ), + IoniaActionButton( + buttonText: rightButtonText, + action: actionRightButton, + backgoundColor: rightActionColor, + ), + ], + ); + } + + Widget title(BuildContext context) { + return Text( + alertTitle, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 20, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: Theme.of(context).primaryTextTheme.title.color, + decoration: TextDecoration.none, + ), + ); + } + + @override + Widget build(BuildContext context) { + return Container( + color: Colors.transparent, + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 3.0, sigmaY: 3.0), + child: Container( + decoration: BoxDecoration(color: PaletteDark.darkNightBlue.withOpacity(0.75)), + child: Center( + child: GestureDetector( + onTap: () => null, + child: ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(30)), + child: Container( + width: 327, + color: Theme.of(context).accentTextTheme.title.decorationColor, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: EdgeInsets.fromLTRB(24, 20, 24, 0), + child: title(context), + ), + Padding( + padding: EdgeInsets.only(top: 16, bottom: 8), + child: Container( + height: 1, + color: Theme.of(context).dividerColor, + ), + ), + alertContent, + actionButtons(context), + ], + ), + ), + ), + ), + ), + ), + ), + ); + } +} + +class IoniaActionButton extends StatelessWidget { + const IoniaActionButton({ + @required this.buttonText, + @required this.action, + this.backgoundColor, + }); + + final String buttonText; + final VoidCallback action; + final Color backgoundColor; + + @override + Widget build(BuildContext context) { + return Flexible( + child: Container( + height: 52, + padding: EdgeInsets.only(left: 6, right: 6), + color: backgoundColor, + child: ButtonTheme( + minWidth: double.infinity, + child: FlatButton( + onPressed: action, + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + child: Text( + buttonText, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 15, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: backgoundColor != null ? Colors.white : Theme.of(context).primaryTextTheme.body1.backgroundColor, + decoration: TextDecoration.none, + ), + )), + ), + )); + } +} diff --git a/lib/src/screens/ionia/widgets/text_icon_button.dart b/lib/src/screens/ionia/widgets/text_icon_button.dart new file mode 100644 index 000000000..16606e65d --- /dev/null +++ b/lib/src/screens/ionia/widgets/text_icon_button.dart @@ -0,0 +1,35 @@ +import 'package:cake_wallet/typography.dart'; +import 'package:flutter/material.dart'; + +class TextIconButton extends StatelessWidget { + final String label; + final VoidCallback onTap; + const TextIconButton({ + Key key, + this.label, + this.onTap, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return + InkWell( + onTap: onTap, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: textMediumSemiBold( + color: Theme.of(context).primaryTextTheme.title.color, + ), + ), + Icon( + Icons.chevron_right_rounded, + color: Theme.of(context).primaryTextTheme.title.color, + ), + ], + ), + ); + } +} diff --git a/lib/src/widgets/alert_with_two_actions.dart b/lib/src/widgets/alert_with_two_actions.dart index c2831675e..f3c66f275 100644 --- a/lib/src/widgets/alert_with_two_actions.dart +++ b/lib/src/widgets/alert_with_two_actions.dart @@ -10,7 +10,10 @@ class AlertWithTwoActions extends BaseAlertDialog { @required this.rightButtonText, @required this.actionLeftButton, @required this.actionRightButton, - this.alertBarrierDismissible = true + this.alertBarrierDismissible = true, + this.isDividerExist = false, + this.leftActionColor, + this.rightActionColor, }); final String alertTitle; @@ -20,6 +23,9 @@ class AlertWithTwoActions extends BaseAlertDialog { final VoidCallback actionLeftButton; final VoidCallback actionRightButton; final bool alertBarrierDismissible; + final Color leftActionColor; + final Color rightActionColor; + final bool isDividerExist; @override String get titleText => alertTitle; @@ -35,4 +41,10 @@ class AlertWithTwoActions extends BaseAlertDialog { VoidCallback get actionRight => actionRightButton; @override bool get barrierDismissible => alertBarrierDismissible; -} \ No newline at end of file + @override + Color get leftButtonColor => leftActionColor; + @override + Color get rightButtonColor => rightActionColor; + @override + bool get isDividerExists => isDividerExist; +} diff --git a/lib/src/widgets/base_alert_dialog.dart b/lib/src/widgets/base_alert_dialog.dart index 19ff62ce2..d0aaace2f 100644 --- a/lib/src/widgets/base_alert_dialog.dart +++ b/lib/src/widgets/base_alert_dialog.dart @@ -46,30 +46,28 @@ class BaseAlertDialog extends StatelessWidget { children: [ Flexible( child: Container( - height: 52, - padding: EdgeInsets.only(left: 6, right: 6), - color: Theme.of(context).accentTextTheme.body2.decorationColor, - child: ButtonTheme( - minWidth: double.infinity, - child: FlatButton( - onPressed: actionLeft, - highlightColor: Colors.transparent, - splashColor: Colors.transparent, - child: Text( - leftActionButtonText, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 15, - fontFamily: 'Lato', - fontWeight: FontWeight.w600, - color: Theme.of(context).primaryTextTheme.body2 - .backgroundColor, - decoration: TextDecoration.none, - ), - )), - ), - ) - ), + height: 52, + padding: EdgeInsets.only(left: 6, right: 6), + color: Theme.of(context).accentTextTheme.body2.decorationColor, + child: ButtonTheme( + minWidth: double.infinity, + child: FlatButton( + onPressed: actionLeft, + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + child: Text( + leftActionButtonText, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 15, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: Theme.of(context).primaryTextTheme.body2.backgroundColor, + decoration: TextDecoration.none, + ), + )), + ), + )), Container( width: 1, height: 52, @@ -77,30 +75,28 @@ class BaseAlertDialog extends StatelessWidget { ), Flexible( child: Container( - height: 52, - padding: EdgeInsets.only(left: 6, right: 6), - color: Theme.of(context).accentTextTheme.body1.backgroundColor, - child: ButtonTheme( - minWidth: double.infinity, - child: FlatButton( - onPressed: actionRight, - highlightColor: Colors.transparent, - splashColor: Colors.transparent, - child: Text( - rightActionButtonText, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 15, - fontFamily: 'Lato', - fontWeight: FontWeight.w600, - color: Theme.of(context).primaryTextTheme.body1 - .backgroundColor, - decoration: TextDecoration.none, - ), - )), - ), - ) - ), + height: 52, + padding: EdgeInsets.only(left: 6, right: 6), + color: Theme.of(context).accentTextTheme.body1.backgroundColor, + child: ButtonTheme( + minWidth: double.infinity, + child: FlatButton( + onPressed: actionRight, + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + child: Text( + rightActionButtonText, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 15, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: Theme.of(context).primaryTextTheme.body1.backgroundColor, + decoration: TextDecoration.none, + ), + )), + ), + )), ], ); } @@ -108,9 +104,7 @@ class BaseAlertDialog extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( - onTap: () => barrierDismissible - ? Navigator.of(context).pop() - : null, + onTap: () => barrierDismissible ? Navigator.of(context).pop() : null, child: Container( color: Colors.transparent, child: BackdropFilter( @@ -136,14 +130,14 @@ class BaseAlertDialog extends StatelessWidget { child: title(context), ), isDividerExists - ? Padding( - padding: EdgeInsets.only(top: 16, bottom: 8), - child: Container( - height: 1, - color: Theme.of(context).dividerColor, - ), - ) - : Offstage(), + ? Padding( + padding: EdgeInsets.only(top: 16, bottom: 8), + child: Container( + height: 1, + color: Theme.of(context).dividerColor, + ), + ) + : Offstage(), Padding( padding: EdgeInsets.fromLTRB(24, 8, 24, 32), child: content(context), @@ -166,4 +160,4 @@ class BaseAlertDialog extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/src/widgets/discount_badge.dart b/lib/src/widgets/discount_badge.dart new file mode 100644 index 000000000..ad9c4e2ce --- /dev/null +++ b/lib/src/widgets/discount_badge.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:cake_wallet/generated/i18n.dart'; + + +class DiscountBadge extends StatelessWidget { + const DiscountBadge({ + Key key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Stack( + alignment: Alignment.centerRight, + children: [ + Image.asset('assets/images/badge_discount.png'), + Padding( + padding: const EdgeInsets.only(right: 10.0), + child: Text( + S.of(context).discount('20'), + style: TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.w500, + fontFamily: 'Lato', + ), + ), + ) + ], + ); + } +} diff --git a/lib/src/widgets/market_place_item.dart b/lib/src/widgets/market_place_item.dart new file mode 100644 index 000000000..0112ea00d --- /dev/null +++ b/lib/src/widgets/market_place_item.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; + +class MarketPlaceItem extends StatelessWidget { + + + MarketPlaceItem({ + @required this.onTap, + @required this.title, + @required this.subTitle, + }); + + final VoidCallback onTap; + final String title; + final String subTitle; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Stack( + children: [ + Container( + padding: EdgeInsets.all(20), + width: double.infinity, + decoration: BoxDecoration( + color: Theme.of(context).textTheme.title.backgroundColor, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: Colors.white.withOpacity(0.20), + ), + ), + child: + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + color: Theme.of(context) + .accentTextTheme + .display3 + .backgroundColor, + fontSize: 24, + fontWeight: FontWeight.w900, + ), + ), + SizedBox(height: 5), + Text( + subTitle, + style: TextStyle( + color: Theme.of(context) + .accentTextTheme + .display3 + .backgroundColor, + fontWeight: FontWeight.w500, + fontFamily: 'Lato'), + ) + ], + ), + ), + ], + ), + ); + } +} + diff --git a/lib/typography.dart b/lib/typography.dart new file mode 100644 index 000000000..08491e2cb --- /dev/null +++ b/lib/typography.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; + +const latoFont = "Lato"; + +TextStyle textXxSmall({Color color}) => _cakeRegular(10, color); + +TextStyle textXxSmallSemiBold({Color color}) => _cakeSemiBold(10, color); + +TextStyle textXSmall({Color color}) => _cakeRegular(12, color); + +TextStyle textXSmallSemiBold({Color color}) => _cakeSemiBold(12, color); + +TextStyle textSmall({Color color}) => _cakeRegular(14, color); + +TextStyle textSmallSemiBold({Color color}) => _cakeSemiBold(14, color); + +TextStyle textMedium({Color color}) => _cakeRegular(16, color); + +TextStyle textMediumBold({Color color}) => _cakeBold(16, color); + +TextStyle textMediumSemiBold({Color color}) => _cakeSemiBold(22, color); + +TextStyle textLarge({Color color}) => _cakeRegular(18, color); + +TextStyle textLargeSemiBold({Color color}) => _cakeSemiBold(24, color); + +TextStyle textXLarge({Color color}) => _cakeRegular(32, color); + +TextStyle textXLargeSemiBold({Color color}) => _cakeSemiBold(32, color); + +TextStyle _cakeRegular(double size, Color color) => _textStyle( + size: size, + fontWeight: FontWeight.normal, + color: color, + ); + +TextStyle _cakeBold(double size, Color color) => _textStyle( + size: size, + fontWeight: FontWeight.w900, + color: color, + ); + +TextStyle _cakeSemiBold(double size, Color color) => _textStyle( + size: size, + fontWeight: FontWeight.w700, + color: color, + ); + +TextStyle _textStyle({ + @required double size, + @required FontWeight fontWeight, + Color color, +}) => + TextStyle( + fontFamily: latoFont, + fontSize: size, + fontWeight: fontWeight, + color: color ?? Colors.white, + ); 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..6aa324db3 --- /dev/null +++ b/lib/view_model/ionia/ionia_view_model.dart @@ -0,0 +1,91 @@ +import 'package:cake_wallet/ionia/ionia.dart'; +import 'package:cake_wallet/ionia/ionia_create_state.dart'; +import 'package:cake_wallet/ionia/ionia_virtual_card.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(), + cardState = IoniaNoCardState() { + _getCard(); + _getAuthStatus().then((value) => isLoggedIn = value); + } + + final IoniaService ioniaService; + + @observable + IoniaCreateAccountState createUserState; + + @observable + IoniaOtpState otpState; + + @observable + IoniaCreateCardState createCardState; + + @observable + IoniaFetchCardState cardState; + + @observable + String email; + + @observable + String otp; + + @observable + bool isLoggedIn; + + @action + Future createUser(String email) async { + createUserState = IoniaCreateStateLoading(); + try { + await ioniaService.createUser(email); + + createUserState = IoniaCreateStateSuccess(); + } on Exception catch (e) { + createUserState = IoniaCreateStateFailure(error: e.toString()); + } + } + + @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(); + } + + @action + Future createCard() async { + createCardState = IoniaCreateCardLoading(); + try { + final card = await ioniaService.createCard(); + createCardState = IoniaCreateCardSuccess(); + return card; + } on Exception catch (e) { + createCardState = IoniaCreateCardFailure(error: e.toString()); + } + return null; + } + + Future _getCard() async { + cardState = IoniaFetchingCard(); + try { + final card = await ioniaService.getCard(); + + cardState = IoniaCardSuccess(card: card); + } catch (_) { + cardState = IoniaFetchCardFailure(); + } + } +} diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index dfe3b84f3..7f85e3eb4 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -530,5 +530,69 @@ "learn_more" : "Learn More", "search": "Search", "new_template" : "New Template", - "electrum_address_disclaimer": "We generate new addresses each time you use one, but previous addresses continue to work" + "electrum_address_disclaimer": "We generate new addresses each time you use one, but previous addresses continue to work", + "market_place": "Market place", + "cake_pay_title": "Gift cards and debit cards", + "cake_pay_subtitle": "Buy gift cards and top up no-KYC debit cards", + "about_cake_pay": "CakePay allows you to easily buy gift cards and load up prepaid debit cards with cryptocurrencies, spendable at millions of merchants in the United States.", + "cake_pay_account_note": "Make an account to see the available cards. Some are even available at a discount!", + "already_have_account": "Already have an account?", + "create_account": "Create Account", + "privacy_policy": "Privacy policy", + "welcome_to_cakepay": "Welcome to CakePay!", + "sign_up": "Sign Up", + "forgot_password": "Forgot Password", + "reset_password": "Reset Password", + "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", + "purchase_gift_card": "Purchase Gift 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", + "debit_card": "Debit Card", + "cakepay_prepaid_card": "CakePay Prepaid Debit Card", + "no_id_needed": "No ID needed!", + "frequently_asked_questions": "Frequently asked questions", + "debit_card_terms": "The storage and usage of your payment card number (and credentials corresponding to your payment card number) in this digital wallet are subject to the Terms and Conditions of the applicable cardholder agreement with the payment card issuer, as in effect from time to time.", + "please_reference_document": "Please reference the documents below for more information.", + "cardholder_agreement": "Cardholder Agreement", + "e_sign_consent": "E-Sign Consent", + "agree_and_continue": "Agree & Continue", + "email_address": "Email Address", + "agree_to": "By creating account you agree to the ", + "and": "and", + "enter_code": "Enter code", + "congratulations": "Congratulations!", + "you_now_have_debit_card": "You now have a debit card", + "min_amount" : "Min: ${value}", + "max_amount" : "Max: ${value}", + "enter_amount": "Enter Amount", + "billing_address_info": "If asked for a billing address, provide your shipping address", + "order_physical_card": "Order Physical Card", + "add_value": "Add value", + "activate": "Activate", + "get_a": "Get a ", + "digital_and_physical_card": " digital and physical prepaid debit card", + "get_card_note": " that you can reload with digital currencies. No additional information needed!", + "signup_for_card_accept_terms": "Sign up for the card and accept the terms.", + "add_fund_to_card": "Add prepaid funds to the cards (up to ${value})", + "use_card_info_two": "Funds are converted to USD when the held in the prepaid account, not in digital currencies.", + "use_card_info_three": "Use the digital card online or with contactless payment methods.", + "optionally_order_card": "Optionally order a physical card.", + "hide_details" : "Hide Details", + "show_details" : "Show Details", + "upto": "up to ${value}", + "discount": "Save ${value}%", + "gift_card_amount": "Gift Card Amount", + "bill_amount": "Bill amount", + "you_pay": "You pay", + "tip": "Tip:", + "custom": "custom", + "by_cake_pay": "by CakePay", + "expires": "Expires", + "mm": "MM", + "yy": "YY" }