dev
dart 479 lines 22.2 KB
Raw
1 import 'package:cake_wallet/bitcoin/bitcoin.dart';
2 import 'package:cake_wallet/generated/i18n.dart';
3 import 'package:cake_wallet/routes.dart';
4 import 'package:cake_wallet/src/screens/dashboard/pages/balance/balance_row_widget.dart';
5 import 'package:cake_wallet/src/screens/dashboard/widgets/home_screen_account_widget.dart';
6 import 'package:cake_wallet/src/screens/dashboard/widgets/info_card.dart';
7 import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
8 import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
9 import 'package:cake_wallet/src/widgets/dashboard_card_widget.dart';
10 import 'package:cake_wallet/src/widgets/introducing_card.dart';
11 import 'package:cake_wallet/src/widgets/standard_switch.dart';
12 import 'package:cake_wallet/themes/core/theme_extension.dart';
13 import 'package:cake_wallet/utils/feature_flag.dart';
14 import 'package:cake_wallet/utils/show_pop_up.dart';
15 import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
16 import 'package:cw_core/wallet_info.dart';
17 import 'package:flutter/material.dart';
18 import 'package:flutter_mobx/flutter_mobx.dart';
19 import 'package:flutter_svg/flutter_svg.dart';
20 import 'package:url_launcher/url_launcher.dart';
21
22 class CryptoBalanceWidget extends StatelessWidget {
23 const CryptoBalanceWidget({
24 super.key,
25 required this.dashboardViewModel,
26 });
27
28 final DashboardViewModel dashboardViewModel;
29 final btcLockLight = 'assets/images/btc_lock_light.png';
30 final btcLockDark = 'assets/images/btc_lock_dark.png';
31
32 String? get hardwareWalletIcon {
33 switch (dashboardViewModel.wallet.hardwareWalletType) {
34 case null:
35 return null;
36 case HardwareWalletType.bitbox:
37 return "assets/new-ui/hardware_wallets/device_bitbox.svg";
38 case HardwareWalletType.ledger:
39 return "assets/new-ui/hardware_wallets/device_ledger_nano_x.svg";
40 case HardwareWalletType.trezor:
41 return "assets/new-ui/hardware_wallets/device_trezor_safe_5.svg";
42 case HardwareWalletType.cupcake:
43 return "assets/images/cupcake.svg";
44 case HardwareWalletType.coldcard:
45 case HardwareWalletType.seedsigner:
46 case HardwareWalletType.keystone:
47 return "assets/images/hardware_wallet/device_qr.svg";
48 }
49 }
50
51 @override
52 Widget build(BuildContext context) {
53 return SingleChildScrollView(
54 child: Column(
55 crossAxisAlignment: CrossAxisAlignment.center,
56 children: [
57 Observer(
58 builder: (_) {
59 if (dashboardViewModel.getMoneroError != null) {
60 return Padding(
61 padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
62 child: DashBoardRoundedCardWidget(
63 title: "Invalid monero bindings",
64 subTitle: dashboardViewModel.getMoneroError.toString(),
65 ),
66 );
67 }
68 return Container();
69 },
70 ),
71 Observer(
72 builder: (_) {
73 if (dashboardViewModel.getWowneroError != null) {
74 return Padding(
75 padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
76 child: DashBoardRoundedCardWidget(
77 title: "Invalid wownero bindings",
78 subTitle: dashboardViewModel.getWowneroError.toString(),
79 ),
80 );
81 }
82 return Container();
83 },
84 ),
85 Observer(
86 builder: (_) => dashboardViewModel.balanceViewModel.hasAccounts
87 ? HomeScreenAccountWidget(
88 walletName: dashboardViewModel.name, accountName: dashboardViewModel.subname)
89 : Column(
90 children: [
91 SizedBox(height: 16),
92 Container(
93 margin: const EdgeInsets.only(left: 24, bottom: 16),
94 child: Observer(
95 builder: (_) {
96 return Row(
97 children: [
98 Text(
99 dashboardViewModel.balanceViewModel.asset,
100 style: Theme.of(context).textTheme.headlineSmall?.copyWith(
101 fontWeight: FontWeight.w500,
102 color: Theme.of(context).colorScheme.onSurface,
103 height: 1,
104 ),
105 maxLines: 1,
106 textAlign: TextAlign.center,
107 ),
108 if (hardwareWalletIcon != null)
109 Container(
110 child: SvgPicture.asset(
111 hardwareWalletIcon!,
112 width: 24,
113 color: Theme.of(context).colorScheme.onSurface,
114 ),
115 ),
116 if (dashboardViewModel
117 .balanceViewModel.isHomeScreenSettingsEnabled)
118 TextButton(
119 style: TextButton.styleFrom(
120 minimumSize: Size(50, 30),
121 tapTargetSize: MaterialTapTargetSize.shrinkWrap,
122 alignment: Alignment.centerLeft),
123 onPressed: () => Navigator.pushNamed(
124 context,
125 Routes.homeSettings,
126 arguments: dashboardViewModel.balanceViewModel,
127 ),
128 child: Container(
129 child: SvgPicture.asset(
130 'assets/images/home_screen_setting_icon.svg',
131 color: Theme.of(context).colorScheme.onSurfaceVariant,
132 height: 30),
133 ),
134 ),
135 ],
136 );
137 },
138 ),
139 ),
140 ],
141 )),
142 Observer(
143 builder: (_) {
144 if (dashboardViewModel.balanceViewModel.isShowCard && FeatureFlag.isCakePayEnabled) {
145 return IntroducingCard(
146 title: S.of(context).introducing_cake_pay,
147 subTitle: S.of(context).cake_pay_learn_more,
148 borderColor: Theme.of(context).colorScheme.outline,
149 closeCard: dashboardViewModel.balanceViewModel.disableIntroCakePayCard);
150 }
151 return Container();
152 },
153 ),
154 Observer(builder: (_) {
155 if (!dashboardViewModel.showRepWarning) {
156 return const SizedBox();
157 }
158 return Padding(
159 padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
160 child: DashBoardRoundedCardWidget(
161 title: S.of(context).rep_warning,
162 subTitle: S.of(context).rep_warning_sub,
163 onTap: () => Navigator.of(context).pushNamed(Routes.changeRep),
164 onClose: () {
165 dashboardViewModel.settingsStore.shouldShowRepWarning = false;
166 },
167 ),
168 );
169 }),
170 Observer(
171 builder: (_) {
172 if (dashboardViewModel.balanceViewModel.formattedBalances.isEmpty) {
173 return Center(
174 child: Container(
175 child: Text(
176 'Loading balances...',
177 style: Theme.of(context).textTheme.bodyMedium?.copyWith(
178 color: Theme.of(context).colorScheme.onSurfaceVariant,
179 height: 1,
180 ),
181 textAlign: TextAlign.center,
182 ),
183 ),
184 );
185 }
186
187 return ListView.separated(
188 physics: NeverScrollableScrollPhysics(),
189 shrinkWrap: true,
190 separatorBuilder: (_, __) => Container(padding: EdgeInsets.only(bottom: 10)),
191 itemCount: dashboardViewModel.balanceViewModel.formattedBalances.length,
192 itemBuilder: (__, index) {
193 final balance =
194 dashboardViewModel.balanceViewModel.formattedBalances.elementAt(index);
195 return Observer(builder: (_) {
196 return BalanceRowWidget(
197 dashboardViewModel: dashboardViewModel,
198 availableBalanceLabel:
199 '${dashboardViewModel.balanceViewModel.availableBalanceLabel}',
200 availableBalance: balance.availableBalance,
201 availableFiatBalance: balance.fiatAvailableBalance,
202 additionalBalanceLabel:
203 '${dashboardViewModel.balanceViewModel.additionalBalanceLabel}',
204 additionalBalance: balance.additionalBalance,
205 additionalFiatBalance: balance.fiatAdditionalBalance,
206 frozenBalance: balance.frozenBalance,
207 frozenFiatBalance: balance.fiatFrozenBalance,
208 currency: balance.asset,
209 hasAdditionalBalance:
210 dashboardViewModel.balanceViewModel.hasAdditionalBalance(balance.asset),
211 hasSecondAdditionalBalance:
212 dashboardViewModel.balanceViewModel.hasSecondAdditionalBalance,
213 hasSecondAvailableBalance:
214 dashboardViewModel.balanceViewModel.hasSecondAvailableBalance,
215 secondAdditionalBalance: balance.secondAdditionalBalance,
216 secondAdditionalFiatBalance: balance.fiatSecondAdditionalBalance,
217 secondAvailableBalance: balance.secondAvailableBalance,
218 secondAvailableFiatBalance: balance.fiatSecondAvailableBalance,
219 secondAdditionalBalanceLabel:
220 '${dashboardViewModel.balanceViewModel.secondAdditionalBalanceLabel}',
221 secondAvailableBalanceLabel:
222 '${dashboardViewModel.balanceViewModel.secondAvailableBalanceLabel}',
223 isTestnet: dashboardViewModel.isTestnet,
224 );
225 });
226 },
227 );
228 },
229 ),
230 Observer(builder: (context) {
231 return Column(
232 children: [
233 if (dashboardViewModel.isMoneroWalletBrokenReasons.isNotEmpty) ...[
234 SizedBox(height: 10),
235 Padding(
236 padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
237 child: DashBoardRoundedCardWidget(
238 customBorder: 30,
239 title: "This wallet has encountered an issue",
240 subTitle: "Here are the things that you should note:\n - " +
241 dashboardViewModel.isMoneroWalletBrokenReasons.join("\n - ") +
242 "\n\nPlease restart your wallet and if it doesn't help contact our support.",
243 ))
244 ],
245 if (dashboardViewModel.showSilentPaymentsCard) ...[
246 SizedBox(height: 10),
247 Padding(
248 padding: const EdgeInsets.fromLTRB(16, 0, 16, 0),
249 child: DashBoardRoundedCardWidget(
250 shadowBlur: dashboardViewModel.getShadowBlur(),
251 shadowSpread: dashboardViewModel.getShadowSpread(),
252 marginV: 0,
253 marginH: 0,
254 customBorder: 30,
255 title: S.of(context).silent_payments,
256 subTitle: S.of(context).enable_silent_payments_scanning,
257 hint: Column(
258 children: [
259 Row(
260 mainAxisAlignment: MainAxisAlignment.spaceBetween,
261 children: [
262 GestureDetector(
263 behavior: HitTestBehavior.opaque,
264 onTap: () => launchUrl(
265 Uri.parse(
266 "https://docs.cakewallet.com/cryptos/bitcoin#silent-payments"),
267 mode: LaunchMode.externalApplication,
268 ),
269 child: Row(
270 children: [
271 Text(
272 S.of(context).what_is_silent_payments,
273 style: Theme.of(context).textTheme.bodySmall?.copyWith(
274 color: Theme.of(context).colorScheme.onSurfaceVariant,
275 height: 1,
276 ),
277 softWrap: true,
278 ),
279 Padding(
280 padding: const EdgeInsets.symmetric(horizontal: 4),
281 child: Icon(
282 Icons.help_outline,
283 size: 16,
284 color: Theme.of(context).colorScheme.onSurfaceVariant,
285 ),
286 )
287 ],
288 ),
289 ),
290 Observer(
291 builder: (_) => StandardSwitch(
292 value: dashboardViewModel.silentPaymentsScanningActive,
293 onTapped: () => _toggleSilentPaymentsScanning(context),
294 ),
295 )
296 ],
297 ),
298 ],
299 ),
300 onTap: () => _toggleSilentPaymentsScanning(context),
301 image: !context.currentTheme.isDark
302 ? Image.asset(btcLockLight, height: 48)
303 : Image.asset(btcLockDark, height: 48),
304 ),
305 ),
306 ],
307 if (dashboardViewModel.showMwebCard) ...[
308 SizedBox(height: 10),
309 Padding(
310 padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
311 child: InfoCard(
312 title: S.of(context).litecoin_mweb,
313 description: S.of(context).litecoin_mweb_description,
314 leftButtonTitle: S.of(context).litecoin_mweb_dismiss,
315 rightButtonTitle: S.of(context).enable,
316 image: 'assets/images/mweb_logo.png',
317 leftButtonAction: () => _dismissMweb(context),
318 rightButtonAction: () => _enableMweb(context),
319 hintWidget: GestureDetector(
320 behavior: HitTestBehavior.opaque,
321 onTap: () => launchUrl(
322 Uri.parse("https://docs.cakewallet.com/cryptos/litecoin/#mweb"),
323 mode: LaunchMode.externalApplication,
324 ),
325 child: Text(
326 S.of(context).learn_more,
327 style: Theme.of(context).textTheme.bodySmall?.copyWith(
328 color: Theme.of(context).colorScheme.onSurfaceVariant,
329 height: 1,
330 ),
331 softWrap: true,
332 ),
333 ),
334 ),
335 ),
336 ],
337 if (dashboardViewModel.showDecredInfoCard) ...[
338 SizedBox(height: 10),
339 Padding(
340 padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
341 child: InfoCard(
342 title: S.of(context).synchronizing,
343 description: S.of(context).decred_info_card_details,
344 image: 'assets/new-ui/crypto_full_icons/decred.svg',
345 leftButtonTitle: S.of(context).litecoin_mweb_dismiss,
346 rightButtonTitle: S.of(context).learn_more,
347 leftButtonAction: () => dashboardViewModel.dismissDecredInfoCard(),
348 rightButtonAction: () => launchUrl(
349 Uri.parse("https://docs.cakewallet.com/cryptos/decred/#spv-sync")),
350 ),
351 ),
352 ],
353 if (dashboardViewModel.showPayjoinCard) ...[
354 SizedBox(height: 10),
355 Padding(
356 padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
357 child: InfoCard(
358 title: "Payjoin",
359 description: S.of(context).payjoin_card_content,
360 hintWidget: GestureDetector(
361 behavior: HitTestBehavior.opaque,
362 onTap: () => launchUrl(
363 Uri.parse("https://docs.cakewallet.com/cryptos/bitcoin/#payjoin"),
364 mode: LaunchMode.externalApplication,
365 ),
366 child: Row(
367 children: [
368 Text(
369 S.of(context).what_is_payjoin,
370 style: Theme.of(context).textTheme.bodySmall?.copyWith(
371 color: Theme.of(context).colorScheme.onSurfaceVariant,
372 height: 1,
373 ),
374 softWrap: true,
375 ),
376 Padding(
377 padding: const EdgeInsets.symmetric(horizontal: 4),
378 child: Icon(
379 Icons.help_outline,
380 size: 16,
381 color: Theme.of(context).colorScheme.onSurfaceVariant,
382 ),
383 )
384 ],
385 ),
386 ),
387 image: 'assets/images/payjoin.png',
388 leftButtonTitle: S.of(context).litecoin_mweb_dismiss,
389 rightButtonTitle: S.of(context).enable,
390 leftButtonAction: () => dashboardViewModel.dismissPayjoin(),
391 rightButtonAction: () => _enablePayjoin(context),
392 ),
393 ),
394 ],
395 ],
396 );
397 }),
398 SizedBox(height: 130),
399 ],
400 ),
401 );
402 }
403
404 Future<void> _toggleSilentPaymentsScanning(BuildContext context) async {
405 final isSilentPaymentsScanningActive = dashboardViewModel.silentPaymentsScanningActive;
406 final newValue = !isSilentPaymentsScanningActive;
407
408 dashboardViewModel.silentPaymentsScanningActive = newValue;
409
410 final needsToSwitch = !isSilentPaymentsScanningActive &&
411 await bitcoin!.getNodeIsElectrsSPEnabled(dashboardViewModel.wallet) == false;
412
413 if (needsToSwitch) {
414 return showPopUp<void>(
415 context: context,
416 builder: (BuildContext context) => AlertWithTwoActions(
417 alertTitle: S.of(context).change_current_node_title,
418 alertContent: S.of(context).confirm_silent_payments_switch_node,
419 rightButtonText: S.of(context).confirm,
420 leftButtonText: S.of(context).cancel,
421 actionRightButton: () {
422 dashboardViewModel.setSilentPaymentsScanning(newValue);
423 Navigator.of(context).pop();
424 },
425 actionLeftButton: () {
426 dashboardViewModel.silentPaymentsScanningActive = isSilentPaymentsScanningActive;
427 Navigator.of(context).pop();
428 },
429 ));
430 }
431
432 return dashboardViewModel.setSilentPaymentsScanning(newValue);
433 }
434
435 void _enablePayjoin(BuildContext context) {
436 showPopUp<void>(
437 context: context,
438 builder: (BuildContext context) => AlertWithOneAction(
439 alertTitle: S.of(context).payjoin_enabling_popup_title,
440 alertContent: S.of(context).payjoin_enabling_popup_content,
441 buttonText: S.of(context).ok,
442 buttonAction: () {
443 Navigator.of(context).pop();
444 },
445 ));
446
447 dashboardViewModel.enablePayjoin();
448 }
449
450 Future<void> _enableMweb(BuildContext context) async {
451 if (!dashboardViewModel.hasEnabledMwebBefore) {
452 await showPopUp<void>(
453 context: context,
454 builder: (BuildContext context) => AlertWithOneAction(
455 alertTitle: S.of(context).alert_notice,
456 alertContent: S.of(context).litecoin_mweb_warning,
457 buttonText: S.of(context).understand,
458 buttonAction: () {
459 Navigator.of(context).pop();
460 },
461 ));
462 }
463 dashboardViewModel.setMwebEnabled();
464 }
465
466 Future<void> _dismissMweb(BuildContext context) async {
467 await showPopUp<void>(
468 context: context,
469 builder: (BuildContext context) => AlertWithOneAction(
470 alertTitle: S.of(context).alert_notice,
471 alertContent: S.of(context).litecoin_mweb_enable_later,
472 buttonText: S.of(context).understand,
473 buttonAction: () {
474 Navigator.of(context).pop();
475 },
476 ));
477 dashboardViewModel.dismissMweb();
478 }
479 }