mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2025-01-10 20:54:33 +00:00
coin control select for sending
This commit is contained in:
parent
35c17033d1
commit
6d22304d7b
4 changed files with 448 additions and 328 deletions
|
@ -15,6 +15,7 @@ import 'package:stackwallet/widgets/background.dart';
|
|||
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
|
||||
import 'package:stackwallet/widgets/rounded_white_container.dart';
|
||||
import 'package:stackwallet/widgets/toggle.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
@ -30,6 +31,7 @@ class CoinControlView extends ConsumerStatefulWidget {
|
|||
required this.walletId,
|
||||
required this.type,
|
||||
this.requestedTotal,
|
||||
this.selectedUTXOs,
|
||||
}) : super(key: key);
|
||||
|
||||
static const routeName = "/coinControl";
|
||||
|
@ -37,6 +39,7 @@ class CoinControlView extends ConsumerStatefulWidget {
|
|||
final String walletId;
|
||||
final CoinControlViewType type;
|
||||
final int? requestedTotal;
|
||||
final Set<UTXO>? selectedUTXOs;
|
||||
|
||||
@override
|
||||
ConsumerState<CoinControlView> createState() => _CoinControlViewState();
|
||||
|
@ -48,6 +51,14 @@ class _CoinControlViewState extends ConsumerState<CoinControlView> {
|
|||
final Set<UTXO> _selectedAvailable = {};
|
||||
final Set<UTXO> _selectedBlocked = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
if (widget.selectedUTXOs != null) {
|
||||
_selectedAvailable.addAll(widget.selectedUTXOs!);
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
debugPrint("BUILD: $runtimeType");
|
||||
|
@ -69,13 +80,39 @@ class _CoinControlViewState extends ConsumerState<CoinControlView> {
|
|||
.idProperty()
|
||||
.findAllSync();
|
||||
|
||||
return Background(
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
Navigator.of(context).pop(
|
||||
widget.type == CoinControlViewType.use ? _selectedAvailable : null);
|
||||
return false;
|
||||
},
|
||||
child: Background(
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
|
||||
backgroundColor:
|
||||
Theme.of(context).extension<StackColors>()!.background,
|
||||
appBar: AppBar(
|
||||
leading: AppBarBackButton(
|
||||
leading: widget.type == CoinControlViewType.use &&
|
||||
_selectedAvailable.isNotEmpty
|
||||
? AppBarIconButton(
|
||||
icon: XIcon(
|
||||
width: 24,
|
||||
height: 24,
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.topNavIconPrimary,
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
setState(() {
|
||||
_selectedAvailable.clear();
|
||||
});
|
||||
},
|
||||
)
|
||||
: AppBarBackButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(
|
||||
widget.type == CoinControlViewType.use
|
||||
? _selectedAvailable
|
||||
: null);
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
|
@ -153,13 +190,20 @@ class _CoinControlViewState extends ConsumerState<CoinControlView> {
|
|||
.idEqualTo(ids[index])
|
||||
.findFirstSync()!;
|
||||
|
||||
final isSelected = _showBlocked
|
||||
? _selectedBlocked.contains(utxo)
|
||||
: _selectedAvailable.contains(utxo);
|
||||
|
||||
return UtxoCard(
|
||||
key: Key("${utxo.walletId}_${utxo.id}"),
|
||||
key: Key(
|
||||
"${utxo.walletId}_${utxo.id}_$isSelected"),
|
||||
walletId: widget.walletId,
|
||||
utxo: utxo,
|
||||
initialSelectedState: _showBlocked
|
||||
? _selectedBlocked.contains(utxo)
|
||||
: _selectedAvailable.contains(utxo),
|
||||
canSelect: widget.type ==
|
||||
CoinControlViewType.manage ||
|
||||
(widget.type == CoinControlViewType.use &&
|
||||
!_showBlocked),
|
||||
initialSelectedState: isSelected,
|
||||
onSelectedChanged: (value) {
|
||||
if (value) {
|
||||
_showBlocked
|
||||
|
@ -253,9 +297,12 @@ class _CoinControlViewState extends ConsumerState<CoinControlView> {
|
|||
child: Column(
|
||||
children: [
|
||||
RoundedWhiteContainer(
|
||||
padding: const EdgeInsets.all(0),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
|
@ -263,11 +310,16 @@ class _CoinControlViewState extends ConsumerState<CoinControlView> {
|
|||
"Selected amount",
|
||||
style: STextStyles.w600_14(context),
|
||||
),
|
||||
Builder(builder: (context) {
|
||||
int selectedSum = _selectedAvailable
|
||||
Builder(
|
||||
builder: (context) {
|
||||
int selectedSum =
|
||||
_selectedAvailable.isEmpty
|
||||
? 0
|
||||
: _selectedAvailable
|
||||
.map((e) => e.value)
|
||||
.reduce(
|
||||
(value, element) => value += element,
|
||||
(value, element) =>
|
||||
value += element,
|
||||
);
|
||||
return Text(
|
||||
"${Format.satoshisToAmount(
|
||||
|
@ -278,10 +330,10 @@ class _CoinControlViewState extends ConsumerState<CoinControlView> {
|
|||
)} ${coin.ticker}",
|
||||
style: widget.requestedTotal == null
|
||||
? STextStyles.w600_14(context)
|
||||
: STextStyles.w600_14(context)
|
||||
.copyWith(
|
||||
: STextStyles.w600_14(context).copyWith(
|
||||
color: selectedSum >=
|
||||
widget.requestedTotal!
|
||||
widget
|
||||
.requestedTotal!
|
||||
? Theme.of(context)
|
||||
.extension<
|
||||
StackColors>()!
|
||||
|
@ -291,24 +343,23 @@ class _CoinControlViewState extends ConsumerState<CoinControlView> {
|
|||
StackColors>()!
|
||||
.accentColorRed),
|
||||
);
|
||||
}),
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
if (widget.requestedTotal != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12,
|
||||
),
|
||||
child: Container(
|
||||
if (widget.requestedTotal != null)
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 2,
|
||||
height: 1.5,
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.backgroundAppBar,
|
||||
),
|
||||
),
|
||||
if (widget.requestedTotal != null)
|
||||
Row(
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
|
@ -327,6 +378,7 @@ class _CoinControlViewState extends ConsumerState<CoinControlView> {
|
|||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -335,27 +387,11 @@ class _CoinControlViewState extends ConsumerState<CoinControlView> {
|
|||
),
|
||||
PrimaryButton(
|
||||
label: "Use coins",
|
||||
enabled: _selectedAvailable.isNotEmpty,
|
||||
onPressed: () async {
|
||||
if (_showBlocked) {
|
||||
await MainDB.instance.putUTXOs(_selectedBlocked
|
||||
.map(
|
||||
(e) => e.copyWith(
|
||||
isBlocked: false,
|
||||
),
|
||||
)
|
||||
.toList());
|
||||
_selectedBlocked.clear();
|
||||
} else {
|
||||
await MainDB.instance.putUTXOs(_selectedAvailable
|
||||
.map(
|
||||
(e) => e.copyWith(
|
||||
isBlocked: true,
|
||||
),
|
||||
)
|
||||
.toList());
|
||||
_selectedAvailable.clear();
|
||||
}
|
||||
setState(() {});
|
||||
Navigator.of(context).pop(
|
||||
_selectedAvailable,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
@ -366,6 +402,7 @@ class _CoinControlViewState extends ConsumerState<CoinControlView> {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ class UtxoCard extends ConsumerStatefulWidget {
|
|||
required this.walletId,
|
||||
required this.onSelectedChanged,
|
||||
required this.initialSelectedState,
|
||||
required this.canSelect,
|
||||
this.onPressed,
|
||||
}) : super(key: key);
|
||||
|
||||
|
@ -29,6 +30,7 @@ class UtxoCard extends ConsumerStatefulWidget {
|
|||
final void Function(bool) onSelectedChanged;
|
||||
final bool initialSelectedState;
|
||||
final VoidCallback? onPressed;
|
||||
final bool canSelect;
|
||||
|
||||
@override
|
||||
ConsumerState<UtxoCard> createState() => _UtxoCardState();
|
||||
|
@ -90,12 +92,16 @@ class _UtxoCardState extends ConsumerState<UtxoCard> {
|
|||
: Colors.transparent,
|
||||
child: Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
ConditionalParent(
|
||||
condition: widget.canSelect,
|
||||
builder: (child) => GestureDetector(
|
||||
onTap: () {
|
||||
_selected = !_selected;
|
||||
widget.onSelectedChanged(_selected);
|
||||
setState(() {});
|
||||
},
|
||||
child: child,
|
||||
),
|
||||
child: SvgPicture.asset(
|
||||
_selected
|
||||
? Assets.svg.coinControl.selected
|
||||
|
|
|
@ -7,9 +7,11 @@ 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:stackwallet/models/isar/models/isar_models.dart';
|
||||
import 'package:stackwallet/models/paynym/paynym_account_lite.dart';
|
||||
import 'package:stackwallet/models/send_view_auto_fill_data.dart';
|
||||
import 'package:stackwallet/pages/address_book_views/address_book_view.dart';
|
||||
import 'package:stackwallet/pages/coin_control/coin_control_view.dart';
|
||||
import 'package:stackwallet/pages/send_view/confirm_transaction_view.dart';
|
||||
import 'package:stackwallet/pages/send_view/sub_widgets/building_transaction_dialog.dart';
|
||||
import 'package:stackwallet/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart';
|
||||
|
@ -43,9 +45,11 @@ import 'package:stackwallet/widgets/icon_widgets/addressbook_icon.dart';
|
|||
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
|
||||
import 'package:stackwallet/widgets/rounded_white_container.dart';
|
||||
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||
import 'package:stackwallet/widgets/textfield_icon_button.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
class SendView extends ConsumerStatefulWidget {
|
||||
const SendView({
|
||||
|
@ -104,6 +108,8 @@ class _SendViewState extends ConsumerState<SendView> {
|
|||
|
||||
Decimal? _cachedBalance;
|
||||
|
||||
Set<UTXO> selectedUTXOs = {};
|
||||
|
||||
void _cryptoAmountChanged() async {
|
||||
if (!_cryptoAmountChangeLock) {
|
||||
final String cryptoAmount = cryptoAmountController.text;
|
||||
|
@ -313,6 +319,8 @@ class _SendViewState extends ConsumerState<SendView> {
|
|||
Format.decimalAmountToSatoshis(manager.balance.getSpendable(), coin);
|
||||
}
|
||||
|
||||
if (!manager.hasCoinControlSupport ||
|
||||
(manager.hasCoinControlSupport && selectedUTXOs.isEmpty)) {
|
||||
// confirm send all
|
||||
if (amount == availableBalance) {
|
||||
final bool? shouldSendAll = await showDialog<bool>(
|
||||
|
@ -360,6 +368,7 @@ class _SendViewState extends ConsumerState<SendView> {
|
|||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
bool wasCancelled = false;
|
||||
|
@ -393,7 +402,12 @@ class _SendViewState extends ConsumerState<SendView> {
|
|||
txData = await wallet.preparePaymentCodeSend(
|
||||
paymentCode: paymentCode,
|
||||
satoshiAmount: amount,
|
||||
args: {"feeRate": feeRate},
|
||||
args: {
|
||||
"feeRate": feeRate,
|
||||
"UTXOs": (manager.hasCoinControlSupport && selectedUTXOs.isNotEmpty)
|
||||
? selectedUTXOs
|
||||
: null,
|
||||
},
|
||||
);
|
||||
} else if ((coin == Coin.firo || coin == Coin.firoTestNet) &&
|
||||
ref.read(publicPrivateBalanceStateProvider.state).state !=
|
||||
|
@ -407,7 +421,12 @@ class _SendViewState extends ConsumerState<SendView> {
|
|||
txData = await manager.prepareSend(
|
||||
address: _address!,
|
||||
satoshiAmount: amount,
|
||||
args: {"feeRate": ref.read(feeRateTypeStateProvider)},
|
||||
args: {
|
||||
"feeRate": ref.read(feeRateTypeStateProvider),
|
||||
"UTXOs": (manager.hasCoinControlSupport && selectedUTXOs.isNotEmpty)
|
||||
? selectedUTXOs
|
||||
: null,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -565,6 +584,12 @@ class _SendViewState extends ConsumerState<SendView> {
|
|||
final String locale = ref.watch(
|
||||
localeServiceChangeNotifierProvider.select((value) => value.locale));
|
||||
|
||||
final showCoinControl = ref.watch(
|
||||
walletsChangeNotifierProvider.select(
|
||||
(value) => value.getManager(walletId).hasCoinControlSupport,
|
||||
),
|
||||
);
|
||||
|
||||
if (coin == Coin.firo || coin == Coin.firoTestNet) {
|
||||
ref.listen(publicPrivateBalanceStateProvider, (previous, next) {
|
||||
if (_amountToSend == null) {
|
||||
|
@ -1484,6 +1509,56 @@ class _SendViewState extends ConsumerState<SendView> {
|
|||
),
|
||||
),
|
||||
),
|
||||
if (showCoinControl)
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
if (showCoinControl)
|
||||
RoundedWhiteContainer(
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Coin control",
|
||||
style:
|
||||
STextStyles.w500_14(context).copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textSubtitle1,
|
||||
),
|
||||
),
|
||||
CustomTextButton(
|
||||
text: selectedUTXOs.isEmpty
|
||||
? "Select coins"
|
||||
: "Selected coins (${selectedUTXOs.length})",
|
||||
onTap: () async {
|
||||
final result =
|
||||
await Navigator.of(context).pushNamed(
|
||||
CoinControlView.routeName,
|
||||
arguments: Tuple4(
|
||||
walletId,
|
||||
CoinControlViewType.use,
|
||||
_amountToSend != null
|
||||
? Format.decimalAmountToSatoshis(
|
||||
_amountToSend!,
|
||||
coin,
|
||||
)
|
||||
: null,
|
||||
selectedUTXOs,
|
||||
),
|
||||
);
|
||||
|
||||
if (result is Set<UTXO>) {
|
||||
setState(() {
|
||||
selectedUTXOs = result;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
|
|
|
@ -229,13 +229,15 @@ class RouteGenerator {
|
|||
name: settings.name,
|
||||
),
|
||||
);
|
||||
} else if (args is Tuple3<String, CoinControlViewType, int?>) {
|
||||
} else if (args
|
||||
is Tuple4<String, CoinControlViewType, int?, Set<UTXO>?>) {
|
||||
return getRoute(
|
||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
builder: (_) => CoinControlView(
|
||||
walletId: args.item1,
|
||||
type: args.item2,
|
||||
requestedTotal: args.item3,
|
||||
selectedUTXOs: args.item4,
|
||||
),
|
||||
settings: RouteSettings(
|
||||
name: settings.name,
|
||||
|
|
Loading…
Reference in a new issue