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
}