| 1 | import 'package:another_flushbar/flushbar.dart'; |
| 2 | import 'package:cake_wallet/core/new_wallet_arguments.dart'; |
| 3 | import 'package:cake_wallet/core/auth_service.dart'; |
| 4 | import 'package:cake_wallet/entities/desktop_dropdown_item.dart'; |
| 5 | import 'package:cake_wallet/generated/i18n.dart'; |
| 6 | import 'package:cake_wallet/routes.dart'; |
| 7 | import 'package:cake_wallet/src/screens/auth/auth_page.dart'; |
| 8 | import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/dropdown_item_widget.dart'; |
| 9 | import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.dart'; |
| 10 | import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; |
| 11 | import 'package:cake_wallet/store/settings_store.dart'; |
| 12 | import 'package:cake_wallet/utils/show_bar.dart'; |
| 13 | import 'package:cake_wallet/utils/show_pop_up.dart'; |
| 14 | import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; |
| 15 | import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; |
| 16 | import 'package:cake_wallet/wallet_type_utils.dart'; |
| 17 | import 'package:cw_core/wallet_type.dart'; |
| 18 | import 'package:flutter/material.dart'; |
| 19 | import 'package:flutter_mobx/flutter_mobx.dart'; |
| 20 | import 'package:flutter_svg/svg.dart'; |
| 21 | |
| 22 | class DesktopWalletSelectionDropDown extends StatefulWidget { |
| 23 | final WalletListViewModel walletListViewModel; |
| 24 | final AuthService _authService; |
| 25 | |
| 26 | DesktopWalletSelectionDropDown(this.walletListViewModel, this._authService, {Key? key}) |
| 27 | : super(key: key); |
| 28 | |
| 29 | @override |
| 30 | State<DesktopWalletSelectionDropDown> createState() => _DesktopWalletSelectionDropDownState(); |
| 31 | } |
| 32 | |
| 33 | class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionDropDown> { |
| 34 | final moneroIcon = |
| 35 | Image.asset('assets/new-ui/crypto_full_icons/monero.svg', height: 24, width: 24); |
| 36 | final bitcoinIcon = |
| 37 | Image.asset('assets/new-ui/crypto_full_icons/bitcoin.svg', height: 24, width: 24); |
| 38 | final tBitcoinIcon = Image.asset('assets/images/tbtc.png', height: 24, width: 24); |
| 39 | final litecoinIcon = |
| 40 | Image.asset('assets/new-ui/crypto_full_icons/litecoin.svg', height: 24, width: 24); |
| 41 | final havenIcon = Image.asset('assets/images/haven_logo.webp', height: 24, width: 24); |
| 42 | final ethereumIcon = |
| 43 | Image.asset('assets/new-ui/crypto_full_icons/ethereum.svg', height: 24, width: 24); |
| 44 | final polygonIcon = |
| 45 | Image.asset('assets/new-ui/crypto_full_icons/polygon.svg', height: 24, width: 24); |
| 46 | final bitcoinCashIcon = |
| 47 | Image.asset('assets/new-ui/crypto_full_icons/bitcoin-cash.svg', height: 24, width: 24); |
| 48 | final baseIcon = Image.asset('assets/new-ui/crypto_full_icons/base.svg', height: 24, width: 24); |
| 49 | final arbitrumIcon = |
| 50 | Image.asset('assets/new-ui/crypto_full_icons/arbitrum.svg', height: 24, width: 24); |
| 51 | final bscIcon = Image.asset('assets/new-ui/crypto_full_icons/bnb.svg', height: 24, width: 24); |
| 52 | final nanoIcon = Image.asset('assets/new-ui/crypto_full_icons/nano.svg', height: 24, width: 24); |
| 53 | final bananoIcon = Image.asset('assets/new-ui/crypto_full_icons/nano.svg', height: 24, width: 24); |
| 54 | final solanaIcon = |
| 55 | Image.asset('assets/new-ui/crypto_full_icons/solana.svg', height: 24, width: 24); |
| 56 | final tronIcon = Image.asset('assets/new-ui/crypto_full_icons/tron.svg', height: 24, width: 24); |
| 57 | final wowneroIcon = |
| 58 | Image.asset('assets/new-ui/crypto_full_icons/wownero.svg', height: 24, width: 24); |
| 59 | final zanoIcon = Image.asset('assets/new-ui/crypto_full_icons/zano.svg', height: 24, width: 24); |
| 60 | final decredIcon = |
| 61 | Image.asset('assets/new-ui/crypto_full_icons/decred.svg', height: 24, width: 24); |
| 62 | final dogeIcon = |
| 63 | Image.asset('assets/new-ui/crypto_full_icons/dogecoin.svg', height: 24, width: 24); |
| 64 | final zcashIcon = Image.asset('assets/new-ui/crypto_full_icons/zcash.svg', height: 24, width: 24); |
| 65 | final nonWalletTypeIcon = Image.asset('assets/images/close.png', height: 24, width: 24); |
| 66 | |
| 67 | Image _newWalletImage(BuildContext context) => Image.asset( |
| 68 | 'assets/images/new_wallet.png', |
| 69 | height: 12, |
| 70 | width: 12, |
| 71 | color: Theme.of(context).colorScheme.onSurface, |
| 72 | ); |
| 73 | |
| 74 | Image _restoreWalletImage(BuildContext context) => Image.asset( |
| 75 | 'assets/images/restore_wallet.png', |
| 76 | height: 12, |
| 77 | width: 12, |
| 78 | color: Theme.of(context).colorScheme.onSurface, |
| 79 | ); |
| 80 | |
| 81 | Flushbar<void>? _progressBar; |
| 82 | |
| 83 | @override |
| 84 | Widget build(BuildContext context) { |
| 85 | final themeData = Theme.of(context); |
| 86 | return Observer(builder: (context) { |
| 87 | final dropDownItems = [ |
| 88 | ...widget.walletListViewModel.wallets |
| 89 | .map((wallet) => DesktopDropdownItem( |
| 90 | isSelected: wallet.isCurrent, |
| 91 | child: ConstrainedBox( |
| 92 | constraints: BoxConstraints(maxWidth: 500), |
| 93 | child: DropDownItemWidget( |
| 94 | title: wallet.name, |
| 95 | image: wallet.isEnabled |
| 96 | ? _imageFor(type: wallet.type, isTestnet: wallet.isTestnet) |
| 97 | : nonWalletTypeIcon, |
| 98 | ), |
| 99 | ), |
| 100 | onSelected: () => _onSelectedWallet(wallet), |
| 101 | )) |
| 102 | .toList(), |
| 103 | DesktopDropdownItem( |
| 104 | onSelected: () => _navigateToCreateWallet(), |
| 105 | child: DropDownItemWidget( |
| 106 | title: S.of(context).create_new, |
| 107 | image: _newWalletImage(context), |
| 108 | ), |
| 109 | ), |
| 110 | DesktopDropdownItem( |
| 111 | onSelected: () => _navigateToRestoreWallet(), |
| 112 | child: DropDownItemWidget( |
| 113 | title: S.of(context).restore_wallet, |
| 114 | image: _restoreWalletImage(context), |
| 115 | ), |
| 116 | ), |
| 117 | ]; |
| 118 | |
| 119 | final selectedItem = dropDownItems.firstWhere( |
| 120 | (element) => element.isSelected, |
| 121 | orElse: () => dropDownItems.first, |
| 122 | ); |
| 123 | |
| 124 | return DropdownButton<DesktopDropdownItem>( |
| 125 | items: dropDownItems |
| 126 | .map( |
| 127 | (wallet) => DropdownMenuItem<DesktopDropdownItem>( |
| 128 | child: wallet.child, |
| 129 | value: wallet, |
| 130 | ), |
| 131 | ) |
| 132 | .toList(), |
| 133 | onChanged: (item) { |
| 134 | item?.onSelected(); |
| 135 | }, |
| 136 | dropdownColor: themeData.colorScheme.surface, |
| 137 | style: themeData.textTheme.bodyMedium, |
| 138 | selectedItemBuilder: (context) => dropDownItems.map((item) => item.child).toList(), |
| 139 | value: selectedItem, |
| 140 | underline: const SizedBox(), |
| 141 | focusColor: Colors.transparent, |
| 142 | borderRadius: BorderRadius.circular(15.0), |
| 143 | ); |
| 144 | }); |
| 145 | } |
| 146 | |
| 147 | void _onSelectedWallet(WalletListItem selectedWallet) async { |
| 148 | if (selectedWallet.isCurrent || !selectedWallet.isEnabled) return; |
| 149 | |
| 150 | WidgetsBinding.instance.addPostFrameCallback((_) async { |
| 151 | final confirmed = await showPopUp<bool>( |
| 152 | context: context, |
| 153 | builder: (dialogContext) { |
| 154 | return AlertWithTwoActions( |
| 155 | alertTitle: S.of(context).change_wallet_alert_title, |
| 156 | alertContent: S.of(context).change_wallet_alert_content(selectedWallet.name), |
| 157 | leftButtonText: S.of(context).cancel, |
| 158 | rightButtonText: S.of(context).change, |
| 159 | actionLeftButton: () => Navigator.of(dialogContext).pop(false), |
| 160 | actionRightButton: () => Navigator.of(dialogContext).pop(true)); |
| 161 | }) ?? |
| 162 | false; |
| 163 | |
| 164 | if (confirmed) { |
| 165 | await _loadWallet(selectedWallet); |
| 166 | } |
| 167 | }); |
| 168 | } |
| 169 | |
| 170 | Widget _imageFor({required WalletType type, bool? isTestnet}) { |
| 171 | switch (type) { |
| 172 | case WalletType.bitcoin: |
| 173 | if (isTestnet == true) return tBitcoinIcon; |
| 174 | return bitcoinIcon; |
| 175 | case WalletType.monero: |
| 176 | return moneroIcon; |
| 177 | case WalletType.wownero: |
| 178 | return wowneroIcon; |
| 179 | case WalletType.litecoin: |
| 180 | return litecoinIcon; |
| 181 | case WalletType.haven: |
| 182 | return havenIcon; |
| 183 | case WalletType.ethereum: |
| 184 | return ethereumIcon; |
| 185 | case WalletType.bitcoinCash: |
| 186 | return bitcoinCashIcon; |
| 187 | case WalletType.nano: |
| 188 | return nanoIcon; |
| 189 | case WalletType.banano: |
| 190 | return bananoIcon; |
| 191 | case WalletType.polygon: |
| 192 | return polygonIcon; |
| 193 | case WalletType.solana: |
| 194 | return solanaIcon; |
| 195 | case WalletType.tron: |
| 196 | return tronIcon; |
| 197 | case WalletType.zano: |
| 198 | return zanoIcon; |
| 199 | case WalletType.decred: |
| 200 | return decredIcon; |
| 201 | case WalletType.dogecoin: |
| 202 | return dogeIcon; |
| 203 | case WalletType.base: |
| 204 | return baseIcon; |
| 205 | case WalletType.arbitrum: |
| 206 | return arbitrumIcon; |
| 207 | case WalletType.bsc: |
| 208 | return bscIcon; |
| 209 | case WalletType.zcash: |
| 210 | return zcashIcon; |
| 211 | case WalletType.none: |
| 212 | return nonWalletTypeIcon; |
| 213 | } |
| 214 | } |
| 215 | |
| 216 | Future<void> _loadWallet(WalletListItem wallet) async { |
| 217 | if (SettingsStoreBase.walletPasswordDirectInput) { |
| 218 | Navigator.of(context).pushNamed(Routes.walletUnlockLoadable, |
| 219 | arguments: WalletUnlockArguments( |
| 220 | callback: (bool isAuthenticatedSuccessfully, AuthPageState auth) async { |
| 221 | if (isAuthenticatedSuccessfully) { |
| 222 | auth.close(); |
| 223 | setState(() {}); |
| 224 | } |
| 225 | }, |
| 226 | walletName: wallet.name, |
| 227 | walletType: wallet.type)); |
| 228 | return; |
| 229 | } |
| 230 | |
| 231 | widget._authService.authenticateAction( |
| 232 | context, |
| 233 | onAuthSuccess: (isAuthenticatedSuccessfully) async { |
| 234 | if (!isAuthenticatedSuccessfully) return; |
| 235 | |
| 236 | try { |
| 237 | if (mounted) { |
| 238 | changeProcessText(S.of(context).wallet_list_loading_wallet(wallet.name)); |
| 239 | } |
| 240 | await widget.walletListViewModel.loadWallet(wallet); |
| 241 | hideProgressText(); |
| 242 | setState(() {}); |
| 243 | } catch (e) { |
| 244 | if (mounted) { |
| 245 | changeProcessText(S.of(context).wallet_list_failed_to_load(wallet.name, e.toString())); |
| 246 | } |
| 247 | } |
| 248 | }, |
| 249 | conditionToDetermineIfToUse2FA: |
| 250 | widget.walletListViewModel.shouldRequireTOTP2FAForAccessingWallet, |
| 251 | ); |
| 252 | } |
| 253 | |
| 254 | void _navigateToCreateWallet() { |
| 255 | if (isSingleCoin) { |
| 256 | widget._authService.authenticateAction( |
| 257 | context, |
| 258 | route: Routes.newWallet, |
| 259 | arguments: NewWalletArguments(type: widget.walletListViewModel.currentWalletType), |
| 260 | conditionToDetermineIfToUse2FA: |
| 261 | widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets, |
| 262 | ); |
| 263 | } else { |
| 264 | widget._authService.authenticateAction( |
| 265 | context, |
| 266 | route: Routes.newWalletType, |
| 267 | conditionToDetermineIfToUse2FA: |
| 268 | widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets, |
| 269 | ); |
| 270 | } |
| 271 | } |
| 272 | |
| 273 | void _navigateToRestoreWallet() { |
| 274 | Navigator.of(context).pushNamed(Routes.restoreOptions, arguments: false); |
| 275 | } |
| 276 | |
| 277 | void changeProcessText(String text) { |
| 278 | _progressBar = createBar<void>(text, context, duration: null)..show(context); |
| 279 | } |
| 280 | |
| 281 | void hideProgressText() { |
| 282 | _progressBar?.dismiss(); |
| 283 | _progressBar = null; |
| 284 | } |
| 285 | } |