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 <justin.ehrenhofer@gmail.com>
Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>
This commit is contained in:
Godwin Asuquo 2023-04-21 21:03:42 +03:00 committed by GitHub
parent 8ffac75e8c
commit f2b8dd21a1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 504 additions and 340 deletions

View file

@ -1,6 +1,7 @@
import 'package:cake_wallet/core/validator.dart'; import 'package:cake_wallet/core/validator.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency.dart';
class AmountValidator extends TextValidator { class AmountValidator extends TextValidator {
AmountValidator({ AmountValidator({
@ -57,7 +58,7 @@ class SymbolsAmountValidator extends TextValidator {
} }
class DecimalAmountValidator extends TextValidator { class DecimalAmountValidator extends TextValidator {
DecimalAmountValidator({required CryptoCurrency currency, required bool isAutovalidate }) DecimalAmountValidator({required Currency currency, required bool isAutovalidate })
: super( : super(
errorMessage: S.current.decimal_places_error, errorMessage: S.current.decimal_places_error,
pattern: _pattern(currency), pattern: _pattern(currency),
@ -65,7 +66,7 @@ class DecimalAmountValidator extends TextValidator {
minLength: 0, minLength: 0,
maxLength: 0); maxLength: 0);
static String _pattern(CryptoCurrency currency) { static String _pattern(Currency currency) {
switch (currency) { switch (currency) {
case CryptoCurrency.xmr: case CryptoCurrency.xmr:
return '^([0-9]+([.\,][0-9]{1,12})?|[.\,][0-9]{1,12})\$'; return '^([0-9]+([.\,][0-9]{1,12})?|[.\,][0-9]{1,12})\$';

View file

@ -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/src/screens/receive/fullscreen_qr_page.dart';
import 'package:cake_wallet/core/wallet_loading_service.dart'; import 'package:cake_wallet/core/wallet_loading_service.dart';
import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/crypto_currency.dart';
import 'package:cake_wallet/entities/qr_view_data.dart';
final getIt = GetIt.instance; final getIt = GetIt.instance;
@ -321,7 +322,9 @@ Future setup(
getIt.registerFactory<WalletAddressListViewModel>(() => getIt.registerFactory<WalletAddressListViewModel>(() =>
WalletAddressListViewModel( WalletAddressListViewModel(
appStore: getIt.get<AppStore>(), yatStore: getIt.get<YatStore>())); appStore: getIt.get<AppStore>(), yatStore: getIt.get<YatStore>(),
fiatConversionStore: getIt.get<FiatConversionStore>()
));
getIt.registerFactory(() => BalanceViewModel( getIt.registerFactory(() => BalanceViewModel(
appStore: getIt.get<AppStore>(), appStore: getIt.get<AppStore>(),
@ -815,8 +818,8 @@ Future setup(
getIt.registerFactory(() => AddressResolver(yatService: getIt.get<YatService>(), getIt.registerFactory(() => AddressResolver(yatService: getIt.get<YatService>(),
walletType: getIt.get<AppStore>().wallet!.type)); walletType: getIt.get<AppStore>().wallet!.type));
getIt.registerFactoryParam<FullscreenQRPage, String, int?>( getIt.registerFactoryParam<FullscreenQRPage, QrViewData, void>(
(String qrData, int? version) => FullscreenQRPage(qrData: qrData, version: version,)); (QrViewData viewData, _) => FullscreenQRPage(qrViewData: viewData));
getIt.registerFactory(() => IoniaApi()); getIt.registerFactory(() => IoniaApi());

View file

@ -0,0 +1,11 @@
class QrViewData {
final int? version;
final String? heroTag;
final String data;
QrViewData({
this.version,
this.heroTag,
required this.data,
});
}

View file

@ -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/anonpay/anonpay_invoice_info.dart';
import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/contact_record.dart';
import 'package:cake_wallet/buy/order.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/anonpay_details/anonpay_details_page.dart';
import 'package:cake_wallet/src/screens/backup/backup_page.dart'; import 'package:cake_wallet/src/screens/backup/backup_page.dart';
import 'package:cake_wallet/src/screens/backup/edit_backup_password_page.dart'; import 'package:cake_wallet/src/screens/backup/edit_backup_password_page.dart';
@ -242,7 +243,7 @@ Route<dynamic> createRoute(RouteSettings settings) {
case Routes.receive: case Routes.receive:
return CupertinoPageRoute<void>( return CupertinoPageRoute<void>(
fullscreenDialog: true, builder: (_) => getIt.get<ReceivePage>()); builder: (_) => getIt.get<ReceivePage>());
case Routes.addressPage: case Routes.addressPage:
return CupertinoPageRoute<void>( return CupertinoPageRoute<void>(
@ -451,14 +452,10 @@ Route<dynamic> createRoute(RouteSettings settings) {
param1: args)); param1: args));
case Routes.fullscreenQR: case Routes.fullscreenQR:
final args = settings.arguments as Map<String, dynamic>;
return MaterialPageRoute<void>( return MaterialPageRoute<void>(
builder: (_) => builder: (_) =>
getIt.get<FullscreenQRPage>( getIt.get<FullscreenQRPage>(
param1: args['qrData'] as String, param1: settings.arguments as QrViewData,
param2: args['version'] as int?,
)); ));
case Routes.ioniaWelcomePage: case Routes.ioniaWelcomePage:

View file

@ -26,11 +26,23 @@ class AddressPage extends BasePage {
required this.addressListViewModel, required this.addressListViewModel,
required this.dashboardViewModel, required this.dashboardViewModel,
required this.receiveOptionViewModel, required this.receiveOptionViewModel,
}) : _cryptoAmountFocus = FocusNode(); }) : _cryptoAmountFocus = FocusNode(),
_formKey = GlobalKey<FormState>(),
_amountController = TextEditingController(){
_amountController.addListener(() {
if (_formKey.currentState!.validate()) {
addressListViewModel.changeAmount(
_amountController.text,
);
}
});
}
final WalletAddressListViewModel addressListViewModel; final WalletAddressListViewModel addressListViewModel;
final DashboardViewModel dashboardViewModel; final DashboardViewModel dashboardViewModel;
final ReceiveOptionViewModel receiveOptionViewModel; final ReceiveOptionViewModel receiveOptionViewModel;
final TextEditingController _amountController;
final GlobalKey<FormState> _formKey;
final FocusNode _cryptoAmountFocus; final FocusNode _cryptoAmountFocus;
@ -69,28 +81,27 @@ class AddressPage extends BasePage {
@override @override
Widget? trailing(BuildContext context) { Widget? trailing(BuildContext context) {
final shareImage = Image.asset('assets/images/share.png', return Material(
color: Theme.of(context).accentTextTheme!.headline2!.backgroundColor!); color: Colors.transparent,
child: IconButton(
return !addressListViewModel.hasAddressList padding: EdgeInsets.zero,
? Material( constraints: BoxConstraints(),
color: Colors.transparent, highlightColor: Colors.transparent,
child: IconButton( splashColor: Colors.transparent,
padding: EdgeInsets.zero, iconSize: 25,
constraints: BoxConstraints(), onPressed: () {
highlightColor: Colors.transparent, ShareUtil.share(
splashColor: Colors.transparent, text: addressListViewModel.uri.toString(),
iconSize: 25, context: context,
onPressed: () { );
ShareUtil.share( },
text: addressListViewModel.address.address, icon: Icon(
context: context, Icons.share,
); size: 20,
}, color: Theme.of(context).accentTextTheme.headline2!.backgroundColor!,
icon: shareImage, ),
), ),
) );
: null;
} }
@override @override
@ -137,16 +148,18 @@ class AddressPage extends BasePage {
) )
]), ]),
child: Container( child: Container(
padding: EdgeInsets.fromLTRB(24, 24, 24, 32), padding: EdgeInsets.fromLTRB(24, 0, 24, 32),
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
child: Observer(builder: (_) => QRWidget( child: Observer(
addressListViewModel: addressListViewModel, builder: (_) => QRWidget(
amountTextFieldFocusNode: _cryptoAmountFocus, formKey: _formKey,
isAmountFieldShow: !addressListViewModel.hasAccounts, addressListViewModel: addressListViewModel,
isLight: dashboardViewModel.settingsStore.currentTheme.type == ThemeType.light)) amountTextFieldFocusNode: _cryptoAmountFocus,
), amountController: _amountController,
isLight: dashboardViewModel.settingsStore.currentTheme.type ==
ThemeType.light))),
Observer(builder: (_) { Observer(builder: (_) {
return addressListViewModel.hasAddressList return addressListViewModel.hasAddressList
? GestureDetector( ? GestureDetector(

View file

@ -115,10 +115,6 @@ class ExchangePage extends BasePage {
WidgetsBinding.instance WidgetsBinding.instance
.addPostFrameCallback((_) => _setReactions(context, exchangeViewModel)); .addPostFrameCallback((_) => _setReactions(context, exchangeViewModel));
if (exchangeViewModel.isLowFee) {
_showFeeAlert(context);
}
return KeyboardActions( return KeyboardActions(
disableScroll: true, disableScroll: true,
config: KeyboardActionsConfig( config: KeyboardActionsConfig(
@ -319,6 +315,10 @@ class ExchangePage extends BasePage {
return; return;
} }
if (exchangeViewModel.isLowFee) {
_showFeeAlert(context);
}
final depositAddressController = depositKey.currentState!.addressController; final depositAddressController = depositKey.currentState!.addressController;
final depositAmountController = depositKey.currentState!.amountController; final depositAmountController = depositKey.currentState!.amountController;
final receiveAddressController = receiveKey.currentState!.addressController; final receiveAddressController = receiveKey.currentState!.addressController;

View file

@ -1,5 +1,6 @@
import 'package:cake_wallet/anonpay/anonpay_info_base.dart'; import 'package:cake_wallet/anonpay/anonpay_info_base.dart';
import 'package:cake_wallet/anonpay/anonpay_invoice_info.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/entities/receive_page_option.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/routes.dart';
@ -133,10 +134,9 @@ class AnonPayReceivePage extends BasePage {
await Navigator.pushNamed( await Navigator.pushNamed(
context, context,
Routes.fullscreenQR, Routes.fullscreenQR,
arguments: { arguments: QrViewData(data: invoiceInfo.clearnetUrl,
'qrData': invoiceInfo.clearnetUrl, version: qr.QrVersions.auto,
'version': qr.QrVersions.auto, )
},
); );
// ignore: unawaited_futures // ignore: unawaited_futures
DeviceDisplayBrightness.setBrightness(brightness); DeviceDisplayBrightness.setBrightness(brightness);

View file

@ -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/src/screens/receive/widgets/qr_image.dart';
import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/themes/theme_base.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/base_page.dart';
class FullscreenQRPage extends BasePage { class FullscreenQRPage extends BasePage {
FullscreenQRPage({required this.qrData, int? this.version}); FullscreenQRPage({required this.qrViewData});
final String qrData; final QrViewData qrViewData;
final int? version;
@override @override
Color get backgroundLightColor => currentTheme.type == ThemeType.bright ? Colors.transparent : Colors.white; Color get backgroundLightColor => currentTheme.type == ThemeType.bright ? Colors.transparent : Colors.white;
@ -63,7 +63,7 @@ class FullscreenQRPage extends BasePage {
return Padding( return Padding(
padding: EdgeInsets.symmetric(horizontal: MediaQuery.of(context).size.width * 0.05), padding: EdgeInsets.symmetric(horizontal: MediaQuery.of(context).size.width * 0.05),
child: Hero( child: Hero(
tag: Key(qrData), tag: Key(qrViewData.heroTag ?? qrViewData.data),
child: Center( child: Center(
child: AspectRatio( child: AspectRatio(
aspectRatio: 1.0, aspectRatio: 1.0,
@ -71,7 +71,7 @@ class FullscreenQRPage extends BasePage {
padding: EdgeInsets.all(10), padding: EdgeInsets.all(10),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all(width: 3, color: Theme.of(context).accentTextTheme!.headline2!.backgroundColor!)), 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),
), ),
), ),
), ),

View file

@ -21,16 +21,28 @@ import 'package:cake_wallet/src/screens/receive/widgets/qr_widget.dart';
import 'package:keyboard_actions/keyboard_actions.dart'; import 'package:keyboard_actions/keyboard_actions.dart';
class ReceivePage extends BasePage { class ReceivePage extends BasePage {
ReceivePage({required this.addressListViewModel}) : _cryptoAmountFocus = FocusNode(); ReceivePage({required this.addressListViewModel})
: _cryptoAmountFocus = FocusNode(),
_amountController = TextEditingController(),
_formKey = GlobalKey<FormState>() {
_amountController.addListener(() {
if (_formKey.currentState!.validate()) {
addressListViewModel.changeAmount(_amountController.text);
}
});
}
final WalletAddressListViewModel addressListViewModel; final WalletAddressListViewModel addressListViewModel;
final TextEditingController _amountController;
final GlobalKey<FormState> _formKey;
static const _heroTag = 'receive_page';
@override @override
String get title => S.current.receive; String get title => S.current.receive;
@override @override
Color get backgroundLightColor => currentTheme.type == ThemeType.bright Color get backgroundLightColor =>
? Colors.transparent : Colors.white; currentTheme.type == ThemeType.bright ? Colors.transparent : Colors.white;
@override @override
Color get backgroundDarkColor => Colors.transparent; Color get backgroundDarkColor => Colors.transparent;
@ -68,162 +80,153 @@ class ReceivePage extends BasePage {
@override @override
Widget trailing(BuildContext context) { Widget trailing(BuildContext context) {
final shareImage =
Image.asset('assets/images/share.png',
color: Theme.of(context).accentTextTheme!.headline2!.backgroundColor!);
return Material( return Material(
color: Colors.transparent, color: Colors.transparent,
child: Semantics( child: Semantics(
label: 'Share', label: 'Share',
child: IconButton( child: IconButton(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
constraints: BoxConstraints(), constraints: BoxConstraints(),
highlightColor: Colors.transparent, highlightColor: Colors.transparent,
splashColor: Colors.transparent, splashColor: Colors.transparent,
iconSize: 25, iconSize: 25,
onPressed: () { onPressed: () {
ShareUtil.share( ShareUtil.share(
text: addressListViewModel.address.address, text: addressListViewModel.uri.toString(),
context: context, context: context,
); );
}, },
icon: shareImage icon: Icon(
Icons.share,
size: 20,
color: Theme.of(context).accentTextTheme.headline2!.backgroundColor!,
),
), ),
) ));
);
} }
@override @override
Widget body(BuildContext context) { Widget body(BuildContext context) {
return (addressListViewModel.type == WalletType.monero || addressListViewModel.type == WalletType.haven) return (addressListViewModel.type == WalletType.monero ||
addressListViewModel.type == WalletType.haven)
? KeyboardActions( ? KeyboardActions(
config: KeyboardActionsConfig( config: KeyboardActionsConfig(
keyboardActionsPlatform: KeyboardActionsPlatform.IOS, keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
keyboardBarColor: Theme.of(context).accentTextTheme!.bodyText1! keyboardBarColor: Theme.of(context).accentTextTheme!.bodyText1!.backgroundColor!,
.backgroundColor!, nextFocus: false,
nextFocus: false, actions: [
actions: [ KeyboardActionsItem(
KeyboardActionsItem( focusNode: _cryptoAmountFocus,
focusNode: _cryptoAmountFocus, toolbarButtons: [(_) => KeyboardDoneButton()],
toolbarButtons: [(_) => KeyboardDoneButton()], )
) ]),
]), child: SingleChildScrollView(
child: SingleChildScrollView( child: Column(
child: Column( children: <Widget>[
children: <Widget>[ Padding(
Padding( padding: EdgeInsets.fromLTRB(24, 50, 24, 24),
padding: EdgeInsets.fromLTRB(24, 80, 24, 24), child: QRWidget(
child: QRWidget( addressListViewModel: addressListViewModel,
addressListViewModel: addressListViewModel, formKey: _formKey,
isAmountFieldShow: true, heroTag: _heroTag,
amountTextFieldFocusNode: _cryptoAmountFocus, amountTextFieldFocusNode: _cryptoAmountFocus,
isLight: currentTheme.type == ThemeType.light), 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<void>(
context: context,
builder: (_) => getIt.get<MoneroAccountListPage>()),
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(
padding: EdgeInsets.all(0), padding: EdgeInsets.fromLTRB(24, 24, 24, 32),
separatorBuilder: (context, _) => const SectionDivider(), child: Column(
shrinkWrap: true, children: [
physics: NeverScrollableScrollPhysics(), Expanded(
itemCount: addressListViewModel.items.length, flex: 7,
itemBuilder: (context, index) { child: QRWidget(
final item = addressListViewModel.items[index]; formKey: _formKey,
Widget cell = Container(); heroTag: _heroTag,
addressListViewModel: addressListViewModel,
if (item is WalletAccountListHeader) { amountTextFieldFocusNode: _cryptoAmountFocus,
cell = HeaderTile( amountController: _amountController,
onTap: () async => await showPopUp<void>( isLight: currentTheme.type == ThemeType.light),
context: context, ),
builder: (_) => Expanded(
getIt.get<MoneroAccountListPage>()), flex: 2,
title: S.of(context).accounts, child: SizedBox(),
icon: Icon( ),
Icons.arrow_forward_ios, Text(S.of(context).electrum_address_disclaimer,
size: 14, textAlign: TextAlign.center,
color: style: TextStyle(
Theme.of(context).textTheme!.headline4!.color!, fontSize: 15,
)); color: Theme.of(context).accentTextTheme!.headline3!.backgroundColor!)),
} ],
),
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!)),
],
),
);
} }
} }

View file

@ -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: <Widget>[
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,
),
),
),
]),
),
),
),
),
),
),
],
);
}
}

View file

@ -1,37 +1,36 @@
import 'package:cake_wallet/entities/qr_view_data.dart';
import 'package:cake_wallet/routes.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/device_info.dart';
import 'package:cake_wallet/utils/show_bar.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:device_display_brightness/device_display_brightness.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/receive/widgets/qr_image.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'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
class QRWidget extends StatelessWidget { class QRWidget extends StatelessWidget {
QRWidget( QRWidget({
{required this.addressListViewModel, required this.addressListViewModel,
required this.isLight, required this.isLight,
this.qrVersion, this.qrVersion,
this.isAmountFieldShow = false, this.heroTag,
this.amountTextFieldFocusNode}) required this.amountController,
: amountController = TextEditingController(), required this.formKey,
_formKey = GlobalKey<FormState>() { this.amountTextFieldFocusNode,
amountController.addListener(() => addressListViewModel?.amount = });
_formKey.currentState!.validate() ? amountController.text : '');
}
final WalletAddressListViewModel addressListViewModel; final WalletAddressListViewModel addressListViewModel;
final bool isAmountFieldShow;
final TextEditingController amountController; final TextEditingController amountController;
final FocusNode? amountTextFieldFocusNode; final FocusNode? amountTextFieldFocusNode;
final GlobalKey<FormState> _formKey; final GlobalKey<FormState> formKey;
final bool isLight; final bool isLight;
final int? qrVersion; final int? qrVersion;
final String? heroTag;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -40,7 +39,7 @@ class QRWidget extends StatelessWidget {
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[ children: <Widget>[
Column( Column(
@ -63,18 +62,18 @@ class QRWidget extends StatelessWidget {
flex: 5, flex: 5,
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
changeBrightnessForRoute(() async { changeBrightnessForRoute(
await Navigator.pushNamed( () async {
context, await Navigator.pushNamed(context, Routes.fullscreenQR,
Routes.fullscreenQR, arguments: QrViewData(
arguments: { data: addressListViewModel.uri.toString(),
'qrData': addressListViewModel.uri.toString(), heroTag: heroTag,
}, ));
); },
}); );
}, },
child: Hero( child: Hero(
tag: Key(addressListViewModel.uri.toString()), tag: Key(heroTag ?? addressListViewModel.uri.toString()),
child: Center( child: Center(
child: AspectRatio( child: AspectRatio(
aspectRatio: 1.0, aspectRatio: 1.0,
@ -83,7 +82,8 @@ class QRWidget extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all( border: Border.all(
width: 3, width: 3,
color: Theme.of(context).accentTextTheme.headline2!.backgroundColor!, color:
Theme.of(context).accentTextTheme.headline2!.backgroundColor!,
), ),
), ),
child: QrImage(data: addressListViewModel.uri.toString()), child: QrImage(data: addressListViewModel.uri.toString()),
@ -99,77 +99,77 @@ class QRWidget extends StatelessWidget {
), ),
], ],
), ),
if (isAmountFieldShow) Observer(builder: (_) {
Padding( return Padding(
padding: EdgeInsets.only(top: 10), padding: EdgeInsets.only(top: 10),
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
child: Form( child: Form(
key: _formKey, key: formKey,
child: BaseTextFormField( child: CurrencyInputField(
focusNode: amountTextFieldFocusNode, focusNode: amountTextFieldFocusNode,
controller: amountController, controller: amountController,
keyboardType: TextInputType.numberWithOptions(decimal: true), onTapPicker: () => _presentPicker(context),
inputFormatters: [FilteringTextInputFormatter.deny(RegExp('[\\-|\\ ]'))], selectedCurrency: addressListViewModel.selectedCurrency,
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,
),
), ),
), ),
), ),
], ],
), ),
), );
Padding( }),
padding: EdgeInsets.only(top: 8, bottom: 8), Padding(
child: Builder( padding: EdgeInsets.only(top: 20, bottom: 8),
builder: (context) => Observer( child: Builder(
builder: (context) => GestureDetector( builder: (context) => Observer(
onTap: () { builder: (context) => GestureDetector(
Clipboard.setData(ClipboardData(text: addressListViewModel!.address.address)); onTap: () {
showBar<void>(context, S.of(context).copied_to_clipboard); Clipboard.setData(ClipboardData(text: addressListViewModel.address.address));
}, showBar<void>(context, S.of(context).copied_to_clipboard);
child: Row( },
mainAxisSize: MainAxisSize.max, child: Row(
crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.max,
children: <Widget>[ crossAxisAlignment: CrossAxisAlignment.start,
Expanded( children: <Widget>[
child: Text( Expanded(
addressListViewModel!.address.address, child: Text(
textAlign: TextAlign.center, addressListViewModel.address.address,
style: TextStyle( textAlign: TextAlign.center,
fontSize: 15, style: TextStyle(
fontWeight: FontWeight.w500, fontSize: 15,
color: fontWeight: FontWeight.w500,
Theme.of(context).accentTextTheme!.headline2!.backgroundColor!), color: Theme.of(context).accentTextTheme.headline2!.backgroundColor!),
),
), ),
Padding( ),
padding: EdgeInsets.only(left: 12), Padding(
child: copyImage, padding: EdgeInsets.only(left: 12),
) child: copyImage,
], )
), ],
), ),
), ),
), ),
) ),
)
], ],
); );
} }
void _presentPicker(BuildContext context) async {
await showPopUp<void>(
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<void> changeBrightnessForRoute(Future<void> Function() navigation) async { Future<void> changeBrightnessForRoute(Future<void> Function() navigation) async {
// if not mobile, just navigate // if not mobile, just navigate
if (!DeviceInfo.instance.isMobile) { if (!DeviceInfo.instance.isMobile) {

View file

@ -1,4 +1,5 @@
import 'package:auto_size_text/auto_size_text.dart'; 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/src/widgets/section_divider.dart';
import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/utils/show_bar.dart';
import 'package:device_display_brightness/device_display_brightness.dart'; import 'package:device_display_brightness/device_display_brightness.dart';
@ -31,9 +32,7 @@ class WalletKeysPage extends BasePage {
await Navigator.pushNamed( await Navigator.pushNamed(
context, context,
Routes.fullscreenQR, Routes.fullscreenQR,
arguments: { arguments: QrViewData(data: await walletKeysViewModel.url.toString()),
'qrData': (await walletKeysViewModel.url).toString(),
},
); );
// ignore: unawaited_futures // ignore: unawaited_futures
DeviceDisplayBrightness.setBrightness(brightness); DeviceDisplayBrightness.setBrightness(brightness);

View file

@ -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: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:mobx/mobx.dart';
import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_base.dart';
import 'package:cake_wallet/utils/list_item.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/transaction_history.dart';
import 'package:cw_core/balance.dart'; import 'package:cw_core/balance.dart';
import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/app_store.dart';
import 'dart:async';
import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/haven/haven.dart';
part 'wallet_address_list_view_model.g.dart'; part 'wallet_address_list_view_model.g.dart';
class WalletAddressListViewModel = WalletAddressListViewModelBase class WalletAddressListViewModel = WalletAddressListViewModelBase with _$WalletAddressListViewModel;
with _$WalletAddressListViewModel;
abstract class PaymentURI { abstract class PaymentURI {
PaymentURI({ PaymentURI({required this.amount, required this.address});
required this.amount,
required this.address});
final String amount; final String amount;
final String address; final String address;
} }
class MoneroURI extends PaymentURI { class MoneroURI extends PaymentURI {
MoneroURI({ MoneroURI({required String amount, required String address})
required String amount,
required String address})
: super(amount: amount, address: address); : super(amount: amount, address: address);
@override @override
String toString() { String toString() {
var base = 'monero:' + address; var base = 'monero:' + address;
if (amount?.isNotEmpty ?? false) { if (amount.isNotEmpty) {
base += '?tx_amount=${amount.replaceAll(',', '.')}'; base += '?tx_amount=${amount.replaceAll(',', '.')}';
} }
@ -50,16 +46,14 @@ class MoneroURI extends PaymentURI {
} }
class HavenURI extends PaymentURI { class HavenURI extends PaymentURI {
HavenURI({ HavenURI({required String amount, required String address})
required String amount,
required String address})
: super(amount: amount, address: address); : super(amount: amount, address: address);
@override @override
String toString() { String toString() {
var base = 'haven:' + address; var base = 'haven:' + address;
if (amount?.isNotEmpty ?? false) { if (amount.isNotEmpty) {
base += '?tx_amount=${amount.replaceAll(',', '.')}'; base += '?tx_amount=${amount.replaceAll(',', '.')}';
} }
@ -68,16 +62,14 @@ class HavenURI extends PaymentURI {
} }
class BitcoinURI extends PaymentURI { class BitcoinURI extends PaymentURI {
BitcoinURI({ BitcoinURI({required String amount, required String address})
required String amount,
required String address})
: super(amount: amount, address: address); : super(amount: amount, address: address);
@override @override
String toString() { String toString() {
var base = 'bitcoin:' + address; var base = 'bitcoin:' + address;
if (amount?.isNotEmpty ?? false) { if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}'; base += '?amount=${amount.replaceAll(',', '.')}';
} }
@ -86,16 +78,14 @@ class BitcoinURI extends PaymentURI {
} }
class LitecoinURI extends PaymentURI { class LitecoinURI extends PaymentURI {
LitecoinURI({ LitecoinURI({required String amount, required String address})
required String amount,
required String address})
: super(amount: amount, address: address); : super(amount: amount, address: address);
@override @override
String toString() { String toString() {
var base = 'litecoin:' + address; var base = 'litecoin:' + address;
if (amount?.isNotEmpty ?? false) { if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}'; base += '?amount=${amount.replaceAll(',', '.')}';
} }
@ -106,24 +96,33 @@ class LitecoinURI extends PaymentURI {
abstract class WalletAddressListViewModelBase with Store { abstract class WalletAddressListViewModelBase with Store {
WalletAddressListViewModelBase({ WalletAddressListViewModelBase({
required AppStore appStore, required AppStore appStore,
required this.yatStore required this.yatStore,
}) : _appStore = appStore, required this.fiatConversionStore,
_baseItems = <ListItem>[], }) : _appStore = appStore,
_wallet = appStore.wallet!, _baseItems = <ListItem>[],
hasAccounts = appStore.wallet!.type == WalletType.monero || appStore.wallet!.type == WalletType.haven, _wallet = appStore.wallet!,
amount = '' { selectedCurrency = walletTypeToCryptoCurrency(appStore.wallet!.type),
_onWalletChangeReaction = reaction((_) => _appStore.wallet, (WalletBase< _cryptoNumberFormat = NumberFormat(_cryptoNumberPattern),
Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>? hasAccounts =
wallet) { appStore.wallet!.type == WalletType.monero || appStore.wallet!.type == WalletType.haven,
if (wallet == null) { amount = '' {
return;
}
_wallet = wallet;
hasAccounts = _wallet.type == WalletType.monero;
});
_init(); _init();
} }
static const String _cryptoNumberPattern = '0.00000000';
final NumberFormat _cryptoNumberFormat;
final FiatConversionStore fiatConversionStore;
List<Currency> get currencies => [walletTypeToCryptoCurrency(_wallet.type), ...FiatCurrency.all];
@observable
Currency selectedCurrency;
@computed
int get selectedCurrencyIndex => currencies.indexOf(selectedCurrency);
@observable @observable
String amount; String amount;
@ -156,8 +155,9 @@ abstract class WalletAddressListViewModelBase with Store {
} }
@computed @computed
ObservableList<ListItem> get items => ObservableList<ListItem> get items => ObservableList<ListItem>()
ObservableList<ListItem>()..addAll(_baseItems)..addAll(addressList); ..addAll(_baseItems)
..addAll(addressList);
@computed @computed
ObservableList<ListItem> get addressList { ObservableList<ListItem> get addressList {
@ -166,10 +166,7 @@ abstract class WalletAddressListViewModelBase with Store {
if (wallet.type == WalletType.monero) { if (wallet.type == WalletType.monero) {
final primaryAddress = monero!.getSubaddressList(wallet).subaddresses.first; final primaryAddress = monero!.getSubaddressList(wallet).subaddresses.first;
final addressItems = monero final addressItems = monero!.getSubaddressList(wallet).subaddresses.map((subaddress) {
!.getSubaddressList(wallet)
.subaddresses
.map((subaddress) {
final isPrimary = subaddress == primaryAddress; final isPrimary = subaddress == primaryAddress;
return WalletAddressListItem( return WalletAddressListItem(
@ -183,10 +180,7 @@ abstract class WalletAddressListViewModelBase with Store {
if (wallet.type == WalletType.haven) { if (wallet.type == WalletType.haven) {
final primaryAddress = haven!.getSubaddressList(wallet).subaddresses.first; final primaryAddress = haven!.getSubaddressList(wallet).subaddresses.first;
final addressItems = haven final addressItems = haven!.getSubaddressList(wallet).subaddresses.map((subaddress) {
!.getSubaddressList(wallet)
.subaddresses
.map((subaddress) {
final isPrimary = subaddress == primaryAddress; final isPrimary = subaddress == primaryAddress;
return WalletAddressListItem( return WalletAddressListItem(
@ -203,8 +197,7 @@ abstract class WalletAddressListViewModelBase with Store {
final bitcoinAddresses = bitcoin!.getAddresses(wallet).map((addr) { final bitcoinAddresses = bitcoin!.getAddresses(wallet).map((addr) {
final isPrimary = addr == primaryAddress; final isPrimary = addr == primaryAddress;
return WalletAddressListItem( return WalletAddressListItem(isPrimary: isPrimary, name: null, address: addr);
isPrimary: isPrimary, name: null, address: addr);
}); });
addressList.addAll(bitcoinAddresses); addressList.addAll(bitcoinAddresses);
} }
@ -234,8 +227,7 @@ abstract class WalletAddressListViewModelBase with Store {
bool get hasAddressList => _wallet.type == WalletType.monero || _wallet.type == WalletType.haven; bool get hasAddressList => _wallet.type == WalletType.monero || _wallet.type == WalletType.haven;
@observable @observable
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> _wallet;
_wallet;
List<ListItem> _baseItems; List<ListItem> _baseItems;
@ -243,8 +235,6 @@ abstract class WalletAddressListViewModelBase with Store {
final YatStore yatStore; final YatStore yatStore;
ReactionDisposer? _onWalletChangeReaction;
@action @action
void setAddress(WalletAddressListItem address) => void setAddress(WalletAddressListItem address) =>
_wallet.walletAddresses.address = address.address; _wallet.walletAddresses.address = address.address;
@ -258,4 +248,31 @@ abstract class WalletAddressListViewModelBase with Store {
_baseItems.add(WalletAddressListHeader()); _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 = '';
}
}
} }