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>

Godwin Asuquo committed Apr 21, 2023 at 21:03 UTC f2b8dd21a1acd4e5eedfe314782780b21d022217
13 files changed +504 -340
lib/core/amount_validator.dart
+3 -2
@@ -1,6 +1,7 @@
1 import 'package:cake_wallet/core/validator.dart';
2 import 'package:cake_wallet/generated/i18n.dart';
3 import 'package:cw_core/crypto_currency.dart';
4 +import 'package:cw_core/currency.dart';
5
6 class AmountValidator extends TextValidator {
7 AmountValidator({
@@ -57,7 +58,7 @@ class SymbolsAmountValidator extends TextValidator {
58 }
59
60 class DecimalAmountValidator extends TextValidator {
60 - DecimalAmountValidator({required CryptoCurrency currency, required bool isAutovalidate })
61 + DecimalAmountValidator({required Currency currency, required bool isAutovalidate })
62 : super(
63 errorMessage: S.current.decimal_places_error,
64 pattern: _pattern(currency),
@@ -65,7 +66,7 @@ class DecimalAmountValidator extends TextValidator {
66 minLength: 0,
67 maxLength: 0);
68
68 - static String _pattern(CryptoCurrency currency) {
69 + static String _pattern(Currency currency) {
70 switch (currency) {
71 case CryptoCurrency.xmr:
72 return '^([0-9]+([.\,][0-9]{1,12})?|[.\,][0-9]{1,12})\$';
lib/di.dart
+6 -3
@@ -183,6 +183,7 @@ import 'package:cake_wallet/ionia/ionia_any_pay_payment_info.dart';
183 import 'package:cake_wallet/src/screens/receive/fullscreen_qr_page.dart';
184 import 'package:cake_wallet/core/wallet_loading_service.dart';
185 import 'package:cw_core/crypto_currency.dart';
186 +import 'package:cake_wallet/entities/qr_view_data.dart';
187
188 final getIt = GetIt.instance;
189
@@ -321,7 +322,9 @@ Future setup(
322
323 getIt.registerFactory<WalletAddressListViewModel>(() =>
324 WalletAddressListViewModel(
324 - appStore: getIt.get<AppStore>(), yatStore: getIt.get<YatStore>()));
325 + appStore: getIt.get<AppStore>(), yatStore: getIt.get<YatStore>(),
326 + fiatConversionStore: getIt.get<FiatConversionStore>()
327 + ));
328
329 getIt.registerFactory(() => BalanceViewModel(
330 appStore: getIt.get<AppStore>(),
@@ -815,8 +818,8 @@ Future setup(
818 getIt.registerFactory(() => AddressResolver(yatService: getIt.get<YatService>(),
819 walletType: getIt.get<AppStore>().wallet!.type));
820
818 - getIt.registerFactoryParam<FullscreenQRPage, String, int?>(
819 - (String qrData, int? version) => FullscreenQRPage(qrData: qrData, version: version,));
821 + getIt.registerFactoryParam<FullscreenQRPage, QrViewData, void>(
822 + (QrViewData viewData, _) => FullscreenQRPage(qrViewData: viewData));
823
824 getIt.registerFactory(() => IoniaApi());
825
lib/entities/qr_view_data.dart new
+11
@@ -0,0 +1,11 @@
1 +class QrViewData {
2 + final int? version;
3 + final String? heroTag;
4 + final String data;
5 +
6 + QrViewData({
7 + this.version,
8 + this.heroTag,
9 + required this.data,
10 + });
11 +}
\ No newline at end of file
lib/router.dart
+3 -6
@@ -2,6 +2,7 @@ import 'package:cake_wallet/anonpay/anonpay_info_base.dart';
2 import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart';
3 import 'package:cake_wallet/entities/contact_record.dart';
4 import 'package:cake_wallet/buy/order.dart';
5 +import 'package:cake_wallet/entities/qr_view_data.dart';
6 import 'package:cake_wallet/src/screens/anonpay_details/anonpay_details_page.dart';
7 import 'package:cake_wallet/src/screens/backup/backup_page.dart';
8 import 'package:cake_wallet/src/screens/backup/edit_backup_password_page.dart';
@@ -242,7 +243,7 @@ Route<dynamic> createRoute(RouteSettings settings) {
243
244 case Routes.receive:
245 return CupertinoPageRoute<void>(
245 - fullscreenDialog: true, builder: (_) => getIt.get<ReceivePage>());
246 + builder: (_) => getIt.get<ReceivePage>());
247
248 case Routes.addressPage:
249 return CupertinoPageRoute<void>(
@@ -451,14 +452,10 @@ Route<dynamic> createRoute(RouteSettings settings) {
452 param1: args));
453
454 case Routes.fullscreenQR:
454 - final args = settings.arguments as Map<String, dynamic>;
455 -
455 return MaterialPageRoute<void>(
456 builder: (_) =>
457 getIt.get<FullscreenQRPage>(
459 - param1: args['qrData'] as String,
460 - param2: args['version'] as int?,
461 -
458 + param1: settings.arguments as QrViewData,
459 ));
460
461 case Routes.ioniaWelcomePage:
lib/src/screens/dashboard/widgets/address_page.dart
+44 -31
@@ -26,11 +26,23 @@ class AddressPage extends BasePage {
26 required this.addressListViewModel,
27 required this.dashboardViewModel,
28 required this.receiveOptionViewModel,
29 - }) : _cryptoAmountFocus = FocusNode();
29 + }) : _cryptoAmountFocus = FocusNode(),
30 + _formKey = GlobalKey<FormState>(),
31 + _amountController = TextEditingController(){
32 + _amountController.addListener(() {
33 + if (_formKey.currentState!.validate()) {
34 + addressListViewModel.changeAmount(
35 + _amountController.text,
36 + );
37 + }
38 + });
39 + }
40
41 final WalletAddressListViewModel addressListViewModel;
42 final DashboardViewModel dashboardViewModel;
43 final ReceiveOptionViewModel receiveOptionViewModel;
44 + final TextEditingController _amountController;
45 + final GlobalKey<FormState> _formKey;
46
47 final FocusNode _cryptoAmountFocus;
48
@@ -69,28 +81,27 @@ class AddressPage extends BasePage {
81
82 @override
83 Widget? trailing(BuildContext context) {
72 - final shareImage = Image.asset('assets/images/share.png',
73 - color: Theme.of(context).accentTextTheme!.headline2!.backgroundColor!);
74 -
75 - return !addressListViewModel.hasAddressList
76 - ? Material(
77 - color: Colors.transparent,
78 - child: IconButton(
79 - padding: EdgeInsets.zero,
80 - constraints: BoxConstraints(),
81 - highlightColor: Colors.transparent,
82 - splashColor: Colors.transparent,
83 - iconSize: 25,
84 - onPressed: () {
85 - ShareUtil.share(
86 - text: addressListViewModel.address.address,
87 - context: context,
88 - );
89 - },
90 - icon: shareImage,
91 - ),
92 - )
93 - : null;
84 + return Material(
85 + color: Colors.transparent,
86 + child: IconButton(
87 + padding: EdgeInsets.zero,
88 + constraints: BoxConstraints(),
89 + highlightColor: Colors.transparent,
90 + splashColor: Colors.transparent,
91 + iconSize: 25,
92 + onPressed: () {
93 + ShareUtil.share(
94 + text: addressListViewModel.uri.toString(),
95 + context: context,
96 + );
97 + },
98 + icon: Icon(
99 + Icons.share,
100 + size: 20,
101 + color: Theme.of(context).accentTextTheme.headline2!.backgroundColor!,
102 + ),
103 + ),
104 + );
105 }
106
107 @override
@@ -137,16 +148,18 @@ class AddressPage extends BasePage {
148 )
149 ]),
150 child: Container(
140 - padding: EdgeInsets.fromLTRB(24, 24, 24, 32),
151 + padding: EdgeInsets.fromLTRB(24, 0, 24, 32),
152 child: Column(
153 children: <Widget>[
143 - Expanded(
144 - child: Observer(builder: (_) => QRWidget(
145 - addressListViewModel: addressListViewModel,
146 - amountTextFieldFocusNode: _cryptoAmountFocus,
147 - isAmountFieldShow: !addressListViewModel.hasAccounts,
148 - isLight: dashboardViewModel.settingsStore.currentTheme.type == ThemeType.light))
149 - ),
154 + Expanded(
155 + child: Observer(
156 + builder: (_) => QRWidget(
157 + formKey: _formKey,
158 + addressListViewModel: addressListViewModel,
159 + amountTextFieldFocusNode: _cryptoAmountFocus,
160 + amountController: _amountController,
161 + isLight: dashboardViewModel.settingsStore.currentTheme.type ==
162 + ThemeType.light))),
163 Observer(builder: (_) {
164 return addressListViewModel.hasAddressList
165 ? GestureDetector(
lib/src/screens/exchange/exchange_page.dart
+4 -4
@@ -115,10 +115,6 @@ class ExchangePage extends BasePage {
115 WidgetsBinding.instance
116 .addPostFrameCallback((_) => _setReactions(context, exchangeViewModel));
117
118 - if (exchangeViewModel.isLowFee) {
119 - _showFeeAlert(context);
120 - }
121 -
118 return KeyboardActions(
119 disableScroll: true,
120 config: KeyboardActionsConfig(
@@ -319,6 +315,10 @@ class ExchangePage extends BasePage {
315 return;
316 }
317
318 + if (exchangeViewModel.isLowFee) {
319 + _showFeeAlert(context);
320 + }
321 +
322 final depositAddressController = depositKey.currentState!.addressController;
323 final depositAmountController = depositKey.currentState!.amountController;
324 final receiveAddressController = receiveKey.currentState!.addressController;
lib/src/screens/receive/anonpay_receive_page.dart
+4 -4
@@ -1,5 +1,6 @@
1 import 'package:cake_wallet/anonpay/anonpay_info_base.dart';
2 import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart';
3 +import 'package:cake_wallet/entities/qr_view_data.dart';
4 import 'package:cake_wallet/entities/receive_page_option.dart';
5 import 'package:cake_wallet/generated/i18n.dart';
6 import 'package:cake_wallet/routes.dart';
@@ -133,10 +134,9 @@ class AnonPayReceivePage extends BasePage {
134 await Navigator.pushNamed(
135 context,
136 Routes.fullscreenQR,
136 - arguments: {
137 - 'qrData': invoiceInfo.clearnetUrl,
138 - 'version': qr.QrVersions.auto,
139 - },
137 + arguments: QrViewData(data: invoiceInfo.clearnetUrl,
138 + version: qr.QrVersions.auto,
139 + )
140 );
141 // ignore: unawaited_futures
142 DeviceDisplayBrightness.setBrightness(brightness);
lib/src/screens/receive/fullscreen_qr_page.dart
+5 -5
@@ -1,13 +1,13 @@
1 +import 'package:cake_wallet/entities/qr_view_data.dart';
2 import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart';
3 import 'package:cake_wallet/themes/theme_base.dart';
4 import 'package:flutter/material.dart';
5 import 'package:cake_wallet/src/screens/base_page.dart';
6
7 class FullscreenQRPage extends BasePage {
7 - FullscreenQRPage({required this.qrData, int? this.version});
8 + FullscreenQRPage({required this.qrViewData});
9
9 - final String qrData;
10 - final int? version;
10 + final QrViewData qrViewData;
11
12 @override
13 Color get backgroundLightColor => currentTheme.type == ThemeType.bright ? Colors.transparent : Colors.white;
@@ -63,7 +63,7 @@ class FullscreenQRPage extends BasePage {
63 return Padding(
64 padding: EdgeInsets.symmetric(horizontal: MediaQuery.of(context).size.width * 0.05),
65 child: Hero(
66 - tag: Key(qrData),
66 + tag: Key(qrViewData.heroTag ?? qrViewData.data),
67 child: Center(
68 child: AspectRatio(
69 aspectRatio: 1.0,
@@ -71,7 +71,7 @@ class FullscreenQRPage extends BasePage {
71 padding: EdgeInsets.all(10),
72 decoration: BoxDecoration(
73 border: Border.all(width: 3, color: Theme.of(context).accentTextTheme!.headline2!.backgroundColor!)),
74 - child: QrImage(data: qrData, version: version),
74 + child: QrImage(data: qrViewData.data, version: qrViewData.version),
75 ),
76 ),
77 ),
lib/src/screens/receive/receive_page.dart
+151 -148
@@ -21,16 +21,28 @@ import 'package:cake_wallet/src/screens/receive/widgets/qr_widget.dart';
21 import 'package:keyboard_actions/keyboard_actions.dart';
22
23 class ReceivePage extends BasePage {
24 - ReceivePage({required this.addressListViewModel}) : _cryptoAmountFocus = FocusNode();
24 + ReceivePage({required this.addressListViewModel})
25 + : _cryptoAmountFocus = FocusNode(),
26 + _amountController = TextEditingController(),
27 + _formKey = GlobalKey<FormState>() {
28 + _amountController.addListener(() {
29 + if (_formKey.currentState!.validate()) {
30 + addressListViewModel.changeAmount(_amountController.text);
31 + }
32 + });
33 + }
34
35 final WalletAddressListViewModel addressListViewModel;
36 + final TextEditingController _amountController;
37 + final GlobalKey<FormState> _formKey;
38 + static const _heroTag = 'receive_page';
39
40 @override
41 String get title => S.current.receive;
42
43 @override
32 - Color get backgroundLightColor => currentTheme.type == ThemeType.bright
33 - ? Colors.transparent : Colors.white;
44 + Color get backgroundLightColor =>
45 + currentTheme.type == ThemeType.bright ? Colors.transparent : Colors.white;
46
47 @override
48 Color get backgroundDarkColor => Colors.transparent;
@@ -68,162 +80,153 @@ class ReceivePage extends BasePage {
80
81 @override
82 Widget trailing(BuildContext context) {
71 - final shareImage =
72 - Image.asset('assets/images/share.png',
73 - color: Theme.of(context).accentTextTheme!.headline2!.backgroundColor!);
74 -
83 return Material(
84 color: Colors.transparent,
85 child: Semantics(
86 label: 'Share',
87 child: IconButton(
80 - padding: EdgeInsets.zero,
81 - constraints: BoxConstraints(),
82 - highlightColor: Colors.transparent,
83 - splashColor: Colors.transparent,
84 - iconSize: 25,
85 - onPressed: () {
86 - ShareUtil.share(
87 - text: addressListViewModel.address.address,
88 - context: context,
89 - );
90 - },
91 - icon: shareImage
88 + padding: EdgeInsets.zero,
89 + constraints: BoxConstraints(),
90 + highlightColor: Colors.transparent,
91 + splashColor: Colors.transparent,
92 + iconSize: 25,
93 + onPressed: () {
94 + ShareUtil.share(
95 + text: addressListViewModel.uri.toString(),
96 + context: context,
97 + );
98 + },
99 + icon: Icon(
100 + Icons.share,
101 + size: 20,
102 + color: Theme.of(context).accentTextTheme.headline2!.backgroundColor!,
103 + ),
104 ),
93 - )
94 - );
105 + ));
106 }
107
108 @override
109 Widget body(BuildContext context) {
99 - return (addressListViewModel.type == WalletType.monero || addressListViewModel.type == WalletType.haven)
110 + return (addressListViewModel.type == WalletType.monero ||
111 + addressListViewModel.type == WalletType.haven)
112 ? KeyboardActions(
101 - config: KeyboardActionsConfig(
102 - keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
103 - keyboardBarColor: Theme.of(context).accentTextTheme!.bodyText1!
104 - .backgroundColor!,
105 - nextFocus: false,
106 - actions: [
107 - KeyboardActionsItem(
108 - focusNode: _cryptoAmountFocus,
109 - toolbarButtons: [(_) => KeyboardDoneButton()],
110 - )
111 - ]),
112 - child: SingleChildScrollView(
113 - child: Column(
114 - children: <Widget>[
115 - Padding(
116 - padding: EdgeInsets.fromLTRB(24, 80, 24, 24),
117 - child: QRWidget(
118 - addressListViewModel: addressListViewModel,
119 - isAmountFieldShow: true,
120 - amountTextFieldFocusNode: _cryptoAmountFocus,
121 - isLight: currentTheme.type == ThemeType.light),
113 + config: KeyboardActionsConfig(
114 + keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
115 + keyboardBarColor: Theme.of(context).accentTextTheme!.bodyText1!.backgroundColor!,
116 + nextFocus: false,
117 + actions: [
118 + KeyboardActionsItem(
119 + focusNode: _cryptoAmountFocus,
120 + toolbarButtons: [(_) => KeyboardDoneButton()],
121 + )
122 + ]),
123 + child: SingleChildScrollView(
124 + child: Column(
125 + children: <Widget>[
126 + Padding(
127 + padding: EdgeInsets.fromLTRB(24, 50, 24, 24),
128 + child: QRWidget(
129 + addressListViewModel: addressListViewModel,
130 + formKey: _formKey,
131 + heroTag: _heroTag,
132 + amountTextFieldFocusNode: _cryptoAmountFocus,
133 + amountController: _amountController,
134 + isLight: currentTheme.type == ThemeType.light),
135 + ),
136 + Observer(
137 + builder: (_) => ListView.separated(
138 + padding: EdgeInsets.all(0),
139 + separatorBuilder: (context, _) => const SectionDivider(),
140 + shrinkWrap: true,
141 + physics: NeverScrollableScrollPhysics(),
142 + itemCount: addressListViewModel.items.length,
143 + itemBuilder: (context, index) {
144 + final item = addressListViewModel.items[index];
145 + Widget cell = Container();
146 +
147 + if (item is WalletAccountListHeader) {
148 + cell = HeaderTile(
149 + onTap: () async => await showPopUp<void>(
150 + context: context,
151 + builder: (_) => getIt.get<MoneroAccountListPage>()),
152 + title: S.of(context).accounts,
153 + icon: Icon(
154 + Icons.arrow_forward_ios,
155 + size: 14,
156 + color: Theme.of(context).textTheme!.headline4!.color!,
157 + ));
158 + }
159 +
160 + if (item is WalletAddressListHeader) {
161 + cell = HeaderTile(
162 + onTap: () =>
163 + Navigator.of(context).pushNamed(Routes.newSubaddress),
164 + title: S.of(context).addresses,
165 + icon: Icon(
166 + Icons.add,
167 + size: 20,
168 + color: Theme.of(context).textTheme!.headline4!.color!,
169 + ));
170 + }
171 +
172 + if (item is WalletAddressListItem) {
173 + cell = Observer(builder: (_) {
174 + final isCurrent =
175 + item.address == addressListViewModel.address.address;
176 + final backgroundColor = isCurrent
177 + ? Theme.of(context).textTheme!.headline2!.decorationColor!
178 + : Theme.of(context).textTheme!.headline3!.decorationColor!;
179 + final textColor = isCurrent
180 + ? Theme.of(context).textTheme!.headline2!.color!
181 + : Theme.of(context).textTheme!.headline3!.color!;
182 +
183 + return AddressCell.fromItem(item,
184 + isCurrent: isCurrent,
185 + backgroundColor: backgroundColor,
186 + textColor: textColor,
187 + onTap: (_) => addressListViewModel.setAddress(item),
188 + onEdit: () => Navigator.of(context)
189 + .pushNamed(Routes.newSubaddress, arguments: item));
190 + });
191 + }
192 +
193 + return index != 0
194 + ? cell
195 + : ClipRRect(
196 + borderRadius: BorderRadius.only(
197 + topLeft: Radius.circular(30),
198 + topRight: Radius.circular(30)),
199 + child: cell,
200 + );
201 + })),
202 + ],
203 ),
123 - Observer(
124 - builder: (_) => ListView.separated(
125 - padding: EdgeInsets.all(0),
126 - separatorBuilder: (context, _) => const SectionDivider(),
127 - shrinkWrap: true,
128 - physics: NeverScrollableScrollPhysics(),
129 - itemCount: addressListViewModel.items.length,
130 - itemBuilder: (context, index) {
131 - final item = addressListViewModel.items[index];
132 - Widget cell = Container();
133 -
134 - if (item is WalletAccountListHeader) {
135 - cell = HeaderTile(
136 - onTap: () async => await showPopUp<void>(
137 - context: context,
138 - builder: (_) =>
139 - getIt.get<MoneroAccountListPage>()),
140 - title: S.of(context).accounts,
141 - icon: Icon(
142 - Icons.arrow_forward_ios,
143 - size: 14,
144 - color:
145 - Theme.of(context).textTheme!.headline4!.color!,
146 - ));
147 - }
148 -
149 - if (item is WalletAddressListHeader) {
150 - cell = HeaderTile(
151 - onTap: () => Navigator.of(context)
152 - .pushNamed(Routes.newSubaddress),
153 - title: S.of(context).addresses,
154 - icon: Icon(
155 - Icons.add,
156 - size: 20,
157 - color:
158 - Theme.of(context).textTheme!.headline4!.color!,
159 - ));
160 - }
161 -
162 - if (item is WalletAddressListItem) {
163 - cell = Observer(builder: (_) {
164 - final isCurrent = item.address ==
165 - addressListViewModel.address.address;
166 - final backgroundColor = isCurrent
167 - ? Theme.of(context)
168 - .textTheme!
169 - .headline2!
170 - .decorationColor!
171 - : Theme.of(context)
172 - .textTheme!
173 - .headline3!
174 - .decorationColor!;
175 - final textColor = isCurrent
176 - ? Theme.of(context).textTheme!.headline2!.color!
177 - : Theme.of(context).textTheme!.headline3!.color!;
178 -
179 - return AddressCell.fromItem(item,
180 - isCurrent: isCurrent,
181 - backgroundColor: backgroundColor,
182 - textColor: textColor,
183 - onTap: (_) => addressListViewModel.setAddress(item),
184 - onEdit: () => Navigator.of(context).pushNamed(
185 - Routes.newSubaddress,
186 - arguments: item));
187 - });
188 - }
189 -
190 - return index != 0
191 - ? cell
192 - : ClipRRect(
193 - borderRadius: BorderRadius.only(
194 - topLeft: Radius.circular(30),
195 - topRight: Radius.circular(30)),
196 - child: cell,
197 - );
198 - })),
199 - ],
200 - ),
201 - )) : Padding(
202 - padding: EdgeInsets.fromLTRB(24, 24, 24, 32),
203 - child: Column(
204 - children: [
205 - Expanded(
206 - flex: 7,
207 - child: QRWidget(
208 - addressListViewModel: addressListViewModel,
209 - isAmountFieldShow: true,
210 - amountTextFieldFocusNode: _cryptoAmountFocus,
211 - isLight: currentTheme.type == ThemeType.light),
212 - ),
213 - Expanded(
214 - flex: 2,
215 - child: SizedBox(),
216 - ),
217 - Text(S.of(context).electrum_address_disclaimer,
218 - textAlign: TextAlign.center,
219 - style: TextStyle(
220 - fontSize: 15,
221 - color: Theme.of(context)
222 - .accentTextTheme!
223 - .headline3!
224 - .backgroundColor!)),
225 - ],
226 - ),
227 - );
204 + ))
205 + : Padding(
206 + padding: EdgeInsets.fromLTRB(24, 24, 24, 32),
207 + child: Column(
208 + children: [
209 + Expanded(
210 + flex: 7,
211 + child: QRWidget(
212 + formKey: _formKey,
213 + heroTag: _heroTag,
214 + addressListViewModel: addressListViewModel,
215 + amountTextFieldFocusNode: _cryptoAmountFocus,
216 + amountController: _amountController,
217 + isLight: currentTheme.type == ThemeType.light),
218 + ),
219 + Expanded(
220 + flex: 2,
221 + child: SizedBox(),
222 + ),
223 + Text(S.of(context).electrum_address_disclaimer,
224 + textAlign: TextAlign.center,
225 + style: TextStyle(
226 + fontSize: 15,
227 + color: Theme.of(context).accentTextTheme!.headline3!.backgroundColor!)),
228 + ],
229 + ),
230 + );
231 }
232 }
lib/src/screens/receive/widgets/currency_input_field.dart new
+120
@@ -0,0 +1,120 @@
1 +import 'package:cake_wallet/core/amount_validator.dart';
2 +import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
3 +import 'package:cw_core/currency.dart';
4 +import 'package:flutter/material.dart';
5 +import 'package:flutter/services.dart';
6 +
7 +class CurrencyInputField extends StatelessWidget {
8 + const CurrencyInputField({
9 + super.key,
10 + required this.onTapPicker,
11 + required this.selectedCurrency,
12 + this.focusNode,
13 + required this.controller,
14 + });
15 + final Function() onTapPicker;
16 + final Currency selectedCurrency;
17 + final FocusNode? focusNode;
18 + final TextEditingController controller;
19 +
20 + @override
21 + Widget build(BuildContext context) {
22 + final arrowBottomPurple = Image.asset(
23 + 'assets/images/arrow_bottom_purple_icon.png',
24 + color: Colors.white,
25 + height: 8,
26 + );
27 + final _width = MediaQuery.of(context).size.width;
28 +
29 + return Column(
30 + children: [
31 + Padding(
32 + padding: EdgeInsets.only(top: 20),
33 + child: SizedBox(
34 + height: 40,
35 + child: BaseTextFormField(
36 + focusNode: focusNode,
37 + controller: controller,
38 + keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true),
39 + inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d+(\.|\,)?\d{0,8}'))],
40 + hintText: '0.000',
41 + placeholderTextStyle: TextStyle(
42 + color: Theme.of(context).primaryTextTheme.headline5!.color!,
43 + fontWeight: FontWeight.w600,
44 + ),
45 + borderColor: Theme.of(context).accentTextTheme.headline6!.backgroundColor!,
46 + textColor: Colors.white,
47 + textStyle: TextStyle(
48 + color: Colors.white,
49 + ),
50 + prefixIcon: Padding(
51 + padding: EdgeInsets.only(
52 + left: _width / 4,
53 + ),
54 + child: Container(
55 + padding: EdgeInsets.only(right: 8),
56 + child: InkWell(
57 + onTap: onTapPicker,
58 + child: Row(
59 + mainAxisAlignment: MainAxisAlignment.spaceBetween,
60 + mainAxisSize: MainAxisSize.min,
61 + children: <Widget>[
62 + Padding(
63 + padding: EdgeInsets.only(right: 5),
64 + child: arrowBottomPurple,
65 + ),
66 + Text(
67 + selectedCurrency.name.toUpperCase(),
68 + style: TextStyle(
69 + fontWeight: FontWeight.w600,
70 + fontSize: 16,
71 + color: Colors.white,
72 + ),
73 + ),
74 + if (selectedCurrency.tag != null)
75 + Padding(
76 + padding: const EdgeInsets.only(right: 3.0),
77 + child: Container(
78 + decoration: BoxDecoration(
79 + color: Theme.of(context).primaryTextTheme.headline4!.color!,
80 + borderRadius: BorderRadius.all(
81 + Radius.circular(6),
82 + ),
83 + ),
84 + child: Center(
85 + child: Text(
86 + selectedCurrency.tag!,
87 + style: TextStyle(
88 + fontSize: 12,
89 + fontWeight: FontWeight.bold,
90 + color: Theme.of(context)
91 + .primaryTextTheme
92 + .headline4!
93 + .decorationColor!,
94 + ),
95 + ),
96 + ),
97 + ),
98 + ),
99 + Padding(
100 + padding: const EdgeInsets.only(bottom: 3.0),
101 + child: Text(
102 + ':',
103 + style: TextStyle(
104 + fontWeight: FontWeight.w600,
105 + fontSize: 20,
106 + color: Colors.white,
107 + ),
108 + ),
109 + ),
110 + ]),
111 + ),
112 + ),
113 + ),
114 + ),
115 + ),
116 + ),
117 + ],
118 + );
119 + }
120 +}
lib/src/screens/receive/widgets/qr_widget.dart
+79 -79
@@ -1,37 +1,36 @@
1 +import 'package:cake_wallet/entities/qr_view_data.dart';
2 import 'package:cake_wallet/routes.dart';
3 +import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
4 +import 'package:cake_wallet/src/screens/receive/widgets/currency_input_field.dart';
5 import 'package:cake_wallet/utils/device_info.dart';
6 import 'package:cake_wallet/utils/show_bar.dart';
4 -import 'package:cw_core/wallet_type.dart';
7 +import 'package:cake_wallet/utils/show_pop_up.dart';
8 import 'package:device_display_brightness/device_display_brightness.dart';
9 import 'package:flutter/material.dart';
10 import 'package:flutter/services.dart';
11 import 'package:flutter_mobx/flutter_mobx.dart';
12 import 'package:cake_wallet/generated/i18n.dart';
13 import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart';
11 -import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
12 -import 'package:cake_wallet/core/amount_validator.dart';
14 import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
15
16 class QRWidget extends StatelessWidget {
16 - QRWidget(
17 - {required this.addressListViewModel,
18 - required this.isLight,
19 - this.qrVersion,
20 - this.isAmountFieldShow = false,
21 - this.amountTextFieldFocusNode})
22 - : amountController = TextEditingController(),
23 - _formKey = GlobalKey<FormState>() {
24 - amountController.addListener(() => addressListViewModel?.amount =
25 - _formKey.currentState!.validate() ? amountController.text : '');
26 - }
17 + QRWidget({
18 + required this.addressListViewModel,
19 + required this.isLight,
20 + this.qrVersion,
21 + this.heroTag,
22 + required this.amountController,
23 + required this.formKey,
24 + this.amountTextFieldFocusNode,
25 + });
26
27 final WalletAddressListViewModel addressListViewModel;
29 - final bool isAmountFieldShow;
28 final TextEditingController amountController;
29 final FocusNode? amountTextFieldFocusNode;
32 - final GlobalKey<FormState> _formKey;
30 + final GlobalKey<FormState> formKey;
31 final bool isLight;
32 final int? qrVersion;
33 + final String? heroTag;
34
35 @override
36 Widget build(BuildContext context) {
@@ -40,7 +39,7 @@ class QRWidget extends StatelessWidget {
39
40 return Column(
41 mainAxisSize: MainAxisSize.min,
43 - mainAxisAlignment: MainAxisAlignment.spaceEvenly,
42 + mainAxisAlignment: MainAxisAlignment.center,
43 crossAxisAlignment: CrossAxisAlignment.center,
44 children: <Widget>[
45 Column(
@@ -63,18 +62,18 @@ class QRWidget extends StatelessWidget {
62 flex: 5,
63 child: GestureDetector(
64 onTap: () {
66 - changeBrightnessForRoute(() async {
67 - await Navigator.pushNamed(
68 - context,
69 - Routes.fullscreenQR,
70 - arguments: {
71 - 'qrData': addressListViewModel.uri.toString(),
72 - },
73 - );
74 - });
65 + changeBrightnessForRoute(
66 + () async {
67 + await Navigator.pushNamed(context, Routes.fullscreenQR,
68 + arguments: QrViewData(
69 + data: addressListViewModel.uri.toString(),
70 + heroTag: heroTag,
71 + ));
72 + },
73 + );
74 },
75 child: Hero(
77 - tag: Key(addressListViewModel.uri.toString()),
76 + tag: Key(heroTag ?? addressListViewModel.uri.toString()),
77 child: Center(
78 child: AspectRatio(
79 aspectRatio: 1.0,
@@ -83,7 +82,8 @@ class QRWidget extends StatelessWidget {
82 decoration: BoxDecoration(
83 border: Border.all(
84 width: 3,
86 - color: Theme.of(context).accentTextTheme.headline2!.backgroundColor!,
85 + color:
86 + Theme.of(context).accentTextTheme.headline2!.backgroundColor!,
87 ),
88 ),
89 child: QrImage(data: addressListViewModel.uri.toString()),
@@ -99,77 +99,77 @@ class QRWidget extends StatelessWidget {
99 ),
100 ],
101 ),
102 - if (isAmountFieldShow)
103 - Padding(
102 + Observer(builder: (_) {
103 + return Padding(
104 padding: EdgeInsets.only(top: 10),
105 child: Row(
106 children: <Widget>[
107 Expanded(
108 child: Form(
109 - key: _formKey,
110 - child: BaseTextFormField(
109 + key: formKey,
110 + child: CurrencyInputField(
111 focusNode: amountTextFieldFocusNode,
112 controller: amountController,
113 - keyboardType: TextInputType.numberWithOptions(decimal: true),
114 - inputFormatters: [FilteringTextInputFormatter.deny(RegExp('[\\-|\\ ]'))],
115 - textAlign: TextAlign.center,
116 - hintText: S.of(context).receive_amount,
117 - textColor: Theme.of(context).accentTextTheme!.headline2!.backgroundColor!,
118 - borderColor: Theme.of(context).textTheme!.headline5!.decorationColor!,
119 - validator: AmountValidator(
120 - currency: walletTypeToCryptoCurrency(addressListViewModel!.type),
121 - isAutovalidate: true),
122 - // FIX-ME: Check does it equal to autovalidate: true,
123 - autovalidateMode: AutovalidateMode.always,
124 - placeholderTextStyle: TextStyle(
125 - color: Theme.of(context).hoverColor,
126 - fontSize: 18,
127 - fontWeight: FontWeight.w500,
128 - ),
113 + onTapPicker: () => _presentPicker(context),
114 + selectedCurrency: addressListViewModel.selectedCurrency,
115 ),
116 ),
117 ),
118 ],
119 ),
134 - ),
135 - Padding(
136 - padding: EdgeInsets.only(top: 8, bottom: 8),
137 - child: Builder(
138 - builder: (context) => Observer(
139 - builder: (context) => GestureDetector(
140 - onTap: () {
141 - Clipboard.setData(ClipboardData(text: addressListViewModel!.address.address));
142 - showBar<void>(context, S.of(context).copied_to_clipboard);
143 - },
144 - child: Row(
145 - mainAxisSize: MainAxisSize.max,
146 - crossAxisAlignment: CrossAxisAlignment.start,
147 - children: <Widget>[
148 - Expanded(
149 - child: Text(
150 - addressListViewModel!.address.address,
151 - textAlign: TextAlign.center,
152 - style: TextStyle(
153 - fontSize: 15,
154 - fontWeight: FontWeight.w500,
155 - color:
156 - Theme.of(context).accentTextTheme!.headline2!.backgroundColor!),
157 - ),
120 + );
121 + }),
122 + Padding(
123 + padding: EdgeInsets.only(top: 20, bottom: 8),
124 + child: Builder(
125 + builder: (context) => Observer(
126 + builder: (context) => GestureDetector(
127 + onTap: () {
128 + Clipboard.setData(ClipboardData(text: addressListViewModel.address.address));
129 + showBar<void>(context, S.of(context).copied_to_clipboard);
130 + },
131 + child: Row(
132 + mainAxisSize: MainAxisSize.max,
133 + crossAxisAlignment: CrossAxisAlignment.start,
134 + children: <Widget>[
135 + Expanded(
136 + child: Text(
137 + addressListViewModel.address.address,
138 + textAlign: TextAlign.center,
139 + style: TextStyle(
140 + fontSize: 15,
141 + fontWeight: FontWeight.w500,
142 + color: Theme.of(context).accentTextTheme.headline2!.backgroundColor!),
143 ),
159 - Padding(
160 - padding: EdgeInsets.only(left: 12),
161 - child: copyImage,
162 - )
163 - ],
164 - ),
144 + ),
145 + Padding(
146 + padding: EdgeInsets.only(left: 12),
147 + child: copyImage,
148 + )
149 + ],
150 ),
151 ),
152 ),
168 - )
153 + ),
154 + )
155 ],
156 );
157 }
158
159 + void _presentPicker(BuildContext context) async {
160 + await showPopUp<void>(
161 + builder: (_) => CurrencyPicker(
162 + selectedAtIndex: addressListViewModel.selectedCurrencyIndex,
163 + items: addressListViewModel.currencies,
164 + hintText: S.of(context).search_currency,
165 + onItemSelected: addressListViewModel.selectCurrency,
166 + ),
167 + context: context,
168 + );
169 + // update amount if currency changed
170 + addressListViewModel.changeAmount(amountController.text);
171 + }
172 +
173 Future<void> changeBrightnessForRoute(Future<void> Function() navigation) async {
174 // if not mobile, just navigate
175 if (!DeviceInfo.instance.isMobile) {
lib/src/screens/wallet_keys/wallet_keys_page.dart
+2 -3
@@ -1,4 +1,5 @@
1 import 'package:auto_size_text/auto_size_text.dart';
2 +import 'package:cake_wallet/entities/qr_view_data.dart';
3 import 'package:cake_wallet/src/widgets/section_divider.dart';
4 import 'package:cake_wallet/utils/show_bar.dart';
5 import 'package:device_display_brightness/device_display_brightness.dart';
@@ -31,9 +32,7 @@ class WalletKeysPage extends BasePage {
32 await Navigator.pushNamed(
33 context,
34 Routes.fullscreenQR,
34 - arguments: {
35 - 'qrData': (await walletKeysViewModel.url).toString(),
36 - },
35 + arguments: QrViewData(data: await walletKeysViewModel.url.toString()),
36 );
37 // ignore: unawaited_futures
38 DeviceDisplayBrightness.setBrightness(brightness);
lib/view_model/wallet_address_list/wallet_address_list_view_model.dart
+72 -55
@@ -1,5 +1,8 @@
1 +import 'package:cake_wallet/entities/fiat_currency.dart';
2 +import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
3 import 'package:cake_wallet/store/yat/yat_store.dart';
2 -import 'package:flutter/foundation.dart';
4 +import 'package:cw_core/currency.dart';
5 +import 'package:intl/intl.dart';
6 import 'package:mobx/mobx.dart';
7 import 'package:cw_core/wallet_base.dart';
8 import 'package:cake_wallet/utils/list_item.dart';
@@ -11,37 +14,30 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart';
14 import 'package:cw_core/transaction_history.dart';
15 import 'package:cw_core/balance.dart';
16 import 'package:cw_core/transaction_info.dart';
14 -import 'package:cw_core/wallet_type.dart';
17 import 'package:cake_wallet/store/app_store.dart';
16 -import 'dart:async';
18 import 'package:cake_wallet/monero/monero.dart';
19 import 'package:cake_wallet/haven/haven.dart';
20
21 part 'wallet_address_list_view_model.g.dart';
22
22 -class WalletAddressListViewModel = WalletAddressListViewModelBase
23 - with _$WalletAddressListViewModel;
23 +class WalletAddressListViewModel = WalletAddressListViewModelBase with _$WalletAddressListViewModel;
24
25 abstract class PaymentURI {
26 - PaymentURI({
27 - required this.amount,
28 - required this.address});
26 + PaymentURI({required this.amount, required this.address});
27
28 final String amount;
29 final String address;
30 }
31
32 class MoneroURI extends PaymentURI {
35 - MoneroURI({
36 - required String amount,
37 - required String address})
33 + MoneroURI({required String amount, required String address})
34 : super(amount: amount, address: address);
35
36 @override
37 String toString() {
38 var base = 'monero:' + address;
39
44 - if (amount?.isNotEmpty ?? false) {
40 + if (amount.isNotEmpty) {
41 base += '?tx_amount=${amount.replaceAll(',', '.')}';
42 }
43
@@ -50,16 +46,14 @@ class MoneroURI extends PaymentURI {
46 }
47
48 class HavenURI extends PaymentURI {
53 - HavenURI({
54 - required String amount,
55 - required String address})
49 + HavenURI({required String amount, required String address})
50 : super(amount: amount, address: address);
51
52 @override
53 String toString() {
54 var base = 'haven:' + address;
55
62 - if (amount?.isNotEmpty ?? false) {
56 + if (amount.isNotEmpty) {
57 base += '?tx_amount=${amount.replaceAll(',', '.')}';
58 }
59
@@ -68,16 +62,14 @@ class HavenURI extends PaymentURI {
62 }
63
64 class BitcoinURI extends PaymentURI {
71 - BitcoinURI({
72 - required String amount,
73 - required String address})
65 + BitcoinURI({required String amount, required String address})
66 : super(amount: amount, address: address);
67
68 @override
69 String toString() {
70 var base = 'bitcoin:' + address;
71
80 - if (amount?.isNotEmpty ?? false) {
72 + if (amount.isNotEmpty) {
73 base += '?amount=${amount.replaceAll(',', '.')}';
74 }
75
@@ -86,16 +78,14 @@ class BitcoinURI extends PaymentURI {
78 }
79
80 class LitecoinURI extends PaymentURI {
89 - LitecoinURI({
90 - required String amount,
91 - required String address})
81 + LitecoinURI({required String amount, required String address})
82 : super(amount: amount, address: address);
83
84 @override
85 String toString() {
86 var base = 'litecoin:' + address;
87
98 - if (amount?.isNotEmpty ?? false) {
88 + if (amount.isNotEmpty) {
89 base += '?amount=${amount.replaceAll(',', '.')}';
90 }
91
@@ -106,24 +96,33 @@ class LitecoinURI extends PaymentURI {
96 abstract class WalletAddressListViewModelBase with Store {
97 WalletAddressListViewModelBase({
98 required AppStore appStore,
109 - required this.yatStore
110 - }) : _appStore = appStore,
111 - _baseItems = <ListItem>[],
112 - _wallet = appStore.wallet!,
113 - hasAccounts = appStore.wallet!.type == WalletType.monero || appStore.wallet!.type == WalletType.haven,
114 - amount = '' {
115 - _onWalletChangeReaction = reaction((_) => _appStore.wallet, (WalletBase<
116 - Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>?
117 - wallet) {
118 - if (wallet == null) {
119 - return;
120 - }
121 - _wallet = wallet;
122 - hasAccounts = _wallet.type == WalletType.monero;
123 - });
99 + required this.yatStore,
100 + required this.fiatConversionStore,
101 + }) : _appStore = appStore,
102 + _baseItems = <ListItem>[],
103 + _wallet = appStore.wallet!,
104 + selectedCurrency = walletTypeToCryptoCurrency(appStore.wallet!.type),
105 + _cryptoNumberFormat = NumberFormat(_cryptoNumberPattern),
106 + hasAccounts =
107 + appStore.wallet!.type == WalletType.monero || appStore.wallet!.type == WalletType.haven,
108 + amount = '' {
109 _init();
110 }
111
112 + static const String _cryptoNumberPattern = '0.00000000';
113 +
114 + final NumberFormat _cryptoNumberFormat;
115 +
116 + final FiatConversionStore fiatConversionStore;
117 +
118 + List<Currency> get currencies => [walletTypeToCryptoCurrency(_wallet.type), ...FiatCurrency.all];
119 +
120 + @observable
121 + Currency selectedCurrency;
122 +
123 + @computed
124 + int get selectedCurrencyIndex => currencies.indexOf(selectedCurrency);
125 +
126 @observable
127 String amount;
128
@@ -156,8 +155,9 @@ abstract class WalletAddressListViewModelBase with Store {
155 }
156
157 @computed
159 - ObservableList<ListItem> get items =>
160 - ObservableList<ListItem>()..addAll(_baseItems)..addAll(addressList);
158 + ObservableList<ListItem> get items => ObservableList<ListItem>()
159 + ..addAll(_baseItems)
160 + ..addAll(addressList);
161
162 @computed
163 ObservableList<ListItem> get addressList {
@@ -166,10 +166,7 @@ abstract class WalletAddressListViewModelBase with Store {
166
167 if (wallet.type == WalletType.monero) {
168 final primaryAddress = monero!.getSubaddressList(wallet).subaddresses.first;
169 - final addressItems = monero
170 - !.getSubaddressList(wallet)
171 - .subaddresses
172 - .map((subaddress) {
169 + final addressItems = monero!.getSubaddressList(wallet).subaddresses.map((subaddress) {
170 final isPrimary = subaddress == primaryAddress;
171
172 return WalletAddressListItem(
@@ -183,10 +180,7 @@ abstract class WalletAddressListViewModelBase with Store {
180
181 if (wallet.type == WalletType.haven) {
182 final primaryAddress = haven!.getSubaddressList(wallet).subaddresses.first;
186 - final addressItems = haven
187 - !.getSubaddressList(wallet)
188 - .subaddresses
189 - .map((subaddress) {
183 + final addressItems = haven!.getSubaddressList(wallet).subaddresses.map((subaddress) {
184 final isPrimary = subaddress == primaryAddress;
185
186 return WalletAddressListItem(
@@ -203,8 +197,7 @@ abstract class WalletAddressListViewModelBase with Store {
197 final bitcoinAddresses = bitcoin!.getAddresses(wallet).map((addr) {
198 final isPrimary = addr == primaryAddress;
199
206 - return WalletAddressListItem(
207 - isPrimary: isPrimary, name: null, address: addr);
200 + return WalletAddressListItem(isPrimary: isPrimary, name: null, address: addr);
201 });
202 addressList.addAll(bitcoinAddresses);
203 }
@@ -234,8 +227,7 @@ abstract class WalletAddressListViewModelBase with Store {
227 bool get hasAddressList => _wallet.type == WalletType.monero || _wallet.type == WalletType.haven;
228
229 @observable
237 - WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>
238 - _wallet;
230 + WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> _wallet;
231
232 List<ListItem> _baseItems;
233
@@ -243,8 +235,6 @@ abstract class WalletAddressListViewModelBase with Store {
235
236 final YatStore yatStore;
237
246 - ReactionDisposer? _onWalletChangeReaction;
247 -
238 @action
239 void setAddress(WalletAddressListItem address) =>
240 _wallet.walletAddresses.address = address.address;
@@ -258,4 +248,31 @@ abstract class WalletAddressListViewModelBase with Store {
248
249 _baseItems.add(WalletAddressListHeader());
250 }
251 +
252 + @action
253 + void selectCurrency(Currency currency) {
254 + selectedCurrency = currency;
255 + }
256 +
257 + @action
258 + void changeAmount(String amount) {
259 + this.amount = amount;
260 + if (selectedCurrency is FiatCurrency) {
261 + _convertAmountToCrypto();
262 + }
263 + }
264 +
265 + void _convertAmountToCrypto() {
266 + final cryptoCurrency = walletTypeToCryptoCurrency(_wallet.type);
267 + try {
268 + final crypto =
269 + double.parse(amount.replaceAll(',', '.')) / fiatConversionStore.prices[cryptoCurrency]!;
270 + final cryptoAmountTmp = _cryptoNumberFormat.format(crypto);
271 + if (amount != cryptoAmountTmp) {
272 + amount = cryptoAmountTmp;
273 + }
274 + } catch (e) {
275 + amount = '';
276 + }
277 + }
278 }