From f2b8dd21a1acd4e5eedfe314782780b21d022217 Mon Sep 17 00:00:00 2001 From: Godwin Asuquo <41484542+godilite@users.noreply.github.com> Date: Fri, 21 Apr 2023 21:03:42 +0300 Subject: [PATCH] CW-240 Receive fiat currency amount and receive animations (#877) * Redesign receive amount field * Fix issues with animations * Fix issues with animations * Fix max fraction digit to 8 * add another 0 * Update amount when currency is changed --------- Co-authored-by: Justin Ehrenhofer Co-authored-by: OmarHatem --- lib/core/amount_validator.dart | 5 +- lib/di.dart | 9 +- lib/entities/qr_view_data.dart | 11 + lib/router.dart | 9 +- .../dashboard/widgets/address_page.dart | 75 +++-- lib/src/screens/exchange/exchange_page.dart | 8 +- .../screens/receive/anonpay_receive_page.dart | 8 +- .../screens/receive/fullscreen_qr_page.dart | 10 +- lib/src/screens/receive/receive_page.dart | 299 +++++++++--------- .../receive/widgets/currency_input_field.dart | 120 +++++++ .../screens/receive/widgets/qr_widget.dart | 158 ++++----- .../screens/wallet_keys/wallet_keys_page.dart | 5 +- .../wallet_address_list_view_model.dart | 127 ++++---- 13 files changed, 504 insertions(+), 340 deletions(-) create mode 100644 lib/entities/qr_view_data.dart create mode 100644 lib/src/screens/receive/widgets/currency_input_field.dart diff --git a/lib/core/amount_validator.dart b/lib/core/amount_validator.dart index acd0ab135..fb5214d54 100644 --- a/lib/core/amount_validator.dart +++ b/lib/core/amount_validator.dart @@ -1,6 +1,7 @@ import 'package:cake_wallet/core/validator.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/currency.dart'; class AmountValidator extends TextValidator { AmountValidator({ @@ -57,7 +58,7 @@ class SymbolsAmountValidator extends TextValidator { } class DecimalAmountValidator extends TextValidator { - DecimalAmountValidator({required CryptoCurrency currency, required bool isAutovalidate }) + DecimalAmountValidator({required Currency currency, required bool isAutovalidate }) : super( errorMessage: S.current.decimal_places_error, pattern: _pattern(currency), @@ -65,7 +66,7 @@ class DecimalAmountValidator extends TextValidator { minLength: 0, maxLength: 0); - static String _pattern(CryptoCurrency currency) { + static String _pattern(Currency currency) { switch (currency) { case CryptoCurrency.xmr: return '^([0-9]+([.\,][0-9]{1,12})?|[.\,][0-9]{1,12})\$'; diff --git a/lib/di.dart b/lib/di.dart index d1b4bda42..78a9b7802 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -183,6 +183,7 @@ import 'package:cake_wallet/ionia/ionia_any_pay_payment_info.dart'; import 'package:cake_wallet/src/screens/receive/fullscreen_qr_page.dart'; import 'package:cake_wallet/core/wallet_loading_service.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cake_wallet/entities/qr_view_data.dart'; final getIt = GetIt.instance; @@ -321,7 +322,9 @@ Future setup( getIt.registerFactory(() => WalletAddressListViewModel( - appStore: getIt.get(), yatStore: getIt.get())); + appStore: getIt.get(), yatStore: getIt.get(), + fiatConversionStore: getIt.get() + )); getIt.registerFactory(() => BalanceViewModel( appStore: getIt.get(), @@ -815,8 +818,8 @@ Future setup( getIt.registerFactory(() => AddressResolver(yatService: getIt.get(), walletType: getIt.get().wallet!.type)); - getIt.registerFactoryParam( - (String qrData, int? version) => FullscreenQRPage(qrData: qrData, version: version,)); + getIt.registerFactoryParam( + (QrViewData viewData, _) => FullscreenQRPage(qrViewData: viewData)); getIt.registerFactory(() => IoniaApi()); diff --git a/lib/entities/qr_view_data.dart b/lib/entities/qr_view_data.dart new file mode 100644 index 000000000..a975d137b --- /dev/null +++ b/lib/entities/qr_view_data.dart @@ -0,0 +1,11 @@ +class QrViewData { + final int? version; + final String? heroTag; + final String data; + + QrViewData({ + this.version, + this.heroTag, + required this.data, + }); +} \ No newline at end of file diff --git a/lib/router.dart b/lib/router.dart index aebee0942..103a0889e 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -2,6 +2,7 @@ import 'package:cake_wallet/anonpay/anonpay_info_base.dart'; import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart'; import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/buy/order.dart'; +import 'package:cake_wallet/entities/qr_view_data.dart'; import 'package:cake_wallet/src/screens/anonpay_details/anonpay_details_page.dart'; import 'package:cake_wallet/src/screens/backup/backup_page.dart'; import 'package:cake_wallet/src/screens/backup/edit_backup_password_page.dart'; @@ -242,7 +243,7 @@ Route createRoute(RouteSettings settings) { case Routes.receive: return CupertinoPageRoute( - fullscreenDialog: true, builder: (_) => getIt.get()); + builder: (_) => getIt.get()); case Routes.addressPage: return CupertinoPageRoute( @@ -451,14 +452,10 @@ Route createRoute(RouteSettings settings) { param1: args)); case Routes.fullscreenQR: - final args = settings.arguments as Map; - return MaterialPageRoute( builder: (_) => getIt.get( - param1: args['qrData'] as String, - param2: args['version'] as int?, - + param1: settings.arguments as QrViewData, )); case Routes.ioniaWelcomePage: diff --git a/lib/src/screens/dashboard/widgets/address_page.dart b/lib/src/screens/dashboard/widgets/address_page.dart index ebfabcd02..cdaa22673 100644 --- a/lib/src/screens/dashboard/widgets/address_page.dart +++ b/lib/src/screens/dashboard/widgets/address_page.dart @@ -26,11 +26,23 @@ class AddressPage extends BasePage { required this.addressListViewModel, required this.dashboardViewModel, required this.receiveOptionViewModel, - }) : _cryptoAmountFocus = FocusNode(); + }) : _cryptoAmountFocus = FocusNode(), + _formKey = GlobalKey(), + _amountController = TextEditingController(){ + _amountController.addListener(() { + if (_formKey.currentState!.validate()) { + addressListViewModel.changeAmount( + _amountController.text, + ); + } + }); + } final WalletAddressListViewModel addressListViewModel; final DashboardViewModel dashboardViewModel; final ReceiveOptionViewModel receiveOptionViewModel; + final TextEditingController _amountController; + final GlobalKey _formKey; final FocusNode _cryptoAmountFocus; @@ -69,28 +81,27 @@ class AddressPage extends BasePage { @override Widget? trailing(BuildContext context) { - final shareImage = Image.asset('assets/images/share.png', - color: Theme.of(context).accentTextTheme!.headline2!.backgroundColor!); - - return !addressListViewModel.hasAddressList - ? Material( - color: Colors.transparent, - child: IconButton( - padding: EdgeInsets.zero, - constraints: BoxConstraints(), - highlightColor: Colors.transparent, - splashColor: Colors.transparent, - iconSize: 25, - onPressed: () { - ShareUtil.share( - text: addressListViewModel.address.address, - context: context, - ); - }, - icon: shareImage, - ), - ) - : null; + return Material( + color: Colors.transparent, + child: IconButton( + padding: EdgeInsets.zero, + constraints: BoxConstraints(), + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + iconSize: 25, + onPressed: () { + ShareUtil.share( + text: addressListViewModel.uri.toString(), + context: context, + ); + }, + icon: Icon( + Icons.share, + size: 20, + color: Theme.of(context).accentTextTheme.headline2!.backgroundColor!, + ), + ), + ); } @override @@ -137,16 +148,18 @@ class AddressPage extends BasePage { ) ]), child: Container( - padding: EdgeInsets.fromLTRB(24, 24, 24, 32), + padding: EdgeInsets.fromLTRB(24, 0, 24, 32), child: Column( children: [ - Expanded( - child: Observer(builder: (_) => QRWidget( - addressListViewModel: addressListViewModel, - amountTextFieldFocusNode: _cryptoAmountFocus, - isAmountFieldShow: !addressListViewModel.hasAccounts, - isLight: dashboardViewModel.settingsStore.currentTheme.type == ThemeType.light)) - ), + Expanded( + child: Observer( + builder: (_) => QRWidget( + formKey: _formKey, + addressListViewModel: addressListViewModel, + amountTextFieldFocusNode: _cryptoAmountFocus, + amountController: _amountController, + isLight: dashboardViewModel.settingsStore.currentTheme.type == + ThemeType.light))), Observer(builder: (_) { return addressListViewModel.hasAddressList ? GestureDetector( diff --git a/lib/src/screens/exchange/exchange_page.dart b/lib/src/screens/exchange/exchange_page.dart index 310e40cd8..a434ed13b 100644 --- a/lib/src/screens/exchange/exchange_page.dart +++ b/lib/src/screens/exchange/exchange_page.dart @@ -115,10 +115,6 @@ class ExchangePage extends BasePage { WidgetsBinding.instance .addPostFrameCallback((_) => _setReactions(context, exchangeViewModel)); - if (exchangeViewModel.isLowFee) { - _showFeeAlert(context); - } - return KeyboardActions( disableScroll: true, config: KeyboardActionsConfig( @@ -319,6 +315,10 @@ class ExchangePage extends BasePage { return; } + if (exchangeViewModel.isLowFee) { + _showFeeAlert(context); + } + final depositAddressController = depositKey.currentState!.addressController; final depositAmountController = depositKey.currentState!.amountController; final receiveAddressController = receiveKey.currentState!.addressController; diff --git a/lib/src/screens/receive/anonpay_receive_page.dart b/lib/src/screens/receive/anonpay_receive_page.dart index 27b5d41a3..1ee947d49 100644 --- a/lib/src/screens/receive/anonpay_receive_page.dart +++ b/lib/src/screens/receive/anonpay_receive_page.dart @@ -1,5 +1,6 @@ import 'package:cake_wallet/anonpay/anonpay_info_base.dart'; import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart'; +import 'package:cake_wallet/entities/qr_view_data.dart'; import 'package:cake_wallet/entities/receive_page_option.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; @@ -133,10 +134,9 @@ class AnonPayReceivePage extends BasePage { await Navigator.pushNamed( context, Routes.fullscreenQR, - arguments: { - 'qrData': invoiceInfo.clearnetUrl, - 'version': qr.QrVersions.auto, - }, + arguments: QrViewData(data: invoiceInfo.clearnetUrl, + version: qr.QrVersions.auto, + ) ); // ignore: unawaited_futures DeviceDisplayBrightness.setBrightness(brightness); diff --git a/lib/src/screens/receive/fullscreen_qr_page.dart b/lib/src/screens/receive/fullscreen_qr_page.dart index 885c548f0..4bde38710 100644 --- a/lib/src/screens/receive/fullscreen_qr_page.dart +++ b/lib/src/screens/receive/fullscreen_qr_page.dart @@ -1,13 +1,13 @@ +import 'package:cake_wallet/entities/qr_view_data.dart'; import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart'; import 'package:cake_wallet/themes/theme_base.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; class FullscreenQRPage extends BasePage { - FullscreenQRPage({required this.qrData, int? this.version}); + FullscreenQRPage({required this.qrViewData}); - final String qrData; - final int? version; + final QrViewData qrViewData; @override Color get backgroundLightColor => currentTheme.type == ThemeType.bright ? Colors.transparent : Colors.white; @@ -63,7 +63,7 @@ class FullscreenQRPage extends BasePage { return Padding( padding: EdgeInsets.symmetric(horizontal: MediaQuery.of(context).size.width * 0.05), child: Hero( - tag: Key(qrData), + tag: Key(qrViewData.heroTag ?? qrViewData.data), child: Center( child: AspectRatio( aspectRatio: 1.0, @@ -71,7 +71,7 @@ class FullscreenQRPage extends BasePage { padding: EdgeInsets.all(10), decoration: BoxDecoration( border: Border.all(width: 3, color: Theme.of(context).accentTextTheme!.headline2!.backgroundColor!)), - child: QrImage(data: qrData, version: version), + child: QrImage(data: qrViewData.data, version: qrViewData.version), ), ), ), diff --git a/lib/src/screens/receive/receive_page.dart b/lib/src/screens/receive/receive_page.dart index 53bda35d8..00a157d97 100644 --- a/lib/src/screens/receive/receive_page.dart +++ b/lib/src/screens/receive/receive_page.dart @@ -21,16 +21,28 @@ import 'package:cake_wallet/src/screens/receive/widgets/qr_widget.dart'; import 'package:keyboard_actions/keyboard_actions.dart'; class ReceivePage extends BasePage { - ReceivePage({required this.addressListViewModel}) : _cryptoAmountFocus = FocusNode(); + ReceivePage({required this.addressListViewModel}) + : _cryptoAmountFocus = FocusNode(), + _amountController = TextEditingController(), + _formKey = GlobalKey() { + _amountController.addListener(() { + if (_formKey.currentState!.validate()) { + addressListViewModel.changeAmount(_amountController.text); + } + }); + } final WalletAddressListViewModel addressListViewModel; + final TextEditingController _amountController; + final GlobalKey _formKey; + static const _heroTag = 'receive_page'; @override String get title => S.current.receive; @override - Color get backgroundLightColor => currentTheme.type == ThemeType.bright - ? Colors.transparent : Colors.white; + Color get backgroundLightColor => + currentTheme.type == ThemeType.bright ? Colors.transparent : Colors.white; @override Color get backgroundDarkColor => Colors.transparent; @@ -68,162 +80,153 @@ class ReceivePage extends BasePage { @override Widget trailing(BuildContext context) { - final shareImage = - Image.asset('assets/images/share.png', - color: Theme.of(context).accentTextTheme!.headline2!.backgroundColor!); - return Material( color: Colors.transparent, child: Semantics( label: 'Share', child: IconButton( - padding: EdgeInsets.zero, - constraints: BoxConstraints(), - highlightColor: Colors.transparent, - splashColor: Colors.transparent, - iconSize: 25, - onPressed: () { - ShareUtil.share( - text: addressListViewModel.address.address, - context: context, - ); - }, - icon: shareImage + padding: EdgeInsets.zero, + constraints: BoxConstraints(), + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + iconSize: 25, + onPressed: () { + ShareUtil.share( + text: addressListViewModel.uri.toString(), + context: context, + ); + }, + icon: Icon( + Icons.share, + size: 20, + color: Theme.of(context).accentTextTheme.headline2!.backgroundColor!, + ), ), - ) - ); + )); } @override Widget body(BuildContext context) { - return (addressListViewModel.type == WalletType.monero || addressListViewModel.type == WalletType.haven) + return (addressListViewModel.type == WalletType.monero || + addressListViewModel.type == WalletType.haven) ? KeyboardActions( - config: KeyboardActionsConfig( - keyboardActionsPlatform: KeyboardActionsPlatform.IOS, - keyboardBarColor: Theme.of(context).accentTextTheme!.bodyText1! - .backgroundColor!, - nextFocus: false, - actions: [ - KeyboardActionsItem( - focusNode: _cryptoAmountFocus, - toolbarButtons: [(_) => KeyboardDoneButton()], - ) - ]), - child: SingleChildScrollView( - child: Column( - children: [ - Padding( - padding: EdgeInsets.fromLTRB(24, 80, 24, 24), - child: QRWidget( - addressListViewModel: addressListViewModel, - isAmountFieldShow: true, - amountTextFieldFocusNode: _cryptoAmountFocus, - isLight: currentTheme.type == ThemeType.light), + config: KeyboardActionsConfig( + keyboardActionsPlatform: KeyboardActionsPlatform.IOS, + keyboardBarColor: Theme.of(context).accentTextTheme!.bodyText1!.backgroundColor!, + nextFocus: false, + actions: [ + KeyboardActionsItem( + focusNode: _cryptoAmountFocus, + toolbarButtons: [(_) => KeyboardDoneButton()], + ) + ]), + child: SingleChildScrollView( + child: Column( + children: [ + Padding( + padding: EdgeInsets.fromLTRB(24, 50, 24, 24), + child: QRWidget( + addressListViewModel: addressListViewModel, + formKey: _formKey, + heroTag: _heroTag, + amountTextFieldFocusNode: _cryptoAmountFocus, + amountController: _amountController, + isLight: currentTheme.type == ThemeType.light), + ), + Observer( + builder: (_) => ListView.separated( + padding: EdgeInsets.all(0), + separatorBuilder: (context, _) => const SectionDivider(), + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: addressListViewModel.items.length, + itemBuilder: (context, index) { + final item = addressListViewModel.items[index]; + Widget cell = Container(); + + if (item is WalletAccountListHeader) { + cell = HeaderTile( + onTap: () async => await showPopUp( + context: context, + builder: (_) => getIt.get()), + title: S.of(context).accounts, + icon: Icon( + Icons.arrow_forward_ios, + size: 14, + color: Theme.of(context).textTheme!.headline4!.color!, + )); + } + + if (item is WalletAddressListHeader) { + cell = HeaderTile( + onTap: () => + Navigator.of(context).pushNamed(Routes.newSubaddress), + title: S.of(context).addresses, + icon: Icon( + Icons.add, + size: 20, + color: Theme.of(context).textTheme!.headline4!.color!, + )); + } + + if (item is WalletAddressListItem) { + cell = Observer(builder: (_) { + final isCurrent = + item.address == addressListViewModel.address.address; + final backgroundColor = isCurrent + ? Theme.of(context).textTheme!.headline2!.decorationColor! + : Theme.of(context).textTheme!.headline3!.decorationColor!; + final textColor = isCurrent + ? Theme.of(context).textTheme!.headline2!.color! + : Theme.of(context).textTheme!.headline3!.color!; + + return AddressCell.fromItem(item, + isCurrent: isCurrent, + backgroundColor: backgroundColor, + textColor: textColor, + onTap: (_) => addressListViewModel.setAddress(item), + onEdit: () => Navigator.of(context) + .pushNamed(Routes.newSubaddress, arguments: item)); + }); + } + + return index != 0 + ? cell + : ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(30), + topRight: Radius.circular(30)), + child: cell, + ); + })), + ], ), - Observer( - builder: (_) => ListView.separated( - padding: EdgeInsets.all(0), - separatorBuilder: (context, _) => const SectionDivider(), - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: addressListViewModel.items.length, - itemBuilder: (context, index) { - final item = addressListViewModel.items[index]; - Widget cell = Container(); - - if (item is WalletAccountListHeader) { - cell = HeaderTile( - onTap: () async => await showPopUp( - context: context, - builder: (_) => - getIt.get()), - title: S.of(context).accounts, - icon: Icon( - Icons.arrow_forward_ios, - size: 14, - color: - Theme.of(context).textTheme!.headline4!.color!, - )); - } - - if (item is WalletAddressListHeader) { - cell = HeaderTile( - onTap: () => Navigator.of(context) - .pushNamed(Routes.newSubaddress), - title: S.of(context).addresses, - icon: Icon( - Icons.add, - size: 20, - color: - Theme.of(context).textTheme!.headline4!.color!, - )); - } - - if (item is WalletAddressListItem) { - cell = Observer(builder: (_) { - final isCurrent = item.address == - addressListViewModel.address.address; - final backgroundColor = isCurrent - ? Theme.of(context) - .textTheme! - .headline2! - .decorationColor! - : Theme.of(context) - .textTheme! - .headline3! - .decorationColor!; - final textColor = isCurrent - ? Theme.of(context).textTheme!.headline2!.color! - : Theme.of(context).textTheme!.headline3!.color!; - - return AddressCell.fromItem(item, - isCurrent: isCurrent, - backgroundColor: backgroundColor, - textColor: textColor, - onTap: (_) => addressListViewModel.setAddress(item), - onEdit: () => Navigator.of(context).pushNamed( - Routes.newSubaddress, - arguments: item)); - }); - } - - return index != 0 - ? cell - : ClipRRect( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(30), - topRight: Radius.circular(30)), - child: cell, - ); - })), - ], - ), - )) : Padding( - padding: EdgeInsets.fromLTRB(24, 24, 24, 32), - child: Column( - children: [ - Expanded( - flex: 7, - child: QRWidget( - addressListViewModel: addressListViewModel, - isAmountFieldShow: true, - amountTextFieldFocusNode: _cryptoAmountFocus, - isLight: currentTheme.type == ThemeType.light), - ), - Expanded( - flex: 2, - child: SizedBox(), - ), - Text(S.of(context).electrum_address_disclaimer, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 15, - color: Theme.of(context) - .accentTextTheme! - .headline3! - .backgroundColor!)), - ], - ), - ); + )) + : Padding( + padding: EdgeInsets.fromLTRB(24, 24, 24, 32), + child: Column( + children: [ + Expanded( + flex: 7, + child: QRWidget( + formKey: _formKey, + heroTag: _heroTag, + addressListViewModel: addressListViewModel, + amountTextFieldFocusNode: _cryptoAmountFocus, + amountController: _amountController, + isLight: currentTheme.type == ThemeType.light), + ), + Expanded( + flex: 2, + child: SizedBox(), + ), + Text(S.of(context).electrum_address_disclaimer, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 15, + color: Theme.of(context).accentTextTheme!.headline3!.backgroundColor!)), + ], + ), + ); } } diff --git a/lib/src/screens/receive/widgets/currency_input_field.dart b/lib/src/screens/receive/widgets/currency_input_field.dart new file mode 100644 index 000000000..286c6f1cd --- /dev/null +++ b/lib/src/screens/receive/widgets/currency_input_field.dart @@ -0,0 +1,120 @@ +import 'package:cake_wallet/core/amount_validator.dart'; +import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; +import 'package:cw_core/currency.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class CurrencyInputField extends StatelessWidget { + const CurrencyInputField({ + super.key, + required this.onTapPicker, + required this.selectedCurrency, + this.focusNode, + required this.controller, + }); + final Function() onTapPicker; + final Currency selectedCurrency; + final FocusNode? focusNode; + final TextEditingController controller; + + @override + Widget build(BuildContext context) { + final arrowBottomPurple = Image.asset( + 'assets/images/arrow_bottom_purple_icon.png', + color: Colors.white, + height: 8, + ); + final _width = MediaQuery.of(context).size.width; + + return Column( + children: [ + Padding( + padding: EdgeInsets.only(top: 20), + child: SizedBox( + height: 40, + child: BaseTextFormField( + focusNode: focusNode, + controller: controller, + keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true), + inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d+(\.|\,)?\d{0,8}'))], + hintText: '0.000', + placeholderTextStyle: TextStyle( + color: Theme.of(context).primaryTextTheme.headline5!.color!, + fontWeight: FontWeight.w600, + ), + borderColor: Theme.of(context).accentTextTheme.headline6!.backgroundColor!, + textColor: Colors.white, + textStyle: TextStyle( + color: Colors.white, + ), + prefixIcon: Padding( + padding: EdgeInsets.only( + left: _width / 4, + ), + child: Container( + padding: EdgeInsets.only(right: 8), + child: InkWell( + onTap: onTapPicker, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: EdgeInsets.only(right: 5), + child: arrowBottomPurple, + ), + Text( + selectedCurrency.name.toUpperCase(), + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + color: Colors.white, + ), + ), + if (selectedCurrency.tag != null) + Padding( + padding: const EdgeInsets.only(right: 3.0), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).primaryTextTheme.headline4!.color!, + borderRadius: BorderRadius.all( + Radius.circular(6), + ), + ), + child: Center( + child: Text( + selectedCurrency.tag!, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Theme.of(context) + .primaryTextTheme + .headline4! + .decorationColor!, + ), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 3.0), + child: Text( + ':', + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 20, + color: Colors.white, + ), + ), + ), + ]), + ), + ), + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/src/screens/receive/widgets/qr_widget.dart b/lib/src/screens/receive/widgets/qr_widget.dart index 9e68ff0e1..fc58d4cec 100644 --- a/lib/src/screens/receive/widgets/qr_widget.dart +++ b/lib/src/screens/receive/widgets/qr_widget.dart @@ -1,37 +1,36 @@ +import 'package:cake_wallet/entities/qr_view_data.dart'; import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart'; +import 'package:cake_wallet/src/screens/receive/widgets/currency_input_field.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/show_bar.dart'; -import 'package:cw_core/wallet_type.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:device_display_brightness/device_display_brightness.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart'; -import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; -import 'package:cake_wallet/core/amount_validator.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; class QRWidget extends StatelessWidget { - QRWidget( - {required this.addressListViewModel, - required this.isLight, - this.qrVersion, - this.isAmountFieldShow = false, - this.amountTextFieldFocusNode}) - : amountController = TextEditingController(), - _formKey = GlobalKey() { - amountController.addListener(() => addressListViewModel?.amount = - _formKey.currentState!.validate() ? amountController.text : ''); - } + QRWidget({ + required this.addressListViewModel, + required this.isLight, + this.qrVersion, + this.heroTag, + required this.amountController, + required this.formKey, + this.amountTextFieldFocusNode, + }); final WalletAddressListViewModel addressListViewModel; - final bool isAmountFieldShow; final TextEditingController amountController; final FocusNode? amountTextFieldFocusNode; - final GlobalKey _formKey; + final GlobalKey formKey; final bool isLight; final int? qrVersion; + final String? heroTag; @override Widget build(BuildContext context) { @@ -40,7 +39,7 @@ class QRWidget extends StatelessWidget { return Column( mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Column( @@ -63,18 +62,18 @@ class QRWidget extends StatelessWidget { flex: 5, child: GestureDetector( onTap: () { - changeBrightnessForRoute(() async { - await Navigator.pushNamed( - context, - Routes.fullscreenQR, - arguments: { - 'qrData': addressListViewModel.uri.toString(), - }, - ); - }); + changeBrightnessForRoute( + () async { + await Navigator.pushNamed(context, Routes.fullscreenQR, + arguments: QrViewData( + data: addressListViewModel.uri.toString(), + heroTag: heroTag, + )); + }, + ); }, child: Hero( - tag: Key(addressListViewModel.uri.toString()), + tag: Key(heroTag ?? addressListViewModel.uri.toString()), child: Center( child: AspectRatio( aspectRatio: 1.0, @@ -83,7 +82,8 @@ class QRWidget extends StatelessWidget { decoration: BoxDecoration( border: Border.all( width: 3, - color: Theme.of(context).accentTextTheme.headline2!.backgroundColor!, + color: + Theme.of(context).accentTextTheme.headline2!.backgroundColor!, ), ), child: QrImage(data: addressListViewModel.uri.toString()), @@ -99,77 +99,77 @@ class QRWidget extends StatelessWidget { ), ], ), - if (isAmountFieldShow) - Padding( + Observer(builder: (_) { + return Padding( padding: EdgeInsets.only(top: 10), child: Row( children: [ Expanded( child: Form( - key: _formKey, - child: BaseTextFormField( + key: formKey, + child: CurrencyInputField( focusNode: amountTextFieldFocusNode, controller: amountController, - keyboardType: TextInputType.numberWithOptions(decimal: true), - inputFormatters: [FilteringTextInputFormatter.deny(RegExp('[\\-|\\ ]'))], - textAlign: TextAlign.center, - hintText: S.of(context).receive_amount, - textColor: Theme.of(context).accentTextTheme!.headline2!.backgroundColor!, - borderColor: Theme.of(context).textTheme!.headline5!.decorationColor!, - validator: AmountValidator( - currency: walletTypeToCryptoCurrency(addressListViewModel!.type), - isAutovalidate: true), - // FIX-ME: Check does it equal to autovalidate: true, - autovalidateMode: AutovalidateMode.always, - placeholderTextStyle: TextStyle( - color: Theme.of(context).hoverColor, - fontSize: 18, - fontWeight: FontWeight.w500, - ), + onTapPicker: () => _presentPicker(context), + selectedCurrency: addressListViewModel.selectedCurrency, ), ), ), ], ), - ), - Padding( - padding: EdgeInsets.only(top: 8, bottom: 8), - child: Builder( - builder: (context) => Observer( - builder: (context) => GestureDetector( - onTap: () { - Clipboard.setData(ClipboardData(text: addressListViewModel!.address.address)); - showBar(context, S.of(context).copied_to_clipboard); - }, - child: Row( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Text( - addressListViewModel!.address.address, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w500, - color: - Theme.of(context).accentTextTheme!.headline2!.backgroundColor!), - ), + ); + }), + Padding( + padding: EdgeInsets.only(top: 20, bottom: 8), + child: Builder( + builder: (context) => Observer( + builder: (context) => GestureDetector( + onTap: () { + Clipboard.setData(ClipboardData(text: addressListViewModel.address.address)); + showBar(context, S.of(context).copied_to_clipboard); + }, + child: Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + addressListViewModel.address.address, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: Theme.of(context).accentTextTheme.headline2!.backgroundColor!), ), - Padding( - padding: EdgeInsets.only(left: 12), - child: copyImage, - ) - ], - ), + ), + Padding( + padding: EdgeInsets.only(left: 12), + child: copyImage, + ) + ], ), ), ), - ) + ), + ) ], ); } + void _presentPicker(BuildContext context) async { + await showPopUp( + builder: (_) => CurrencyPicker( + selectedAtIndex: addressListViewModel.selectedCurrencyIndex, + items: addressListViewModel.currencies, + hintText: S.of(context).search_currency, + onItemSelected: addressListViewModel.selectCurrency, + ), + context: context, + ); + // update amount if currency changed + addressListViewModel.changeAmount(amountController.text); + } + Future changeBrightnessForRoute(Future Function() navigation) async { // if not mobile, just navigate if (!DeviceInfo.instance.isMobile) { diff --git a/lib/src/screens/wallet_keys/wallet_keys_page.dart b/lib/src/screens/wallet_keys/wallet_keys_page.dart index 8c378d174..eb34393cf 100644 --- a/lib/src/screens/wallet_keys/wallet_keys_page.dart +++ b/lib/src/screens/wallet_keys/wallet_keys_page.dart @@ -1,4 +1,5 @@ import 'package:auto_size_text/auto_size_text.dart'; +import 'package:cake_wallet/entities/qr_view_data.dart'; import 'package:cake_wallet/src/widgets/section_divider.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:device_display_brightness/device_display_brightness.dart'; @@ -31,9 +32,7 @@ class WalletKeysPage extends BasePage { await Navigator.pushNamed( context, Routes.fullscreenQR, - arguments: { - 'qrData': (await walletKeysViewModel.url).toString(), - }, + arguments: QrViewData(data: await walletKeysViewModel.url.toString()), ); // ignore: unawaited_futures DeviceDisplayBrightness.setBrightness(brightness); diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index b861261a6..a5e3a6ca7 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -1,5 +1,8 @@ +import 'package:cake_wallet/entities/fiat_currency.dart'; +import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cake_wallet/store/yat/yat_store.dart'; -import 'package:flutter/foundation.dart'; +import 'package:cw_core/currency.dart'; +import 'package:intl/intl.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cake_wallet/utils/list_item.dart'; @@ -11,37 +14,30 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/transaction_info.dart'; -import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/store/app_store.dart'; -import 'dart:async'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/haven/haven.dart'; part 'wallet_address_list_view_model.g.dart'; -class WalletAddressListViewModel = WalletAddressListViewModelBase - with _$WalletAddressListViewModel; +class WalletAddressListViewModel = WalletAddressListViewModelBase with _$WalletAddressListViewModel; abstract class PaymentURI { - PaymentURI({ - required this.amount, - required this.address}); + PaymentURI({required this.amount, required this.address}); final String amount; final String address; } class MoneroURI extends PaymentURI { - MoneroURI({ - required String amount, - required String address}) + MoneroURI({required String amount, required String address}) : super(amount: amount, address: address); @override String toString() { var base = 'monero:' + address; - if (amount?.isNotEmpty ?? false) { + if (amount.isNotEmpty) { base += '?tx_amount=${amount.replaceAll(',', '.')}'; } @@ -50,16 +46,14 @@ class MoneroURI extends PaymentURI { } class HavenURI extends PaymentURI { - HavenURI({ - required String amount, - required String address}) + HavenURI({required String amount, required String address}) : super(amount: amount, address: address); @override String toString() { var base = 'haven:' + address; - if (amount?.isNotEmpty ?? false) { + if (amount.isNotEmpty) { base += '?tx_amount=${amount.replaceAll(',', '.')}'; } @@ -68,16 +62,14 @@ class HavenURI extends PaymentURI { } class BitcoinURI extends PaymentURI { - BitcoinURI({ - required String amount, - required String address}) + BitcoinURI({required String amount, required String address}) : super(amount: amount, address: address); @override String toString() { var base = 'bitcoin:' + address; - if (amount?.isNotEmpty ?? false) { + if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; } @@ -86,16 +78,14 @@ class BitcoinURI extends PaymentURI { } class LitecoinURI extends PaymentURI { - LitecoinURI({ - required String amount, - required String address}) + LitecoinURI({required String amount, required String address}) : super(amount: amount, address: address); @override String toString() { var base = 'litecoin:' + address; - if (amount?.isNotEmpty ?? false) { + if (amount.isNotEmpty) { base += '?amount=${amount.replaceAll(',', '.')}'; } @@ -106,24 +96,33 @@ class LitecoinURI extends PaymentURI { abstract class WalletAddressListViewModelBase with Store { WalletAddressListViewModelBase({ required AppStore appStore, - required this.yatStore - }) : _appStore = appStore, - _baseItems = [], - _wallet = appStore.wallet!, - hasAccounts = appStore.wallet!.type == WalletType.monero || appStore.wallet!.type == WalletType.haven, - amount = '' { - _onWalletChangeReaction = reaction((_) => _appStore.wallet, (WalletBase< - Balance, TransactionHistoryBase, TransactionInfo>? - wallet) { - if (wallet == null) { - return; - } - _wallet = wallet; - hasAccounts = _wallet.type == WalletType.monero; - }); + required this.yatStore, + required this.fiatConversionStore, + }) : _appStore = appStore, + _baseItems = [], + _wallet = appStore.wallet!, + selectedCurrency = walletTypeToCryptoCurrency(appStore.wallet!.type), + _cryptoNumberFormat = NumberFormat(_cryptoNumberPattern), + hasAccounts = + appStore.wallet!.type == WalletType.monero || appStore.wallet!.type == WalletType.haven, + amount = '' { _init(); } + static const String _cryptoNumberPattern = '0.00000000'; + + final NumberFormat _cryptoNumberFormat; + + final FiatConversionStore fiatConversionStore; + + List get currencies => [walletTypeToCryptoCurrency(_wallet.type), ...FiatCurrency.all]; + + @observable + Currency selectedCurrency; + + @computed + int get selectedCurrencyIndex => currencies.indexOf(selectedCurrency); + @observable String amount; @@ -156,8 +155,9 @@ abstract class WalletAddressListViewModelBase with Store { } @computed - ObservableList get items => - ObservableList()..addAll(_baseItems)..addAll(addressList); + ObservableList get items => ObservableList() + ..addAll(_baseItems) + ..addAll(addressList); @computed ObservableList get addressList { @@ -166,10 +166,7 @@ abstract class WalletAddressListViewModelBase with Store { if (wallet.type == WalletType.monero) { final primaryAddress = monero!.getSubaddressList(wallet).subaddresses.first; - final addressItems = monero - !.getSubaddressList(wallet) - .subaddresses - .map((subaddress) { + final addressItems = monero!.getSubaddressList(wallet).subaddresses.map((subaddress) { final isPrimary = subaddress == primaryAddress; return WalletAddressListItem( @@ -183,10 +180,7 @@ abstract class WalletAddressListViewModelBase with Store { if (wallet.type == WalletType.haven) { final primaryAddress = haven!.getSubaddressList(wallet).subaddresses.first; - final addressItems = haven - !.getSubaddressList(wallet) - .subaddresses - .map((subaddress) { + final addressItems = haven!.getSubaddressList(wallet).subaddresses.map((subaddress) { final isPrimary = subaddress == primaryAddress; return WalletAddressListItem( @@ -203,8 +197,7 @@ abstract class WalletAddressListViewModelBase with Store { final bitcoinAddresses = bitcoin!.getAddresses(wallet).map((addr) { final isPrimary = addr == primaryAddress; - return WalletAddressListItem( - isPrimary: isPrimary, name: null, address: addr); + return WalletAddressListItem(isPrimary: isPrimary, name: null, address: addr); }); addressList.addAll(bitcoinAddresses); } @@ -234,8 +227,7 @@ abstract class WalletAddressListViewModelBase with Store { bool get hasAddressList => _wallet.type == WalletType.monero || _wallet.type == WalletType.haven; @observable - WalletBase, TransactionInfo> - _wallet; + WalletBase, TransactionInfo> _wallet; List _baseItems; @@ -243,8 +235,6 @@ abstract class WalletAddressListViewModelBase with Store { final YatStore yatStore; - ReactionDisposer? _onWalletChangeReaction; - @action void setAddress(WalletAddressListItem address) => _wallet.walletAddresses.address = address.address; @@ -258,4 +248,31 @@ abstract class WalletAddressListViewModelBase with Store { _baseItems.add(WalletAddressListHeader()); } + + @action + void selectCurrency(Currency currency) { + selectedCurrency = currency; + } + + @action + void changeAmount(String amount) { + this.amount = amount; + if (selectedCurrency is FiatCurrency) { + _convertAmountToCrypto(); + } + } + + void _convertAmountToCrypto() { + final cryptoCurrency = walletTypeToCryptoCurrency(_wallet.type); + try { + final crypto = + double.parse(amount.replaceAll(',', '.')) / fiatConversionStore.prices[cryptoCurrency]!; + final cryptoAmountTmp = _cryptoNumberFormat.format(crypto); + if (amount != cryptoAmountTmp) { + amount = cryptoAmountTmp; + } + } catch (e) { + amount = ''; + } + } }