zwallet/lib/send.dart

342 lines
12 KiB
Dart
Raw Normal View History

2021-08-05 06:43:05 -07:00
import 'dart:io';
2021-06-26 07:30:12 -07:00
import 'package:barcode_scan/barcode_scan.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_masked_text/flutter_masked_text.dart';
2021-08-07 00:32:10 -07:00
import 'package:flutter_mobx/flutter_mobx.dart';
2021-08-06 00:53:54 -07:00
import 'package:mobx/mobx.dart';
2021-06-26 07:30:12 -07:00
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:warp_api/warp_api.dart';
2021-07-07 08:40:05 -07:00
import 'package:decimal/decimal.dart';
2021-08-05 06:43:05 -07:00
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
2021-07-07 08:40:05 -07:00
import 'dart:math' as math;
2021-06-26 07:30:12 -07:00
import 'main.dart';
2021-07-07 08:40:05 -07:00
import 'store.dart';
2021-08-15 09:18:09 -07:00
import 'generated/l10n.dart';
2021-06-26 07:30:12 -07:00
class SendPage extends StatefulWidget {
2021-07-18 08:59:02 -07:00
final Contact contact;
2021-08-06 00:53:54 -07:00
2021-07-18 08:59:02 -07:00
SendPage(this.contact);
2021-06-26 07:30:12 -07:00
@override
SendState createState() => SendState();
}
class SendState extends State<SendPage> {
final _formKey = GlobalKey<FormState>();
var _address = "";
2021-08-06 00:53:54 -07:00
var _amount = 0;
2021-07-07 08:40:05 -07:00
var _maxAmountPerNote = Decimal.zero;
2021-06-26 07:30:12 -07:00
var _balance = 0;
final _addressController = TextEditingController();
2021-07-09 22:44:34 -07:00
final _memoController = TextEditingController();
2021-08-06 00:53:54 -07:00
final _otherAmountController = TextEditingController();
2021-07-07 08:40:05 -07:00
var _mZEC = true;
2021-08-07 00:32:10 -07:00
var _useFX = false;
2021-07-07 08:40:05 -07:00
var _currencyController = _makeMoneyMaskedTextController(true);
var _maxAmountPerNoteController = _makeMoneyMaskedTextController(true);
var _includeFee = false;
var _isExpanded = false;
2021-08-06 19:18:20 -07:00
ReactionDisposer _priceAutorunDispose;
2021-06-26 07:30:12 -07:00
@override
initState() {
2021-07-18 08:59:02 -07:00
if (widget.contact != null)
_addressController.text = widget.contact.address;
2021-06-26 07:30:12 -07:00
Future.microtask(() async {
2021-07-07 08:40:05 -07:00
final balance = await accountManager
2021-07-09 06:33:39 -07:00
.getBalanceSpendable(syncStatus.latestHeight - settings.anchorOffset);
2021-06-26 07:30:12 -07:00
setState(() {
2021-07-07 08:40:05 -07:00
_balance = math.max(balance - DEFAULT_FEE, 0);
2021-06-26 07:30:12 -07:00
});
});
2021-08-06 00:53:54 -07:00
_updateOtherAmount();
2021-06-26 07:30:12 -07:00
super.initState();
2021-08-06 00:53:54 -07:00
2021-08-06 19:18:20 -07:00
_priceAutorunDispose = autorun((_) {
2021-08-06 00:53:54 -07:00
final price = priceStore.zecPrice;
_updateOtherAmount();
});
2021-06-26 07:30:12 -07:00
}
2021-08-06 19:18:20 -07:00
@override
void dispose() {
_priceAutorunDispose();
super.dispose();
}
2021-06-26 07:30:12 -07:00
@override
Widget build(BuildContext context) {
return Scaffold(
2021-08-15 09:18:09 -07:00
appBar: AppBar(title: Text(S.of(context).sendCointicker(coin.ticker))),
2021-07-07 08:40:05 -07:00
body: Form(
key: _formKey,
child: SingleChildScrollView(
padding: EdgeInsets.all(20),
child: Column(children: <Widget>[
Row(children: <Widget>[
Expanded(
child: TextFormField(
decoration:
2021-08-15 09:18:09 -07:00
InputDecoration(labelText: S.of(context).sendCointickerTo(coin.ticker)),
2021-07-07 08:40:05 -07:00
minLines: 4,
maxLines: null,
keyboardType: TextInputType.multiline,
controller: _addressController,
onSaved: _onAddress,
validator: _checkAddress,
),
2021-06-26 07:30:12 -07:00
),
2021-07-07 08:40:05 -07:00
IconButton(
icon: new Icon(MdiIcons.qrcodeScan), onPressed: _onScan)
]),
Row(children: [
Expanded(
child: TextFormField(
2021-08-06 00:53:54 -07:00
decoration:
InputDecoration(labelText: thisAmountLabel()),
2021-07-07 08:40:05 -07:00
keyboardType: TextInputType.number,
controller: _currencyController,
validator: _checkAmount,
2021-08-06 00:53:54 -07:00
onChanged: (_) { _updateOtherAmount(); },
2021-07-07 08:40:05 -07:00
onSaved: _onAmount)),
2021-08-15 09:18:09 -07:00
TextButton(child: Text(S.of(context).max), onPressed: _onMax),
2021-07-07 08:40:05 -07:00
]),
2021-08-06 00:53:54 -07:00
Row(children: [
Expanded(
child: TextFormField(
readOnly: true,
decoration: InputDecoration(
labelText: otherAmountLabel()),
controller: _otherAmountController,
),
),
]),
2021-07-07 08:40:05 -07:00
ExpansionPanelList(
expansionCallback: (_, isExpanded) {
setState(() {
_isExpanded = !isExpanded;
});
},
children: [
ExpansionPanel(
headerBuilder: (_, __) =>
2021-08-15 09:18:09 -07:00
ListTile(title: Text(S.of(context).advancedOptions)),
2021-07-07 08:40:05 -07:00
body: Column(children: [
2021-08-06 00:53:54 -07:00
ListTile(
title: TextFormField(
2021-08-15 09:18:09 -07:00
decoration: InputDecoration(labelText: S.of(context).memo),
2021-07-09 22:44:34 -07:00
minLines: 4,
maxLines: null,
keyboardType: TextInputType.multiline,
controller: _memoController,
)),
2021-07-07 08:40:05 -07:00
CheckboxListTile(
2021-08-15 09:18:09 -07:00
title: Text(S.of(context).roundToMillis),
2021-07-07 08:40:05 -07:00
value: _mZEC,
onChanged: _onChangedmZEC),
2021-08-07 00:32:10 -07:00
Observer(builder: (context) => CheckboxListTile(
2021-08-15 09:18:09 -07:00
title: Text(S.of(context).useSettingscurrency(settings.currency)),
2021-08-07 00:32:10 -07:00
value: _useFX,
onChanged: _onChangedUseFX)),
2021-07-07 08:40:05 -07:00
CheckboxListTile(
2021-08-15 09:18:09 -07:00
title: Text(S.of(context).includeFeeInAmount),
2021-07-07 08:40:05 -07:00
value: _includeFee,
onChanged: _onChangedIncludeFee),
ListTile(
title: TextFormField(
decoration: InputDecoration(
2021-08-15 09:18:09 -07:00
labelText: S.of(context).maxAmountPerNote),
2021-07-07 08:40:05 -07:00
keyboardType: TextInputType.number,
controller: _maxAmountPerNoteController,
validator: _checkMaxAmountPerNote,
onSaved: _onSavedMaxAmountPerNote,
2021-07-09 22:44:34 -07:00
)),
2021-07-07 08:40:05 -07:00
]),
isExpanded: _isExpanded)
]),
Padding(padding: EdgeInsets.all(8)),
2021-08-15 09:18:09 -07:00
Text(S.of(context).spendable + '${_balance / ZECUNIT} ${coin.ticker}'),
2021-08-13 20:44:53 -07:00
ButtonBar(
2021-08-15 09:18:09 -07:00
children: confirmButtons(context, _onSend, okLabel: S.of(context).send, okIcon: Icon(MdiIcons.send)))
2021-07-07 08:40:05 -07:00
]))));
2021-06-26 07:30:12 -07:00
}
2021-07-07 08:40:05 -07:00
String _checkAddress(String v) {
2021-08-15 09:18:09 -07:00
if (v.isEmpty) return S.of(context).addressIsEmpty;
if (!WarpApi.validAddress(v)) return S.of(context).invalidAddress;
2021-06-26 07:30:12 -07:00
return null;
}
2021-07-07 08:40:05 -07:00
String _checkAmount(String vs) {
2021-08-06 00:53:54 -07:00
final vss = vs.replaceAll(',', '');
final v = double.tryParse(vss);
2021-08-15 09:18:09 -07:00
if (v == null) return S.of(context).amountMustBeANumber;
if (v <= 0.0) return S.of(context).amountMustBePositive;
if (amountInZAT(Decimal.parse(vss)) > _balance) return S.of(context).notEnoughBalance;
2021-06-26 07:30:12 -07:00
return null;
}
2021-07-07 08:40:05 -07:00
String _checkMaxAmountPerNote(String vs) {
final v = double.tryParse(vs);
2021-08-15 09:18:09 -07:00
if (v == null) return S.of(context).amountMustBeANumber;
if (v < 0.0) return S.of(context).amountMustBePositive;
2021-07-07 08:40:05 -07:00
return null;
}
void _onMax() {
setState(() {
_mZEC = false;
2021-08-07 00:32:10 -07:00
_useFX = false;
2021-07-07 08:40:05 -07:00
_currencyController = _makeMoneyMaskedTextController(false);
_includeFee = false;
_currencyController.updateValue(
(Decimal.fromInt(_balance) / ZECUNIT_DECIMAL).toDouble());
2021-08-06 00:53:54 -07:00
_updateOtherAmount();
2021-07-07 08:40:05 -07:00
});
}
void _onChangedIncludeFee(bool v) {
setState(() {
_includeFee = v;
});
}
2021-08-07 00:32:10 -07:00
void _onChangedUseFX(bool v) {
2021-08-06 00:53:54 -07:00
setState(() {
2021-08-07 00:32:10 -07:00
_useFX = v;
2021-08-06 00:53:54 -07:00
_currencyController
.updateValue(double.parse(_otherAmountController.text));
_updateOtherAmount();
});
}
2021-07-07 08:40:05 -07:00
void _onChangedmZEC(bool v) {
setState(() {
_mZEC = v;
final amount = _currencyController.numberValue;
_currencyController = _makeMoneyMaskedTextController(v);
_currencyController.updateValue(amount);
_maxAmountPerNoteController = _makeMoneyMaskedTextController(v);
});
}
void _onScan() async {
2021-06-26 07:30:12 -07:00
var code = await BarcodeScanner.scan();
setState(() {
_address = code.rawContent;
_addressController.text = _address;
});
}
2021-08-06 00:53:54 -07:00
void _onAmount(String vs) {
final vss = vs.replaceAll(',', '');
_amount = amountInZAT(Decimal.parse(vss));
2021-06-26 07:30:12 -07:00
}
2021-07-07 08:40:05 -07:00
void _onAddress(v) {
2021-06-26 07:30:12 -07:00
_address = v;
}
2021-07-07 08:40:05 -07:00
void _onSavedMaxAmountPerNote(v) {
_maxAmountPerNote = Decimal.parse(v);
}
2021-08-06 00:53:54 -07:00
void _updateOtherAmount() {
2021-08-07 00:32:10 -07:00
final price = _fx().toDouble();
2021-08-06 00:53:54 -07:00
final amount = _currencyController.numberValue;
2021-08-07 00:32:10 -07:00
final otherAmount = (_useFX) ? (amount / price).toStringAsFixed(8) : (amount * price).toStringAsFixed(8);
2021-08-06 00:53:54 -07:00
_otherAmountController.text = otherAmount;
}
2021-07-07 08:40:05 -07:00
void _onSend() async {
2021-06-26 07:30:12 -07:00
final form = _formKey.currentState;
if (form == null) return;
if (form.validate()) {
form.save();
2021-08-06 00:53:54 -07:00
final aZEC = (Decimal.fromInt(_amount) / ZECUNIT_DECIMAL).toString();
2021-07-07 08:40:05 -07:00
final approved = await showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) => AlertDialog(
2021-08-15 09:18:09 -07:00
title: Text(S.of(context).pleaseConfirm),
2021-07-07 08:40:05 -07:00
content: SingleChildScrollView(
2021-08-15 09:18:09 -07:00
child: Text(S.of(context).sendingAzecCointickerToAddress(aZEC, coin.ticker, _address))),
actions: confirmButtons(context, () => Navigator.of(context).pop(true), okLabel: S.of(context).approve, cancelValue: false)
2021-07-07 08:40:05 -07:00
));
2021-06-26 07:30:12 -07:00
if (approved) {
Navigator.of(context).pop();
2021-08-15 09:18:09 -07:00
final snackBar1 = SnackBar(content: Text(S.of(context).preparingTransaction));
2021-06-26 07:30:12 -07:00
rootScaffoldMessengerKey.currentState.showSnackBar(snackBar1);
2021-08-06 00:53:54 -07:00
if (_includeFee) _amount -= DEFAULT_FEE;
2021-07-07 08:40:05 -07:00
int maxAmountPerNote = (_maxAmountPerNote * ZECUNIT_DECIMAL).toInt();
2021-07-09 22:44:34 -07:00
final memo = _memoController.text;
2021-06-26 07:30:12 -07:00
2021-08-05 06:43:05 -07:00
if (accountManager.canPay) {
final tx = await compute(
sendPayment,
PaymentParams(
accountManager.active.id,
_address,
2021-08-06 00:53:54 -07:00
_amount,
2021-08-05 06:43:05 -07:00
memo,
maxAmountPerNote,
settings.anchorOffset,
progressPort.sendPort));
2021-08-18 23:10:14 -07:00
final snackBar2 = SnackBar(content: Text("${S.of(context).txId}: $tx"));
2021-08-05 06:43:05 -07:00
rootScaffoldMessengerKey.currentState.showSnackBar(snackBar2);
2021-08-06 00:53:54 -07:00
} else {
2021-08-05 06:43:05 -07:00
Directory tempDir = await getTemporaryDirectory();
String filename = "${tempDir.path}/tx.json";
2021-08-07 00:32:10 -07:00
final msg = WarpApi.prepareTx(accountManager.active.id, _address, _amount, memo,
2021-08-05 06:43:05 -07:00
maxAmountPerNote, settings.anchorOffset, filename);
2021-08-15 09:18:09 -07:00
Share.shareFiles([filename], subject: S.of(context).unsignedTransactionFile);
2021-08-05 06:43:05 -07:00
2021-08-07 00:32:10 -07:00
final snackBar2 = SnackBar(content: Text(msg));
2021-08-05 06:43:05 -07:00
rootScaffoldMessengerKey.currentState.showSnackBar(snackBar2);
}
2021-06-26 07:30:12 -07:00
}
}
}
2021-07-07 08:40:05 -07:00
static MoneyMaskedTextController _makeMoneyMaskedTextController(bool mZEC) =>
MoneyMaskedTextController(
decimalSeparator: '.',
thousandSeparator: ',',
precision: mZEC ? 3 : 8);
2021-08-06 00:53:54 -07:00
2021-08-15 09:18:09 -07:00
String thisAmountLabel() => S.of(context).amountInSettingscurrency(_useFX ? settings.currency : coin.ticker);
2021-08-06 00:53:54 -07:00
2021-08-15 09:18:09 -07:00
String otherAmountLabel() => S.of(context).amountInSettingscurrency(_useFX ? coin.ticker : settings.currency);
2021-08-06 00:53:54 -07:00
2021-08-07 00:32:10 -07:00
int amountInZAT(Decimal v) => _useFX
? (v / _fx() * ZECUNIT_DECIMAL).toInt()
2021-08-06 00:53:54 -07:00
: (v * ZECUNIT_DECIMAL).toInt();
2021-08-07 00:32:10 -07:00
Decimal _fx() { return Decimal.parse("${priceStore.zecPrice}"); }
2021-06-26 07:30:12 -07:00
}
2021-07-09 22:44:34 -07:00
sendPayment(PaymentParams param) async {
param.port.send(0);
final tx = await WarpApi.sendPayment(
param.account,
param.address,
param.amount,
param.memo,
param.maxAmountPerNote,
param.anchorOffset, (percent) {
param.port.send(percent);
2021-07-07 08:40:05 -07:00
});
2021-07-09 22:44:34 -07:00
param.port.send(0);
2021-06-26 07:30:12 -07:00
return tx;
}