stack_wallet/lib/pages/receive_view/receive_view.dart

660 lines
24 KiB
Dart

/*
* 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<ReceiveView> createState() => _ReceiveViewState();
}
class _ReceiveViewState extends ConsumerState<ReceiveView> {
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<AddressType> _walletAddressTypes = [];
final Map<AddressType, String> _addressMap = {};
final Map<AddressType, StreamSubscription<Address?>> _addressSubMap = {};
Future<void> 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<StackColors>()!
.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<void> 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<StackColors>()!
.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<StackColors>()!.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<StackColors>()!.background,
icon: SvgPicture.asset(
Assets.svg.verticalEllipsis,
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
width: 20,
height: 20,
),
onPressed: () {
showDialog<dynamic>(
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<StackColors>()!
.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<StackColors>()!
.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<StackColors>()!
.infoItemLabel,
),
),
const SizedBox(
height: 10,
),
DropdownButtonHideUnderline(
child: DropdownButton2<int>(
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<StackColors>()!
.textFieldActiveSearchIconRight,
),
),
),
buttonStyleData: ButtonStyleData(
decoration: BoxDecoration(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
),
dropdownStyleData: DropdownStyleData(
offset: const Offset(0, -10),
elevation: 0,
decoration: BoxDecoration(
color: Theme.of(context)
.extension<StackColors>()!
.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<StackColors>()!
.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,
),
),
),
);
},
),
],
),
),
),
),
],
),
),
),
),
),
);
}
}