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/custom_buttons/app_bar_icon_button.dart';
|
||||||
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||||
import 'package:stackwallet/widgets/desktop/secondary_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/rounded_white_container.dart';
|
||||||
import 'package:stackwallet/widgets/toggle.dart';
|
import 'package:stackwallet/widgets/toggle.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
|
@ -30,6 +31,7 @@ class CoinControlView extends ConsumerStatefulWidget {
|
||||||
required this.walletId,
|
required this.walletId,
|
||||||
required this.type,
|
required this.type,
|
||||||
this.requestedTotal,
|
this.requestedTotal,
|
||||||
|
this.selectedUTXOs,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
static const routeName = "/coinControl";
|
static const routeName = "/coinControl";
|
||||||
|
@ -37,6 +39,7 @@ class CoinControlView extends ConsumerStatefulWidget {
|
||||||
final String walletId;
|
final String walletId;
|
||||||
final CoinControlViewType type;
|
final CoinControlViewType type;
|
||||||
final int? requestedTotal;
|
final int? requestedTotal;
|
||||||
|
final Set<UTXO>? selectedUTXOs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<CoinControlView> createState() => _CoinControlViewState();
|
ConsumerState<CoinControlView> createState() => _CoinControlViewState();
|
||||||
|
@ -48,6 +51,14 @@ class _CoinControlViewState extends ConsumerState<CoinControlView> {
|
||||||
final Set<UTXO> _selectedAvailable = {};
|
final Set<UTXO> _selectedAvailable = {};
|
||||||
final Set<UTXO> _selectedBlocked = {};
|
final Set<UTXO> _selectedBlocked = {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
if (widget.selectedUTXOs != null) {
|
||||||
|
_selectedAvailable.addAll(widget.selectedUTXOs!);
|
||||||
|
}
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
debugPrint("BUILD: $runtimeType");
|
debugPrint("BUILD: $runtimeType");
|
||||||
|
@ -69,300 +80,326 @@ class _CoinControlViewState extends ConsumerState<CoinControlView> {
|
||||||
.idProperty()
|
.idProperty()
|
||||||
.findAllSync();
|
.findAllSync();
|
||||||
|
|
||||||
return Background(
|
return WillPopScope(
|
||||||
child: Scaffold(
|
onWillPop: () async {
|
||||||
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
|
Navigator.of(context).pop(
|
||||||
appBar: AppBar(
|
widget.type == CoinControlViewType.use ? _selectedAvailable : null);
|
||||||
leading: AppBarBackButton(
|
return false;
|
||||||
onPressed: () {
|
},
|
||||||
Navigator.of(context).pop();
|
child: Background(
|
||||||
},
|
child: Scaffold(
|
||||||
),
|
backgroundColor:
|
||||||
title: Text(
|
Theme.of(context).extension<StackColors>()!.background,
|
||||||
"Coin control",
|
appBar: AppBar(
|
||||||
style: STextStyles.navBarTitle(context),
|
leading: widget.type == CoinControlViewType.use &&
|
||||||
),
|
_selectedAvailable.isNotEmpty
|
||||||
titleSpacing: 0,
|
? AppBarIconButton(
|
||||||
),
|
icon: XIcon(
|
||||||
body: SafeArea(
|
width: 24,
|
||||||
child: Column(
|
height: 24,
|
||||||
children: [
|
color: Theme.of(context)
|
||||||
Expanded(
|
.extension<StackColors>()!
|
||||||
child: Padding(
|
.topNavIconPrimary,
|
||||||
padding: const EdgeInsets.symmetric(
|
),
|
||||||
horizontal: 16,
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_selectedAvailable.clear();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: AppBarBackButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop(
|
||||||
|
widget.type == CoinControlViewType.use
|
||||||
|
? _selectedAvailable
|
||||||
|
: null);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
child: Column(
|
title: Text(
|
||||||
children: [
|
"Coin control",
|
||||||
const SizedBox(
|
style: STextStyles.navBarTitle(context),
|
||||||
height: 10,
|
),
|
||||||
),
|
titleSpacing: 0,
|
||||||
RoundedWhiteContainer(
|
),
|
||||||
child: Text(
|
body: SafeArea(
|
||||||
"This option allows you to control, freeze, and utilize "
|
child: Column(
|
||||||
"outputs at your discretion. Tap the output circle to "
|
children: [
|
||||||
"select.",
|
Expanded(
|
||||||
style: STextStyles.w500_14(context).copyWith(
|
child: Padding(
|
||||||
color: Theme.of(context)
|
padding: const EdgeInsets.symmetric(
|
||||||
.extension<StackColors>()!
|
horizontal: 16,
|
||||||
.textSubtitle1,
|
),
|
||||||
),
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
height: 10,
|
||||||
),
|
),
|
||||||
),
|
RoundedWhiteContainer(
|
||||||
const SizedBox(
|
child: Text(
|
||||||
height: 10,
|
"This option allows you to control, freeze, and utilize "
|
||||||
),
|
"outputs at your discretion. Tap the output circle to "
|
||||||
SizedBox(
|
"select.",
|
||||||
height: 48,
|
style: STextStyles.w500_14(context).copyWith(
|
||||||
child: Toggle(
|
color: Theme.of(context)
|
||||||
key: UniqueKey(),
|
.extension<StackColors>()!
|
||||||
onColor: Theme.of(context)
|
.textSubtitle1,
|
||||||
.extension<StackColors>()!
|
|
||||||
.popupBG,
|
|
||||||
onText: "Available outputs",
|
|
||||||
offColor: Theme.of(context)
|
|
||||||
.extension<StackColors>()!
|
|
||||||
.textFieldDefaultBG,
|
|
||||||
offText: "Frozen outputs",
|
|
||||||
isOn: _showBlocked,
|
|
||||||
onValueChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
_showBlocked = value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.transparent,
|
|
||||||
borderRadius: BorderRadius.circular(
|
|
||||||
Constants.size.circularBorderRadius,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(
|
||||||
const SizedBox(
|
height: 10,
|
||||||
height: 10,
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: ListView.separated(
|
|
||||||
itemCount: ids.length,
|
|
||||||
separatorBuilder: (context, _) => const SizedBox(
|
|
||||||
height: 10,
|
|
||||||
),
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final utxo = MainDB.instance.isar.utxos
|
|
||||||
.where()
|
|
||||||
.idEqualTo(ids[index])
|
|
||||||
.findFirstSync()!;
|
|
||||||
|
|
||||||
return UtxoCard(
|
|
||||||
key: Key("${utxo.walletId}_${utxo.id}"),
|
|
||||||
walletId: widget.walletId,
|
|
||||||
utxo: utxo,
|
|
||||||
initialSelectedState: _showBlocked
|
|
||||||
? _selectedBlocked.contains(utxo)
|
|
||||||
: _selectedAvailable.contains(utxo),
|
|
||||||
onSelectedChanged: (value) {
|
|
||||||
if (value) {
|
|
||||||
_showBlocked
|
|
||||||
? _selectedBlocked.add(utxo)
|
|
||||||
: _selectedAvailable.add(utxo);
|
|
||||||
} else {
|
|
||||||
_showBlocked
|
|
||||||
? _selectedBlocked.remove(utxo)
|
|
||||||
: _selectedAvailable.remove(utxo);
|
|
||||||
}
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
onPressed: () async {
|
|
||||||
final result =
|
|
||||||
await Navigator.of(context).pushNamed(
|
|
||||||
UtxoDetailsView.routeName,
|
|
||||||
arguments: Tuple2(
|
|
||||||
utxo.id,
|
|
||||||
widget.walletId,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (mounted && result == "refresh") {
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
SizedBox(
|
||||||
],
|
height: 48,
|
||||||
),
|
child: Toggle(
|
||||||
),
|
key: UniqueKey(),
|
||||||
),
|
onColor: Theme.of(context)
|
||||||
if (((_showBlocked && _selectedBlocked.isNotEmpty) ||
|
.extension<StackColors>()!
|
||||||
(!_showBlocked && _selectedAvailable.isNotEmpty)) &&
|
.popupBG,
|
||||||
widget.type == CoinControlViewType.manage)
|
onText: "Available outputs",
|
||||||
Container(
|
offColor: Theme.of(context)
|
||||||
decoration: BoxDecoration(
|
.extension<StackColors>()!
|
||||||
color: Theme.of(context)
|
.textFieldDefaultBG,
|
||||||
.extension<StackColors>()!
|
offText: "Frozen outputs",
|
||||||
.backgroundAppBar,
|
isOn: _showBlocked,
|
||||||
boxShadow: [
|
onValueChanged: (value) {
|
||||||
Theme.of(context)
|
setState(() {
|
||||||
.extension<StackColors>()!
|
_showBlocked = value;
|
||||||
.standardBoxShadow,
|
});
|
||||||
],
|
},
|
||||||
),
|
decoration: BoxDecoration(
|
||||||
child: Padding(
|
color: Colors.transparent,
|
||||||
padding: const EdgeInsets.all(16),
|
borderRadius: BorderRadius.circular(
|
||||||
child: SecondaryButton(
|
Constants.size.circularBorderRadius,
|
||||||
label: _showBlocked ? "Unfreeze" : "Freeze",
|
|
||||||
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(() {});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (!_showBlocked && widget.type == CoinControlViewType.use)
|
|
||||||
Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context)
|
|
||||||
.extension<StackColors>()!
|
|
||||||
.backgroundAppBar,
|
|
||||||
boxShadow: [
|
|
||||||
Theme.of(context)
|
|
||||||
.extension<StackColors>()!
|
|
||||||
.standardBoxShadow,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
RoundedWhiteContainer(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment:
|
|
||||||
MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"Selected amount",
|
|
||||||
style: STextStyles.w600_14(context),
|
|
||||||
),
|
|
||||||
Builder(builder: (context) {
|
|
||||||
int selectedSum = _selectedAvailable
|
|
||||||
.map((e) => e.value)
|
|
||||||
.reduce(
|
|
||||||
(value, element) => value += element,
|
|
||||||
);
|
|
||||||
return Text(
|
|
||||||
"${Format.satoshisToAmount(
|
|
||||||
selectedSum,
|
|
||||||
coin: coin,
|
|
||||||
).toStringAsFixed(
|
|
||||||
coin.decimals,
|
|
||||||
)} ${coin.ticker}",
|
|
||||||
style: widget.requestedTotal == null
|
|
||||||
? STextStyles.w600_14(context)
|
|
||||||
: STextStyles.w600_14(context)
|
|
||||||
.copyWith(
|
|
||||||
color: selectedSum >=
|
|
||||||
widget.requestedTotal!
|
|
||||||
? Theme.of(context)
|
|
||||||
.extension<
|
|
||||||
StackColors>()!
|
|
||||||
.accentColorGreen
|
|
||||||
: Theme.of(context)
|
|
||||||
.extension<
|
|
||||||
StackColors>()!
|
|
||||||
.accentColorRed),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
if (widget.requestedTotal != null)
|
),
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
vertical: 12,
|
|
||||||
),
|
|
||||||
child: Container(
|
|
||||||
width: double.infinity,
|
|
||||||
height: 2,
|
|
||||||
color: Theme.of(context)
|
|
||||||
.extension<StackColors>()!
|
|
||||||
.backgroundAppBar,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (widget.requestedTotal != null)
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment:
|
|
||||||
MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"Amount to send",
|
|
||||||
style: STextStyles.w600_14(context),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
"${Format.satoshisToAmount(
|
|
||||||
widget.requestedTotal!,
|
|
||||||
coin: coin,
|
|
||||||
).toStringAsFixed(
|
|
||||||
coin.decimals,
|
|
||||||
)} ${coin.ticker}",
|
|
||||||
style: STextStyles.w600_14(context),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 12,
|
height: 10,
|
||||||
),
|
),
|
||||||
PrimaryButton(
|
Expanded(
|
||||||
label: "Use coins",
|
child: ListView.separated(
|
||||||
onPressed: () async {
|
itemCount: ids.length,
|
||||||
if (_showBlocked) {
|
separatorBuilder: (context, _) => const SizedBox(
|
||||||
await MainDB.instance.putUTXOs(_selectedBlocked
|
height: 10,
|
||||||
.map(
|
),
|
||||||
(e) => e.copyWith(
|
itemBuilder: (context, index) {
|
||||||
isBlocked: false,
|
final utxo = MainDB.instance.isar.utxos
|
||||||
|
.where()
|
||||||
|
.idEqualTo(ids[index])
|
||||||
|
.findFirstSync()!;
|
||||||
|
|
||||||
|
final isSelected = _showBlocked
|
||||||
|
? _selectedBlocked.contains(utxo)
|
||||||
|
: _selectedAvailable.contains(utxo);
|
||||||
|
|
||||||
|
return UtxoCard(
|
||||||
|
key: Key(
|
||||||
|
"${utxo.walletId}_${utxo.id}_$isSelected"),
|
||||||
|
walletId: widget.walletId,
|
||||||
|
utxo: utxo,
|
||||||
|
canSelect: widget.type ==
|
||||||
|
CoinControlViewType.manage ||
|
||||||
|
(widget.type == CoinControlViewType.use &&
|
||||||
|
!_showBlocked),
|
||||||
|
initialSelectedState: isSelected,
|
||||||
|
onSelectedChanged: (value) {
|
||||||
|
if (value) {
|
||||||
|
_showBlocked
|
||||||
|
? _selectedBlocked.add(utxo)
|
||||||
|
: _selectedAvailable.add(utxo);
|
||||||
|
} else {
|
||||||
|
_showBlocked
|
||||||
|
? _selectedBlocked.remove(utxo)
|
||||||
|
: _selectedAvailable.remove(utxo);
|
||||||
|
}
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
onPressed: () async {
|
||||||
|
final result =
|
||||||
|
await Navigator.of(context).pushNamed(
|
||||||
|
UtxoDetailsView.routeName,
|
||||||
|
arguments: Tuple2(
|
||||||
|
utxo.id,
|
||||||
|
widget.walletId,
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
.toList());
|
if (mounted && result == "refresh") {
|
||||||
_selectedBlocked.clear();
|
setState(() {});
|
||||||
} else {
|
}
|
||||||
await MainDB.instance.putUTXOs(_selectedAvailable
|
},
|
||||||
.map(
|
);
|
||||||
(e) => e.copyWith(
|
},
|
||||||
isBlocked: true,
|
),
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList());
|
|
||||||
_selectedAvailable.clear();
|
|
||||||
}
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
if (((_showBlocked && _selectedBlocked.isNotEmpty) ||
|
||||||
|
(!_showBlocked && _selectedAvailable.isNotEmpty)) &&
|
||||||
|
widget.type == CoinControlViewType.manage)
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.backgroundAppBar,
|
||||||
|
boxShadow: [
|
||||||
|
Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.standardBoxShadow,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: SecondaryButton(
|
||||||
|
label: _showBlocked ? "Unfreeze" : "Freeze",
|
||||||
|
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(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!_showBlocked && widget.type == CoinControlViewType.use)
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.backgroundAppBar,
|
||||||
|
boxShadow: [
|
||||||
|
Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.standardBoxShadow,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
RoundedWhiteContainer(
|
||||||
|
padding: const EdgeInsets.all(0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Selected amount",
|
||||||
|
style: STextStyles.w600_14(context),
|
||||||
|
),
|
||||||
|
Builder(
|
||||||
|
builder: (context) {
|
||||||
|
int selectedSum =
|
||||||
|
_selectedAvailable.isEmpty
|
||||||
|
? 0
|
||||||
|
: _selectedAvailable
|
||||||
|
.map((e) => e.value)
|
||||||
|
.reduce(
|
||||||
|
(value, element) =>
|
||||||
|
value += element,
|
||||||
|
);
|
||||||
|
return Text(
|
||||||
|
"${Format.satoshisToAmount(
|
||||||
|
selectedSum,
|
||||||
|
coin: coin,
|
||||||
|
).toStringAsFixed(
|
||||||
|
coin.decimals,
|
||||||
|
)} ${coin.ticker}",
|
||||||
|
style: widget.requestedTotal == null
|
||||||
|
? STextStyles.w600_14(context)
|
||||||
|
: STextStyles.w600_14(context).copyWith(
|
||||||
|
color: selectedSum >=
|
||||||
|
widget
|
||||||
|
.requestedTotal!
|
||||||
|
? Theme.of(context)
|
||||||
|
.extension<
|
||||||
|
StackColors>()!
|
||||||
|
.accentColorGreen
|
||||||
|
: Theme.of(context)
|
||||||
|
.extension<
|
||||||
|
StackColors>()!
|
||||||
|
.accentColorRed),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (widget.requestedTotal != null)
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 1.5,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.backgroundAppBar,
|
||||||
|
),
|
||||||
|
if (widget.requestedTotal != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Amount to send",
|
||||||
|
style: STextStyles.w600_14(context),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"${Format.satoshisToAmount(
|
||||||
|
widget.requestedTotal!,
|
||||||
|
coin: coin,
|
||||||
|
).toStringAsFixed(
|
||||||
|
coin.decimals,
|
||||||
|
)} ${coin.ticker}",
|
||||||
|
style: STextStyles.w600_14(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 12,
|
||||||
|
),
|
||||||
|
PrimaryButton(
|
||||||
|
label: "Use coins",
|
||||||
|
enabled: _selectedAvailable.isNotEmpty,
|
||||||
|
onPressed: () async {
|
||||||
|
Navigator.of(context).pop(
|
||||||
|
_selectedAvailable,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -21,6 +21,7 @@ class UtxoCard extends ConsumerStatefulWidget {
|
||||||
required this.walletId,
|
required this.walletId,
|
||||||
required this.onSelectedChanged,
|
required this.onSelectedChanged,
|
||||||
required this.initialSelectedState,
|
required this.initialSelectedState,
|
||||||
|
required this.canSelect,
|
||||||
this.onPressed,
|
this.onPressed,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@ -29,6 +30,7 @@ class UtxoCard extends ConsumerStatefulWidget {
|
||||||
final void Function(bool) onSelectedChanged;
|
final void Function(bool) onSelectedChanged;
|
||||||
final bool initialSelectedState;
|
final bool initialSelectedState;
|
||||||
final VoidCallback? onPressed;
|
final VoidCallback? onPressed;
|
||||||
|
final bool canSelect;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<UtxoCard> createState() => _UtxoCardState();
|
ConsumerState<UtxoCard> createState() => _UtxoCardState();
|
||||||
|
@ -90,12 +92,16 @@ class _UtxoCardState extends ConsumerState<UtxoCard> {
|
||||||
: Colors.transparent,
|
: Colors.transparent,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
GestureDetector(
|
ConditionalParent(
|
||||||
onTap: () {
|
condition: widget.canSelect,
|
||||||
_selected = !_selected;
|
builder: (child) => GestureDetector(
|
||||||
widget.onSelectedChanged(_selected);
|
onTap: () {
|
||||||
setState(() {});
|
_selected = !_selected;
|
||||||
},
|
widget.onSelectedChanged(_selected);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
child: SvgPicture.asset(
|
child: SvgPicture.asset(
|
||||||
_selected
|
_selected
|
||||||
? Assets.svg.coinControl.selected
|
? Assets.svg.coinControl.selected
|
||||||
|
|
|
@ -7,9 +7,11 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.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/paynym/paynym_account_lite.dart';
|
||||||
import 'package:stackwallet/models/send_view_auto_fill_data.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/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/confirm_transaction_view.dart';
|
||||||
import 'package:stackwallet/pages/send_view/sub_widgets/building_transaction_dialog.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';
|
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/clipboard_icon.dart';
|
||||||
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
|
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
|
||||||
import 'package:stackwallet/widgets/icon_widgets/x_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_dialog.dart';
|
||||||
import 'package:stackwallet/widgets/stack_text_field.dart';
|
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||||
import 'package:stackwallet/widgets/textfield_icon_button.dart';
|
import 'package:stackwallet/widgets/textfield_icon_button.dart';
|
||||||
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
class SendView extends ConsumerStatefulWidget {
|
class SendView extends ConsumerStatefulWidget {
|
||||||
const SendView({
|
const SendView({
|
||||||
|
@ -104,6 +108,8 @@ class _SendViewState extends ConsumerState<SendView> {
|
||||||
|
|
||||||
Decimal? _cachedBalance;
|
Decimal? _cachedBalance;
|
||||||
|
|
||||||
|
Set<UTXO> selectedUTXOs = {};
|
||||||
|
|
||||||
void _cryptoAmountChanged() async {
|
void _cryptoAmountChanged() async {
|
||||||
if (!_cryptoAmountChangeLock) {
|
if (!_cryptoAmountChangeLock) {
|
||||||
final String cryptoAmount = cryptoAmountController.text;
|
final String cryptoAmount = cryptoAmountController.text;
|
||||||
|
@ -313,51 +319,54 @@ class _SendViewState extends ConsumerState<SendView> {
|
||||||
Format.decimalAmountToSatoshis(manager.balance.getSpendable(), coin);
|
Format.decimalAmountToSatoshis(manager.balance.getSpendable(), coin);
|
||||||
}
|
}
|
||||||
|
|
||||||
// confirm send all
|
if (!manager.hasCoinControlSupport ||
|
||||||
if (amount == availableBalance) {
|
(manager.hasCoinControlSupport && selectedUTXOs.isEmpty)) {
|
||||||
final bool? shouldSendAll = await showDialog<bool>(
|
// confirm send all
|
||||||
context: context,
|
if (amount == availableBalance) {
|
||||||
useSafeArea: false,
|
final bool? shouldSendAll = await showDialog<bool>(
|
||||||
barrierDismissible: true,
|
context: context,
|
||||||
builder: (context) {
|
useSafeArea: false,
|
||||||
return StackDialog(
|
barrierDismissible: true,
|
||||||
title: "Confirm send all",
|
builder: (context) {
|
||||||
message:
|
return StackDialog(
|
||||||
"You are about to send your entire balance. Would you like to continue?",
|
title: "Confirm send all",
|
||||||
leftButton: TextButton(
|
message:
|
||||||
style: Theme.of(context)
|
"You are about to send your entire balance. Would you like to continue?",
|
||||||
.extension<StackColors>()!
|
leftButton: TextButton(
|
||||||
.getSecondaryEnabledButtonStyle(context),
|
style: Theme.of(context)
|
||||||
child: Text(
|
.extension<StackColors>()!
|
||||||
"Cancel",
|
.getSecondaryEnabledButtonStyle(context),
|
||||||
style: STextStyles.button(context).copyWith(
|
child: Text(
|
||||||
color: Theme.of(context)
|
"Cancel",
|
||||||
.extension<StackColors>()!
|
style: STextStyles.button(context).copyWith(
|
||||||
.accentColorDark),
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.accentColorDark),
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop(false);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
onPressed: () {
|
rightButton: TextButton(
|
||||||
Navigator.of(context).pop(false);
|
style: Theme.of(context)
|
||||||
},
|
.extension<StackColors>()!
|
||||||
),
|
.getPrimaryEnabledButtonStyle(context),
|
||||||
rightButton: TextButton(
|
child: Text(
|
||||||
style: Theme.of(context)
|
"Yes",
|
||||||
.extension<StackColors>()!
|
style: STextStyles.button(context),
|
||||||
.getPrimaryEnabledButtonStyle(context),
|
),
|
||||||
child: Text(
|
onPressed: () {
|
||||||
"Yes",
|
Navigator.of(context).pop(true);
|
||||||
style: STextStyles.button(context),
|
},
|
||||||
),
|
),
|
||||||
onPressed: () {
|
);
|
||||||
Navigator.of(context).pop(true);
|
},
|
||||||
},
|
);
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (shouldSendAll == null || shouldSendAll == false) {
|
if (shouldSendAll == null || shouldSendAll == false) {
|
||||||
// cancel preview
|
// cancel preview
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -393,7 +402,12 @@ class _SendViewState extends ConsumerState<SendView> {
|
||||||
txData = await wallet.preparePaymentCodeSend(
|
txData = await wallet.preparePaymentCodeSend(
|
||||||
paymentCode: paymentCode,
|
paymentCode: paymentCode,
|
||||||
satoshiAmount: amount,
|
satoshiAmount: amount,
|
||||||
args: {"feeRate": feeRate},
|
args: {
|
||||||
|
"feeRate": feeRate,
|
||||||
|
"UTXOs": (manager.hasCoinControlSupport && selectedUTXOs.isNotEmpty)
|
||||||
|
? selectedUTXOs
|
||||||
|
: null,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
} else if ((coin == Coin.firo || coin == Coin.firoTestNet) &&
|
} else if ((coin == Coin.firo || coin == Coin.firoTestNet) &&
|
||||||
ref.read(publicPrivateBalanceStateProvider.state).state !=
|
ref.read(publicPrivateBalanceStateProvider.state).state !=
|
||||||
|
@ -407,7 +421,12 @@ class _SendViewState extends ConsumerState<SendView> {
|
||||||
txData = await manager.prepareSend(
|
txData = await manager.prepareSend(
|
||||||
address: _address!,
|
address: _address!,
|
||||||
satoshiAmount: amount,
|
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(
|
final String locale = ref.watch(
|
||||||
localeServiceChangeNotifierProvider.select((value) => value.locale));
|
localeServiceChangeNotifierProvider.select((value) => value.locale));
|
||||||
|
|
||||||
|
final showCoinControl = ref.watch(
|
||||||
|
walletsChangeNotifierProvider.select(
|
||||||
|
(value) => value.getManager(walletId).hasCoinControlSupport,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
if (coin == Coin.firo || coin == Coin.firoTestNet) {
|
if (coin == Coin.firo || coin == Coin.firoTestNet) {
|
||||||
ref.listen(publicPrivateBalanceStateProvider, (previous, next) {
|
ref.listen(publicPrivateBalanceStateProvider, (previous, next) {
|
||||||
if (_amountToSend == null) {
|
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(
|
const SizedBox(
|
||||||
height: 12,
|
height: 12,
|
||||||
),
|
),
|
||||||
|
|
|
@ -229,13 +229,15 @@ class RouteGenerator {
|
||||||
name: settings.name,
|
name: settings.name,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (args is Tuple3<String, CoinControlViewType, int?>) {
|
} else if (args
|
||||||
|
is Tuple4<String, CoinControlViewType, int?, Set<UTXO>?>) {
|
||||||
return getRoute(
|
return getRoute(
|
||||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||||
builder: (_) => CoinControlView(
|
builder: (_) => CoinControlView(
|
||||||
walletId: args.item1,
|
walletId: args.item1,
|
||||||
type: args.item2,
|
type: args.item2,
|
||||||
requestedTotal: args.item3,
|
requestedTotal: args.item3,
|
||||||
|
selectedUTXOs: args.item4,
|
||||||
),
|
),
|
||||||
settings: RouteSettings(
|
settings: RouteSettings(
|
||||||
name: settings.name,
|
name: settings.name,
|
||||||
|
|
Loading…
Reference in a new issue