| 1 | import "package:cake_wallet/anonpay/anonpay_donation_link_info.dart"; |
| 2 | import "package:cake_wallet/bitcoin/bitcoin.dart"; |
| 3 | import "package:cake_wallet/core/utilities.dart"; |
| 4 | import "package:cake_wallet/di.dart"; |
| 5 | import "package:cake_wallet/entities/auto_generate_subaddress_status.dart"; |
| 6 | import "package:cake_wallet/entities/preferences_key.dart"; |
| 7 | import "package:cake_wallet/generated/i18n.dart"; |
| 8 | import "package:cake_wallet/new-ui/widgets/modern_button.dart"; |
| 9 | import "package:cake_wallet/new-ui/widgets/receive_page/payjoin_copy_modal.dart"; |
| 10 | import "package:cake_wallet/new-ui/widgets/receive_page/receive_address_type.dart"; |
| 11 | import "package:cake_wallet/new-ui/widgets/receive_page/receive_address_widget.dart"; |
| 12 | import "package:cake_wallet/new-ui/widgets/receive_page/receive_amount_display.dart"; |
| 13 | import "package:cake_wallet/new-ui/widgets/receive_page/receive_amount_modal.dart"; |
| 14 | import "package:cake_wallet/new-ui/widgets/receive_page/receive_bottom_buttons.dart"; |
| 15 | import "package:cake_wallet/new-ui/widgets/receive_page/receive_info_box.dart"; |
| 16 | import "package:cake_wallet/new-ui/widgets/receive_page/receive_label_modal.dart"; |
| 17 | import "package:cake_wallet/new-ui/widgets/receive_page/receive_label_widget.dart"; |
| 18 | import "package:cake_wallet/new-ui/widgets/receive_page/receive_large_amount_preview.dart"; |
| 19 | import "package:cake_wallet/new-ui/widgets/receive_page/receive_qr_code.dart"; |
| 20 | import "package:cake_wallet/new-ui/widgets/receive_page/receive_token_display.dart"; |
| 21 | import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart"; |
| 22 | import "package:cake_wallet/routes.dart"; |
| 23 | import "package:cake_wallet/src/screens/receive/anonpay_receive_page.dart"; |
| 24 | import "package:cake_wallet/utils/share_util.dart"; |
| 25 | import "package:cake_wallet/view_model/dashboard/dashboard_view_model.dart"; |
| 26 | import "package:cake_wallet/view_model/dashboard/receive_option_view_model.dart"; |
| 27 | import "package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart"; |
| 28 | import "package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart"; |
| 29 | import "package:cake_wallet/zcash/zcash.dart"; |
| 30 | import "package:cw_core/crypto_currency.dart"; |
| 31 | import "package:cw_core/payment_uris.dart"; |
| 32 | import "package:cw_core/receive_page_option.dart"; |
| 33 | import "package:cw_core/utils/print_verbose.dart"; |
| 34 | import "package:cw_core/wallet_type.dart"; |
| 35 | import "package:flutter/cupertino.dart"; |
| 36 | import "package:flutter/material.dart"; |
| 37 | import "package:flutter/services.dart"; |
| 38 | import "package:flutter_mobx/flutter_mobx.dart"; |
| 39 | import "package:mobx/mobx.dart"; |
| 40 | import "package:modal_bottom_sheet/modal_bottom_sheet.dart"; |
| 41 | import "package:shared_preferences/shared_preferences.dart"; |
| 42 | |
| 43 | class NewReceivePage extends StatefulWidget { |
| 44 | NewReceivePage({ |
| 45 | required this.addressListViewModel, |
| 46 | required this.receiveOptionViewModel, |
| 47 | required this.dashboardViewModel, |
| 48 | required this.lightningMode, |
| 49 | super.key, |
| 50 | CryptoCurrency? initialCurrency, |
| 51 | }) { |
| 52 | if (initialCurrency != null && initialCurrency != addressListViewModel.selectedCurrency) { |
| 53 | addressListViewModel.setTokenCurrency(initialCurrency); |
| 54 | } |
| 55 | } |
| 56 | |
| 57 | final WalletAddressListViewModel addressListViewModel; |
| 58 | final ReceiveOptionViewModel receiveOptionViewModel; |
| 59 | final DashboardViewModel dashboardViewModel; |
| 60 | final bool lightningMode; |
| 61 | |
| 62 | @override |
| 63 | State<NewReceivePage> createState() => _NewReceivePageState(); |
| 64 | } |
| 65 | |
| 66 | class _NewReceivePageState extends State<NewReceivePage> { |
| 67 | bool _largeQrMode = false; |
| 68 | late WalletAddressListItem? _addressItemWithLabel; |
| 69 | |
| 70 | @override |
| 71 | void initState() { |
| 72 | super.initState(); |
| 73 | |
| 74 | WidgetsBinding.instance.addPostFrameCallback((_) { |
| 75 | if (widget.lightningMode) { |
| 76 | widget.receiveOptionViewModel.selectReceiveOption( |
| 77 | widget.receiveOptionViewModel.options |
| 78 | .firstWhereOrNull((item) => item.value.contains("Lightning")) ?? |
| 79 | ReceivePageOption.mainnet, |
| 80 | ); |
| 81 | widget.addressListViewModel.selectedCurrency = CryptoCurrency.btcln; |
| 82 | } else if (widget.addressListViewModel.wallet.type == WalletType.bitcoin) { |
| 83 | widget.receiveOptionViewModel.selectReceiveOption( |
| 84 | widget.receiveOptionViewModel.options |
| 85 | .firstWhereOrNull((item) => item.value.contains("Standard")) ?? |
| 86 | ReceivePageOption.mainnet, |
| 87 | ); |
| 88 | widget.addressListViewModel.selectedCurrency = CryptoCurrency.btc; |
| 89 | } |
| 90 | }); |
| 91 | |
| 92 | reaction((_) => widget.addressListViewModel.uri, _reloadAddressWithLabel); |
| 93 | |
| 94 | _addressItemWithLabel = widget.addressListViewModel.forceRecomputeItems.firstWhereOrNull( |
| 95 | (item) => |
| 96 | item is WalletAddressListItem && item.address == widget.addressListViewModel.uri.address, |
| 97 | ) as WalletAddressListItem?; |
| 98 | |
| 99 | reaction((_) => widget.receiveOptionViewModel.selectedReceiveOption, (option) { |
| 100 | // The infobox depends on the selected address type, so the page has to be |
| 101 | // rebuilt when it changes. |
| 102 | if (mounted) setState(() {}); |
| 103 | |
| 104 | if (widget.dashboardViewModel.type == WalletType.bitcoin && |
| 105 | bitcoin!.isBitcoinReceivePageOption(option)) { |
| 106 | widget.addressListViewModel.setAddressType(bitcoin!.getOptionToType(option)); |
| 107 | if (option.value.contains("Lightning")) { |
| 108 | widget.addressListViewModel.selectedCurrency = CryptoCurrency.btcln; |
| 109 | } else { |
| 110 | widget.addressListViewModel.selectedCurrency = CryptoCurrency.btc; |
| 111 | } |
| 112 | return; |
| 113 | } |
| 114 | if (widget.dashboardViewModel.type == WalletType.zcash) { |
| 115 | widget.addressListViewModel.setAddressType(zcash!.getOptionToType(option)); |
| 116 | return; |
| 117 | } |
| 118 | |
| 119 | switch (option) { |
| 120 | case ReceivePageOption.anonPayInvoice: |
| 121 | Navigator.pushNamed( |
| 122 | context, |
| 123 | Routes.anonPayInvoicePage, |
| 124 | arguments: [widget.addressListViewModel.address.address, option], |
| 125 | ); |
| 126 | break; |
| 127 | case ReceivePageOption.anonPayDonationLink: |
| 128 | final sharedPreferences = getIt.get<SharedPreferences>(); |
| 129 | final clearnetUrl = sharedPreferences.getString(PreferencesKey.clearnetDonationLink); |
| 130 | final onionUrl = sharedPreferences.getString(PreferencesKey.onionDonationLink); |
| 131 | final donationWalletName = |
| 132 | sharedPreferences.getString(PreferencesKey.donationLinkWalletName); |
| 133 | |
| 134 | if (clearnetUrl != null && |
| 135 | onionUrl != null && |
| 136 | widget.addressListViewModel.wallet.name == donationWalletName) { |
| 137 | Navigator.pushNamed( |
| 138 | context, |
| 139 | Routes.anonPayReceivePage, |
| 140 | arguments: AnonPayReceivePageArgs( |
| 141 | invoiceInfo: AnonpayDonationLinkInfo( |
| 142 | clearnetUrl: clearnetUrl, |
| 143 | onionUrl: onionUrl, |
| 144 | address: widget.addressListViewModel.address.address, |
| 145 | ), |
| 146 | qrImage: widget.addressListViewModel.qrImage, |
| 147 | ), |
| 148 | ); |
| 149 | } else { |
| 150 | Navigator.pushNamed( |
| 151 | context, |
| 152 | Routes.anonPayInvoicePage, |
| 153 | arguments: [widget.addressListViewModel.address.address, option], |
| 154 | ); |
| 155 | } |
| 156 | break; |
| 157 | default: |
| 158 | if ([WalletType.bitcoin, WalletType.litecoin] |
| 159 | .contains(widget.addressListViewModel.type)) { |
| 160 | widget.addressListViewModel.setAddressType(bitcoin!.getBitcoinAddressType(option)); |
| 161 | } |
| 162 | if (widget.addressListViewModel.type == WalletType.zcash) { |
| 163 | printV("help me i'll kms if that wont work: ${zcash!.getZcashAddressType(option)}"); |
| 164 | widget.addressListViewModel.setAddressType(zcash!.getZcashAddressType(option)); |
| 165 | } |
| 166 | } |
| 167 | }); |
| 168 | } |
| 169 | |
| 170 | @override |
| 171 | Widget build(BuildContext context) { |
| 172 | final hasAddressTypeSelector = widget.receiveOptionViewModel.options.length > 1; |
| 173 | final hasLabel = _addressItemWithLabel?.name != null && _addressItemWithLabel!.name!.isNotEmpty; |
| 174 | final infoboxDismissed = widget.addressListViewModel.wallet.walletInfo.receiveInfoboxDismissed; |
| 175 | final infobox = ReceiveInfoBox.forWalletType( |
| 176 | widget.addressListViewModel.type, |
| 177 | supportedCurrencies: |
| 178 | widget.addressListViewModel.tokenCurrencies.whereType<CryptoCurrency>().toList(), |
| 179 | onDismissed: () { |
| 180 | widget.addressListViewModel.dismissInfobox(); |
| 181 | setState(() {}); |
| 182 | }, |
| 183 | autoGenerateSubaddressStatus: widget.lightningMode |
| 184 | ? AutoGenerateSubaddressStatus.disabled |
| 185 | : widget.dashboardViewModel.settingsStore.autoGenerateSubaddressStatus, |
| 186 | addressRotates: _selectedAddressRotates, |
| 187 | ); |
| 188 | |
| 189 | return Container( |
| 190 | decoration: BoxDecoration( |
| 191 | gradient: LinearGradient( |
| 192 | colors: [ |
| 193 | Theme.of(context).colorScheme.surfaceBright, |
| 194 | Theme.of(context).colorScheme.surface, |
| 195 | ], |
| 196 | begin: Alignment.topCenter, |
| 197 | end: Alignment.bottomCenter, |
| 198 | ), |
| 199 | borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), |
| 200 | ), |
| 201 | child: SafeArea( |
| 202 | child: Column( |
| 203 | mainAxisSize: MainAxisSize.max, |
| 204 | mainAxisAlignment: MainAxisAlignment.start, |
| 205 | children: [ |
| 206 | ModalTopBar( |
| 207 | title: _largeQrMode ? "" : S.of(context).receive, |
| 208 | leadingIcon: const Icon(Icons.close), |
| 209 | leadingSemanticLabel: S.of(context).close, |
| 210 | onLeadingPressed: () { |
| 211 | Navigator.of(context, rootNavigator: true).pop(); |
| 212 | }, |
| 213 | trailingWidget: Observer( |
| 214 | builder: (_) => AnimatedSwitcher( |
| 215 | duration: const Duration(milliseconds: 300), |
| 216 | child: _largeQrMode || |
| 217 | widget.addressListViewModel.hasAddressRotation |
| 218 | /* TODO rotating is broken on mweb, disabling for now, fix after mvp*/ |
| 219 | && |
| 220 | !(widget.receiveOptionViewModel.selectedReceiveOption.description ?? |
| 221 | "") |
| 222 | .toLowerCase() |
| 223 | .contains("mweb") |
| 224 | ? ModernButton( |
| 225 | key: ValueKey(_largeQrMode), |
| 226 | size: 36, |
| 227 | icon: _largeQrMode |
| 228 | ? const Icon(Icons.share) |
| 229 | : widget.addressListViewModel.isRotatingAddress |
| 230 | ? const CupertinoActivityIndicator() |
| 231 | : const Icon(Icons.refresh), |
| 232 | semanticLabel: _largeQrMode |
| 233 | ? S.of(context).share_address |
| 234 | : S.of(context).rotate_address, |
| 235 | onPressed: () { |
| 236 | if (_largeQrMode) { |
| 237 | ShareUtil.share( |
| 238 | text: widget.addressListViewModel.uri.toString(), |
| 239 | context: context, |
| 240 | ); |
| 241 | } else { |
| 242 | if (widget.addressListViewModel.hasAddressRotation) { |
| 243 | widget.addressListViewModel.rotateAddress(); |
| 244 | } |
| 245 | } |
| 246 | }, |
| 247 | ) |
| 248 | : const SizedBox.shrink(), |
| 249 | ), |
| 250 | ), |
| 251 | ), |
| 252 | Expanded( |
| 253 | child: Column( |
| 254 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, |
| 255 | mainAxisSize: MainAxisSize.max, |
| 256 | children: [ |
| 257 | ReceiveAmountDisplay( |
| 258 | walletAddressListViewModel: widget.addressListViewModel, |
| 259 | largeQrMode: _largeQrMode, |
| 260 | ), |
| 261 | ReceiveQrCode( |
| 262 | addressListViewModel: widget.addressListViewModel, |
| 263 | onTap: () { |
| 264 | setState(() { |
| 265 | _largeQrMode = !_largeQrMode; |
| 266 | // _infoboxDimissed = true; |
| 267 | widget.addressListViewModel.dismissInfobox(); |
| 268 | }); |
| 269 | }, |
| 270 | largeQrMode: _largeQrMode, |
| 271 | ), |
| 272 | if (widget.addressListViewModel.tokenCurrency != null) |
| 273 | ReceiveTokenDisplay(addressListViewModel: widget.addressListViewModel), |
| 274 | if (hasAddressTypeSelector) |
| 275 | ReceiveAddressTypeDisplay( |
| 276 | lightningMode: widget.lightningMode, |
| 277 | receiveOptionViewModel: widget.receiveOptionViewModel, |
| 278 | largeQrMode: _largeQrMode, |
| 279 | ), |
| 280 | ReceiveAddressWidget( |
| 281 | addressListViewModel: widget.addressListViewModel, |
| 282 | ), |
| 283 | // The label chip animates to zero height when there is no |
| 284 | // label (or in large QR mode); keep it out of the semantics |
| 285 | // tree entirely while it is collapsed. |
| 286 | ExcludeSemantics( |
| 287 | excluding: _largeQrMode || !hasLabel, |
| 288 | child: MergeSemantics( |
| 289 | child: Semantics( |
| 290 | button: true, |
| 291 | hint: S.of(context).set_label, |
| 292 | child: GestureDetector( |
| 293 | onTap: _showLabelModal, |
| 294 | child: ReceiveLabelWidget( |
| 295 | name: _addressItemWithLabel?.name ?? "", |
| 296 | largeQrMode: _largeQrMode, |
| 297 | ), |
| 298 | ), |
| 299 | ), |
| 300 | ), |
| 301 | ), |
| 302 | Observer( |
| 303 | builder: (_) => ReceiveBottomButtons( |
| 304 | key: const ValueKey(0), |
| 305 | largeQrMode: _largeQrMode, |
| 306 | showAccountsButton: widget.addressListViewModel.hasAddressList, |
| 307 | showLabelButton: widget.addressListViewModel.hasAddressList && !hasLabel, |
| 308 | copyData: widget.addressListViewModel.hasPayjoin |
| 309 | ? null |
| 310 | : ClipboardData( |
| 311 | text: widget.addressListViewModel.displayAmount.isEmpty |
| 312 | ? widget.addressListViewModel.uri.address |
| 313 | : widget.addressListViewModel.uri.toString(), |
| 314 | ), |
| 315 | onCopyButtonPressed: () { |
| 316 | if (widget.addressListViewModel.hasPayjoin) { |
| 317 | showModalBottomSheet( |
| 318 | isScrollControlled: true, |
| 319 | context: context, |
| 320 | builder: (context) => |
| 321 | PayjoinCopyModal(uri: widget.addressListViewModel.uri), |
| 322 | ); |
| 323 | } |
| 324 | }, |
| 325 | onAmountButtonPressed: () { |
| 326 | showMaterialModalBottomSheet( |
| 327 | context: context, |
| 328 | backgroundColor: Colors.transparent, |
| 329 | barrierColor: Colors.black.withAlpha(80), |
| 330 | builder: (context) => ReceiveAmountModal( |
| 331 | walletAddressListViewModel: widget.addressListViewModel, |
| 332 | onSubmitted: (amount) {}, |
| 333 | ), |
| 334 | ); |
| 335 | }, |
| 336 | onLabelButtonPressed: _showLabelModal, |
| 337 | onAccountsButtonPressed: () { |
| 338 | Navigator.of(context).pushNamed( |
| 339 | Routes.receiveAddresses, |
| 340 | arguments: false, |
| 341 | ); |
| 342 | }, |
| 343 | ), |
| 344 | ), |
| 345 | ReceiveLargeAmountPreview( |
| 346 | amount: widget.addressListViewModel.displayAmount, |
| 347 | currency: widget.addressListViewModel.cryptoCurrencySymbol, |
| 348 | largeQrMode: _largeQrMode, |
| 349 | ), |
| 350 | if (infobox != null && !widget.addressListViewModel.isLightning) |
| 351 | ClipRect( |
| 352 | child: AnimatedAlign( |
| 353 | duration: const Duration(milliseconds: 200), |
| 354 | curve: Curves.easeOutCubic, |
| 355 | heightFactor: infoboxDismissed ? 0 : 1, |
| 356 | alignment: Alignment.center, |
| 357 | child: AnimatedOpacity( |
| 358 | duration: const Duration(milliseconds: 200), |
| 359 | opacity: infoboxDismissed ? 0 : 1, |
| 360 | curve: Curves.easeOutCubic, |
| 361 | child: infobox, |
| 362 | ), |
| 363 | ), |
| 364 | ), |
| 365 | ], |
| 366 | ), |
| 367 | ), |
| 368 | ], |
| 369 | ), |
| 370 | ), |
| 371 | ); |
| 372 | } |
| 373 | |
| 374 | /// Zcash is the only wallet type that also offers static address types, so |
| 375 | /// the rotation notice must not be shown for it unless the disposable |
| 376 | /// transparent type is the one currently selected. |
| 377 | bool get _selectedAddressRotates => |
| 378 | widget.addressListViewModel.type != WalletType.zcash || |
| 379 | zcash!.isRotatingAddressOption(widget.receiveOptionViewModel.selectedReceiveOption); |
| 380 | |
| 381 | void _showLabelModal() { |
| 382 | showMaterialModalBottomSheet( |
| 383 | context: context, |
| 384 | backgroundColor: Colors.transparent, |
| 385 | barrierColor: Colors.black.withAlpha(80), |
| 386 | builder: (_) => getIt.get<ReceiveLabelModal>(param1: _addressItemWithLabel), |
| 387 | ).then((value) { |
| 388 | _reloadAddressWithLabel(widget.addressListViewModel.uri); |
| 389 | }); |
| 390 | } |
| 391 | |
| 392 | void _reloadAddressWithLabel(PaymentURI newAddress) { |
| 393 | // FIXME: viewmodel doesn't want to load address name here, so we make it. investigate why later |
| 394 | setState(() { |
| 395 | _addressItemWithLabel = widget.addressListViewModel.forceRecomputeItems.firstWhereOrNull( |
| 396 | (item) => item is WalletAddressListItem && item.address == newAddress.address, |
| 397 | ) as WalletAddressListItem?; |
| 398 | }); |
| 399 | } |
| 400 | } |