/* * This file is part of Stack Wallet. * * Copyright (c) 2023 Cypher Stack * All Rights Reserved. * The code is distributed under GPLv3 license, see LICENSE file for details. * Generated by Cypher Stack on 2023-05-26 * */ import 'dart:async'; import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:isar/isar.dart'; import '../../models/isar/models/isar_models.dart'; import '../../models/keys/view_only_wallet_data.dart'; import '../../notifications/show_flush_bar.dart'; import '../../providers/db/main_db_provider.dart'; import '../../providers/providers.dart'; import '../../route_generator.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/address_utils.dart'; import '../../utilities/assets.dart'; import '../../utilities/clipboard_interface.dart'; import '../../utilities/constants.dart'; import '../../utilities/enums/derive_path_type_enum.dart'; import '../../utilities/text_styles.dart'; import '../../wallets/crypto_currency/crypto_currency.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; import '../../wallets/wallet/impl/bitcoin_wallet.dart'; import '../../wallets/wallet/intermediate/bip39_hd_wallet.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart'; import '../../widgets/background.dart'; import '../../widgets/conditional_parent.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../widgets/custom_buttons/blue_text_button.dart'; import '../../widgets/custom_loading_overlay.dart'; import '../../widgets/desktop/primary_button.dart'; import '../../widgets/desktop/secondary_button.dart'; import '../../widgets/qr.dart'; import '../../widgets/rounded_white_container.dart'; import 'addresses/wallet_addresses_view.dart'; import 'generate_receiving_uri_qr_code_view.dart'; class ReceiveView extends ConsumerStatefulWidget { const ReceiveView({ super.key, required this.walletId, this.tokenContract, this.clipboard = const ClipboardWrapper(), }); static const String routeName = "/receiveView"; final String walletId; final EthContract? tokenContract; final ClipboardInterface clipboard; @override ConsumerState createState() => _ReceiveViewState(); } class _ReceiveViewState extends ConsumerState { late final CryptoCurrency coin; late final String walletId; late final ClipboardInterface clipboard; late final bool _supportsSpark; late final bool _showMultiType; int _currentIndex = 0; final List _walletAddressTypes = []; final Map _addressMap = {}; final Map> _addressSubMap = {}; Future generateNewAddress() async { final wallet = ref.read(pWallets).getWallet(walletId); if (wallet is MultiAddressInterface) { bool shouldPop = false; unawaited( showDialog( context: context, builder: (_) { return WillPopScope( onWillPop: () async => shouldPop, child: Container( color: Theme.of(context) .extension()! .overlay .withOpacity(0.5), child: const CustomLoadingOverlay( message: "Generating address", eventBus: null, ), ), ); }, ), ); final Address? address; if (wallet is Bip39HDWallet && wallet is! BCashInterface) { DerivePathType? type; if (wallet.isViewOnly && wallet is ExtendedKeysInterface) { final voData = await wallet.getViewOnlyWalletData() as ExtendedKeysViewOnlyWalletData; for (final t in wallet.cryptoCurrency.supportedDerivationPathTypes) { final testPath = wallet.cryptoCurrency.constructDerivePath( derivePathType: t, chain: 0, index: 0, ); if (testPath.startsWith(voData.xPubs.first.path)) { type = t; break; } } } else { type = DerivePathType.values.firstWhere( (e) => e.getAddressType() == _walletAddressTypes[_currentIndex], ); } address = await wallet.generateNextReceivingAddress( derivePathType: type!, ); final isar = ref.read(mainDBProvider).isar; await isar.writeTxn(() async { await isar.addresses.put(address!); }); final info = ref.read(pWalletInfo(walletId)); await info.updateReceivingAddress( newAddress: address.value, isar: isar, ); } else { await wallet.generateNewReceivingAddress(); address = null; } shouldPop = true; if (mounted) { Navigator.of(context) .popUntil(ModalRoute.withName(ReceiveView.routeName)); setState(() { _addressMap[_walletAddressTypes[_currentIndex]] = address?.value ?? ref.read(pWalletReceivingAddress(walletId)); }); } } } Future generateNewSparkAddress() async { final wallet = ref.read(pWallets).getWallet(walletId); if (wallet is SparkInterface) { bool shouldPop = false; unawaited( showDialog( context: context, builder: (_) { return WillPopScope( onWillPop: () async => shouldPop, child: Container( color: Theme.of(context) .extension()! .overlay .withOpacity(0.5), child: const CustomLoadingOverlay( message: "Generating address", eventBus: null, ), ), ); }, ), ); final address = await wallet.generateNextSparkAddress(); await ref.read(mainDBProvider).isar.writeTxn(() async { await ref.read(mainDBProvider).isar.addresses.put(address); }); shouldPop = true; if (mounted) { Navigator.of(context, rootNavigator: true).pop(); setState(() { _addressMap[AddressType.spark] = address.value; }); } } } @override void initState() { walletId = widget.walletId; coin = ref.read(pWalletCoin(walletId)); clipboard = widget.clipboard; final wallet = ref.read(pWallets).getWallet(walletId); _supportsSpark = wallet is SparkInterface; if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly) { _showMultiType = false; } else { _showMultiType = _supportsSpark || (wallet is! BCashInterface && wallet is Bip39HDWallet && wallet.supportedAddressTypes.length > 1); } _walletAddressTypes.add(wallet.info.mainAddressType); if (_showMultiType) { if (_supportsSpark) { _walletAddressTypes.insert(0, AddressType.spark); } else { _walletAddressTypes.addAll( (wallet as Bip39HDWallet) .supportedAddressTypes .where((e) => e != wallet.info.mainAddressType), ); } } if (_walletAddressTypes.length > 1 && wallet is BitcoinWallet) { _walletAddressTypes.removeWhere((e) => e == AddressType.p2pkh); } _addressMap[_walletAddressTypes[_currentIndex]] = ref.read(pWalletReceivingAddress(walletId)); if (_showMultiType) { for (final type in _walletAddressTypes) { _addressSubMap[type] = ref .read(mainDBProvider) .isar .addresses .where() .walletIdEqualTo(walletId) .filter() .typeEqualTo(type) .sortByDerivationIndexDesc() .findFirst() .asStream() .listen((event) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { setState(() { _addressMap[type] = event?.value ?? _addressMap[type] ?? "[No address yet]"; }); } }); }); } } super.initState(); } @override void dispose() { for (final subscription in _addressSubMap.values) { subscription.cancel(); } super.dispose(); } @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); final ticker = widget.tokenContract?.symbol ?? coin.ticker; final String address; if (_showMultiType) { address = _addressMap[_walletAddressTypes[_currentIndex]]!; } else { address = ref.watch(pWalletReceivingAddress(walletId)); } final wallet = ref.watch(pWallets.select((value) => value.getWallet(walletId))); final bool canGen; if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly && wallet.viewOnlyType == ViewOnlyWalletType.addressOnly) { canGen = false; } else { canGen = (wallet is MultiAddressInterface || _supportsSpark); } return Background( child: Scaffold( backgroundColor: Theme.of(context).extension()!.background, appBar: AppBar( leading: AppBarBackButton( onPressed: () { Navigator.of(context).pop(); }, ), title: Text( "Receive $ticker", style: STextStyles.navBarTitle(context), ), actions: [ Padding( padding: const EdgeInsets.only( top: 10, bottom: 10, right: 10, ), child: AspectRatio( aspectRatio: 1, child: AppBarIconButton( semanticsLabel: "Address List Pop-up Button. Opens A Pop-up For Address List Button.", key: const Key("walletNetworkSettingsAddNewNodeViewButton"), size: 36, shadows: const [], color: Theme.of(context).extension()!.background, icon: SvgPicture.asset( Assets.svg.verticalEllipsis, color: Theme.of(context) .extension()! .accentColorDark, width: 20, height: 20, ), onPressed: () { showDialog( barrierColor: Colors.transparent, barrierDismissible: true, context: context, builder: (_) { return Stack( children: [ Positioned( top: 9, right: 10, child: Container( decoration: BoxDecoration( color: Theme.of(context) .extension()! .popupBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), // boxShadow: [CFColors.standardBoxShadow], boxShadow: const [], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ GestureDetector( onTap: () { Navigator.of(context).pop(); Navigator.of(context).pushNamed( WalletAddressesView.routeName, arguments: walletId, ); }, child: RoundedWhiteContainer( boxShadow: [ Theme.of(context) .extension()! .standardBoxShadow, ], child: Material( color: Colors.transparent, child: Padding( padding: const EdgeInsets.symmetric( horizontal: 12, ), child: Text( "Address list", style: STextStyles.field(context), ), ), ), ), ), ], ), ), ), ], ); }, ); }, ), ), ), ], ), body: Padding( padding: const EdgeInsets.all(12), child: SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(4), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ ConditionalParent( condition: _showMultiType, builder: (child) => Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( "Address type", style: STextStyles.w500_14(context).copyWith( color: Theme.of(context) .extension()! .infoItemLabel, ), ), const SizedBox( height: 10, ), DropdownButtonHideUnderline( child: DropdownButton2( value: _currentIndex, items: [ for (int i = 0; i < _walletAddressTypes.length; i++) DropdownMenuItem( value: i, child: Text( _supportsSpark && _walletAddressTypes[i] == AddressType.p2pkh ? "Transparent address" : "${_walletAddressTypes[i].readableName} address", style: STextStyles.w500_14(context), ), ), ], onChanged: (value) { if (value != null && value != _currentIndex) { setState(() { _currentIndex = value; }); } }, isExpanded: true, iconStyleData: IconStyleData( icon: Padding( padding: const EdgeInsets.only(right: 10), child: SvgPicture.asset( Assets.svg.chevronDown, width: 12, height: 6, color: Theme.of(context) .extension()! .textFieldActiveSearchIconRight, ), ), ), buttonStyleData: ButtonStyleData( decoration: BoxDecoration( color: Theme.of(context) .extension()! .textFieldDefaultBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), ), ), dropdownStyleData: DropdownStyleData( offset: const Offset(0, -10), elevation: 0, decoration: BoxDecoration( color: Theme.of(context) .extension()! .textFieldDefaultBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), ), ), menuItemStyleData: const MenuItemStyleData( padding: EdgeInsets.symmetric( horizontal: 16, vertical: 8, ), ), ), ), const SizedBox( height: 12, ), child, ], ), child: GestureDetector( onTap: () { HapticFeedback.lightImpact(); clipboard.setData( ClipboardData( text: address, ), ); showFloatingFlushBar( type: FlushBarType.info, message: "Copied to clipboard", iconAsset: Assets.svg.copy, context: context, ); }, child: RoundedWhiteContainer( child: Column( children: [ Row( children: [ Text( "Your $ticker address", style: STextStyles.itemSubtitle(context), ), const Spacer(), Row( children: [ SvgPicture.asset( Assets.svg.copy, width: 10, height: 10, color: Theme.of(context) .extension()! .infoItemIcons, ), const SizedBox( width: 4, ), Text( "Copy", style: STextStyles.link2(context), ), ], ), ], ), const SizedBox( height: 4, ), Row( children: [ Expanded( child: Text( address, style: STextStyles.itemSubtitle12(context), ), ), ], ), ], ), ), ), ), const SizedBox( height: 12, ), PrimaryButton( label: "Copy address", onPressed: () { HapticFeedback.lightImpact(); clipboard.setData( ClipboardData( text: address, ), ); showFloatingFlushBar( type: FlushBarType.info, message: "Copied to clipboard", iconAsset: Assets.svg.copy, context: context, ); }, ), if (canGen) const SizedBox( height: 12, ), if (canGen) SecondaryButton( label: "Generate new address", onPressed: _supportsSpark && _walletAddressTypes[_currentIndex] == AddressType.spark ? generateNewSparkAddress : generateNewAddress, ), const SizedBox( height: 30, ), RoundedWhiteContainer( child: Padding( padding: const EdgeInsets.all(8.0), child: Center( child: Column( children: [ QR( data: AddressUtils.buildUriString( coin.uriScheme, address, {}, ), size: MediaQuery.of(context).size.width / 2, ), const SizedBox( height: 20, ), CustomTextButton( text: "Advanced options", onTap: () async { unawaited( Navigator.of(context).push( RouteGenerator.getRoute( shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, builder: (_) => GenerateUriQrCodeView( coin: coin, receivingAddress: address, ), settings: const RouteSettings( name: GenerateUriQrCodeView.routeName, ), ), ), ); }, ), ], ), ), ), ), ], ), ), ), ), ), ); } }