refactor send screen address validation to take into account not being able to send from lelantus to spark directly

This commit is contained in:
julian 2023-12-29 18:12:13 -06:00
parent 97ff9ecf8b
commit 02cb79c6a3
4 changed files with 376 additions and 341 deletions

View file

@ -121,38 +121,173 @@ class _SendViewState extends ConsumerState<SendView> {
late final bool isStellar; late final bool isStellar;
late final bool isFiro; late final bool isFiro;
Amount? _amountToSend;
Amount? _cachedAmountToSend; Amount? _cachedAmountToSend;
String? _address; String? _address;
bool _addressToggleFlag = false; bool _addressToggleFlag = false;
bool _isSparkAddress = false;
bool _cryptoAmountChangeLock = false; bool _cryptoAmountChangeLock = false;
late VoidCallback onCryptoAmountChanged; late VoidCallback onCryptoAmountChanged;
Set<UTXO> selectedUTXOs = {}; Set<UTXO> selectedUTXOs = {};
Future<void> _scanQr() async {
try {
// ref
// .read(
// shouldShowLockscreenOnResumeStateProvider
// .state)
// .state = false;
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 75));
}
final qrResult = await scanner.scan();
// Future<void>.delayed(
// const Duration(seconds: 2),
// () => ref
// .read(
// shouldShowLockscreenOnResumeStateProvider
// .state)
// .state = true,
// );
Logging.instance.log("qrResult content: ${qrResult.rawContent}",
level: LogLevel.Info);
final results = AddressUtils.parseUri(qrResult.rawContent);
Logging.instance.log("qrResult parsed: $results", level: LogLevel.Info);
if (results.isNotEmpty && results["scheme"] == coin.uriScheme) {
// auto fill address
_address = (results["address"] ?? "").trim();
sendToController.text = _address!;
// autofill notes field
if (results["message"] != null) {
noteController.text = results["message"]!;
} else if (results["label"] != null) {
noteController.text = results["label"]!;
}
// autofill amount field
if (results["amount"] != null) {
final Amount amount = Decimal.parse(results["amount"]!).toAmount(
fractionDigits: coin.decimals,
);
cryptoAmountController.text = ref.read(pAmountFormatter(coin)).format(
amount,
withUnitName: false,
);
ref.read(pSendAmount.notifier).state = amount;
}
_setValidAddressProviders(_address);
setState(() {
_addressToggleFlag = sendToController.text.isNotEmpty;
});
// now check for non standard encoded basic address
} else if (ref
.read(pWallets)
.getWallet(walletId)
.cryptoCurrency
.validateAddress(qrResult.rawContent)) {
_address = qrResult.rawContent.trim();
sendToController.text = _address ?? "";
_setValidAddressProviders(_address);
setState(() {
_addressToggleFlag = sendToController.text.isNotEmpty;
});
}
} on PlatformException catch (e, s) {
// ref
// .read(
// shouldShowLockscreenOnResumeStateProvider
// .state)
// .state = true;
// here we ignore the exception caused by not giving permission
// to use the camera to scan a qr code
Logging.instance.log(
"Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s",
level: LogLevel.Warning);
}
}
void _fiatFieldChanged(String baseAmountString) {
final baseAmount = Amount.tryParseFiatString(
baseAmountString,
locale: ref.read(localeServiceChangeNotifierProvider).locale,
);
final Amount? amount;
if (baseAmount != null) {
final Decimal _price =
ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1;
if (_price == Decimal.zero) {
amount = 0.toAmountAsRaw(fractionDigits: coin.decimals);
} else {
amount = baseAmount <= Amount.zero
? 0.toAmountAsRaw(fractionDigits: coin.decimals)
: (baseAmount.decimal / _price)
.toDecimal(
scaleOnInfinitePrecision: coin.decimals,
)
.toAmount(fractionDigits: coin.decimals);
}
if (_cachedAmountToSend != null && _cachedAmountToSend == amount) {
return;
}
_cachedAmountToSend = amount;
Logging.instance
.log("it changed $amount $_cachedAmountToSend", level: LogLevel.Info);
final amountString = ref.read(pAmountFormatter(coin)).format(
amount,
withUnitName: false,
);
_cryptoAmountChangeLock = true;
cryptoAmountController.text = amountString;
_cryptoAmountChangeLock = false;
} else {
amount = 0.toAmountAsRaw(fractionDigits: coin.decimals);
_cryptoAmountChangeLock = true;
cryptoAmountController.text = "";
_cryptoAmountChangeLock = false;
}
// setState(() {
// _calculateFeesFuture = calculateFees(
// Format.decimalAmountToSatoshis(
// _amountToSend!));
// });
ref.read(pSendAmount.notifier).state = amount;
}
void _cryptoAmountChanged() async { void _cryptoAmountChanged() async {
if (!_cryptoAmountChangeLock) { if (!_cryptoAmountChangeLock) {
final cryptoAmount = ref.read(pAmountFormatter(coin)).tryParse( final cryptoAmount = ref.read(pAmountFormatter(coin)).tryParse(
cryptoAmountController.text, cryptoAmountController.text,
); );
final Amount? amount;
if (cryptoAmount != null) { if (cryptoAmount != null) {
_amountToSend = cryptoAmount; amount = cryptoAmount;
if (_cachedAmountToSend != null && if (_cachedAmountToSend != null && _cachedAmountToSend == amount) {
_cachedAmountToSend == _amountToSend) {
return; return;
} }
_cachedAmountToSend = _amountToSend; _cachedAmountToSend = amount;
Logging.instance.log("it changed $_amountToSend $_cachedAmountToSend", Logging.instance.log("it changed $amount $_cachedAmountToSend",
level: LogLevel.Info); level: LogLevel.Info);
final price = final price =
ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1; ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1;
if (price > Decimal.zero) { if (price > Decimal.zero) {
baseAmountController.text = (_amountToSend!.decimal * price) baseAmountController.text = (amount!.decimal * price)
.toAmount( .toAmount(
fractionDigits: 2, fractionDigits: 2,
) )
@ -161,20 +296,20 @@ class _SendViewState extends ConsumerState<SendView> {
); );
} }
} else { } else {
_amountToSend = null; amount = null;
baseAmountController.text = ""; baseAmountController.text = "";
} }
_updatePreviewButtonState(_address, _amountToSend); ref.read(pSendAmount.notifier).state = amount;
_cryptoAmountChangedFeeUpdateTimer?.cancel(); _cryptoAmountChangedFeeUpdateTimer?.cancel();
_cryptoAmountChangedFeeUpdateTimer = Timer(updateFeesTimerDuration, () { _cryptoAmountChangedFeeUpdateTimer = Timer(updateFeesTimerDuration, () {
if (coin != Coin.epicCash && !_baseFocus.hasFocus) { if (coin != Coin.epicCash && !_baseFocus.hasFocus) {
setState(() { setState(() {
_calculateFeesFuture = calculateFees( _calculateFeesFuture = calculateFees(
_amountToSend == null amount == null
? 0.toAmountAsRaw(fractionDigits: coin.decimals) ? 0.toAmountAsRaw(fractionDigits: coin.decimals)
: _amountToSend!, : amount!,
); );
}); });
} }
@ -193,9 +328,9 @@ class _SendViewState extends ConsumerState<SendView> {
if (coin != Coin.epicCash && !_cryptoFocus.hasFocus) { if (coin != Coin.epicCash && !_cryptoFocus.hasFocus) {
setState(() { setState(() {
_calculateFeesFuture = calculateFees( _calculateFeesFuture = calculateFees(
_amountToSend == null ref.read(pSendAmount) == null
? 0.toAmountAsRaw(fractionDigits: coin.decimals) ? 0.toAmountAsRaw(fractionDigits: coin.decimals)
: _amountToSend!, : ref.read(pSendAmount)!,
); );
}); });
} }
@ -230,6 +365,7 @@ class _SendViewState extends ConsumerState<SendView> {
if (_data != null && _data!.contactLabel == address) { if (_data != null && _data!.contactLabel == address) {
return null; return null;
} }
if (address.isNotEmpty && if (address.isNotEmpty &&
!ref !ref
.read(pWallets) .read(pWallets)
@ -241,24 +377,22 @@ class _SendViewState extends ConsumerState<SendView> {
return null; return null;
} }
void _updatePreviewButtonState(String? address, Amount? amount) { void _setValidAddressProviders(String? address) {
if (isPaynymSend) { if (isPaynymSend) {
ref.read(previewTxButtonStateProvider.state).state = ref.read(pValidSendToAddress.notifier).state = true;
(amount != null && amount > Amount.zero);
} else { } else {
final walletCurrency = final wallet = ref.read(pWallets).getWallet(walletId);
ref.read(pWallets).getWallet(walletId).cryptoCurrency; if (wallet is SparkInterface) {
final isValidAddress = walletCurrency.validateAddress(address ?? ""); ref.read(pValidSparkSendToAddress.notifier).state =
SparkInterface.validateSparkAddress(
address: address ?? "",
isTestNet:
wallet.cryptoCurrency.network == CryptoCurrencyNetwork.test,
);
}
_isSparkAddress = isValidAddress ref.read(pValidSendToAddress.notifier).state =
? SparkInterface.validateSparkAddress( wallet.cryptoCurrency.validateAddress(address ?? "");
address: address!,
isTestNet: walletCurrency.network == CryptoCurrencyNetwork.test,
)
: false;
ref.read(previewTxButtonStateProvider.state).state =
(isValidAddress && amount != null && amount > Amount.zero);
} }
} }
@ -392,7 +526,7 @@ class _SendViewState extends ConsumerState<SendView> {
); );
final wallet = ref.read(pWallets).getWallet(walletId); final wallet = ref.read(pWallets).getWallet(walletId);
final Amount amount = _amountToSend!; final Amount amount = ref.read(pSendAmount)!;
final Amount availableBalance; final Amount availableBalance;
if (isFiro) { if (isFiro) {
switch (ref.read(publicPrivateBalanceStateProvider.state).state) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) {
@ -524,7 +658,7 @@ class _SendViewState extends ConsumerState<SendView> {
} else if (wallet is FiroWallet) { } else if (wallet is FiroWallet) {
switch (ref.read(publicPrivateBalanceStateProvider.state).state) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) {
case FiroType.public: case FiroType.public:
if (_isSparkAddress) { if (ref.read(pValidSparkSendToAddress)) {
txDataFuture = wallet.prepareSparkMintTransaction( txDataFuture = wallet.prepareSparkMintTransaction(
txData: TxData( txData: TxData(
sparkRecipients: [ sparkRecipients: [
@ -570,10 +704,10 @@ class _SendViewState extends ConsumerState<SendView> {
case FiroType.spark: case FiroType.spark:
txDataFuture = wallet.prepareSendSpark( txDataFuture = wallet.prepareSendSpark(
txData: TxData( txData: TxData(
recipients: _isSparkAddress recipients: ref.read(pValidSparkSendToAddress)
? null ? null
: [(address: _address!, amount: amount)], : [(address: _address!, amount: amount)],
sparkRecipients: _isSparkAddress sparkRecipients: ref.read(pValidSparkSendToAddress)
? [ ? [
( (
address: _address!, address: _address!,
@ -807,7 +941,7 @@ class _SendViewState extends ConsumerState<SendView> {
if (isFiro) { if (isFiro) {
ref.listen(publicPrivateBalanceStateProvider, (previous, next) { ref.listen(publicPrivateBalanceStateProvider, (previous, next) {
if (_amountToSend == null) { if (ref.read(pSendAmount) == null) {
setState(() { setState(() {
_calculateFeesFuture = _calculateFeesFuture =
calculateFees(0.toAmountAsRaw(fractionDigits: coin.decimals)); calculateFees(0.toAmountAsRaw(fractionDigits: coin.decimals));
@ -815,7 +949,7 @@ class _SendViewState extends ConsumerState<SendView> {
} else { } else {
setState(() { setState(() {
_calculateFeesFuture = calculateFees( _calculateFeesFuture = calculateFees(
_amountToSend!, ref.read(pSendAmount)!,
); );
}); });
} }
@ -1077,8 +1211,7 @@ class _SendViewState extends ConsumerState<SendView> {
), ),
onChanged: (newValue) { onChanged: (newValue) {
_address = newValue.trim(); _address = newValue.trim();
_updatePreviewButtonState( _setValidAddressProviders(_address);
_address, _amountToSend);
setState(() { setState(() {
_addressToggleFlag = newValue.isNotEmpty; _addressToggleFlag = newValue.isNotEmpty;
@ -1115,9 +1248,8 @@ class _SendViewState extends ConsumerState<SendView> {
onTap: () { onTap: () {
sendToController.text = ""; sendToController.text = "";
_address = ""; _address = "";
_updatePreviewButtonState( _setValidAddressProviders(
_address, _address);
_amountToSend);
setState(() { setState(() {
_addressToggleFlag = _addressToggleFlag =
false; false;
@ -1159,9 +1291,8 @@ class _SendViewState extends ConsumerState<SendView> {
content.trim(); content.trim();
_address = content.trim(); _address = content.trim();
_updatePreviewButtonState( _setValidAddressProviders(
_address, _address);
_amountToSend);
setState(() { setState(() {
_addressToggleFlag = _addressToggleFlag =
sendToController sendToController
@ -1195,139 +1326,9 @@ class _SendViewState extends ConsumerState<SendView> {
"Scan QR Button. Opens Camera For Scanning QR Code.", "Scan QR Button. Opens Camera For Scanning QR Code.",
key: const Key( key: const Key(
"sendViewScanQrButtonKey"), "sendViewScanQrButtonKey"),
onTap: () async { onTap: _scanQr,
try {
// ref
// .read(
// shouldShowLockscreenOnResumeStateProvider
// .state)
// .state = false;
if (FocusScope.of(context)
.hasFocus) {
FocusScope.of(context)
.unfocus();
await Future<void>.delayed(
const Duration(
milliseconds: 75));
}
final qrResult =
await scanner.scan();
// Future<void>.delayed(
// const Duration(seconds: 2),
// () => ref
// .read(
// shouldShowLockscreenOnResumeStateProvider
// .state)
// .state = true,
// );
Logging.instance.log(
"qrResult content: ${qrResult.rawContent}",
level: LogLevel.Info);
final results =
AddressUtils.parseUri(
qrResult.rawContent);
Logging.instance.log(
"qrResult parsed: $results",
level: LogLevel.Info);
if (results.isNotEmpty &&
results["scheme"] ==
coin.uriScheme) {
// auto fill address
_address =
(results["address"] ??
"")
.trim();
sendToController.text =
_address!;
// autofill notes field
if (results["message"] !=
null) {
noteController.text =
results["message"]!;
} else if (results[
"label"] !=
null) {
noteController.text =
results["label"]!;
}
// autofill amount field
if (results["amount"] !=
null) {
final Amount amount =
Decimal.parse(results[
"amount"]!)
.toAmount(
fractionDigits:
coin.decimals,
);
cryptoAmountController
.text =
ref
.read(
pAmountFormatter(
coin))
.format(
amount,
withUnitName:
false,
);
_amountToSend = amount;
}
_updatePreviewButtonState(
_address,
_amountToSend);
setState(() {
_addressToggleFlag =
sendToController
.text.isNotEmpty;
});
// now check for non standard encoded basic address
} else if (ref
.read(pWallets)
.getWallet(walletId)
.cryptoCurrency
.validateAddress(qrResult
.rawContent)) {
_address = qrResult
.rawContent
.trim();
sendToController.text =
_address ?? "";
_updatePreviewButtonState(
_address,
_amountToSend);
setState(() {
_addressToggleFlag =
sendToController
.text.isNotEmpty;
});
}
} on PlatformException catch (e, s) {
// ref
// .read(
// shouldShowLockscreenOnResumeStateProvider
// .state)
// .state = true;
// here we ignore the exception caused by not giving permission
// to use the camera to scan a qr code
Logging.instance.log(
"Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s",
level: LogLevel.Warning);
}
},
child: const QrCodeIcon(), child: const QrCodeIcon(),
) ),
], ],
), ),
), ),
@ -1338,7 +1339,11 @@ class _SendViewState extends ConsumerState<SendView> {
const SizedBox( const SizedBox(
height: 10, height: 10,
), ),
if (isStellar || _isSparkAddress) if (isStellar ||
(ref.watch(pValidSparkSendToAddress) &&
ref.watch(
publicPrivateBalanceStateProvider) !=
FiroType.lelantus))
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular( borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius, Constants.size.circularBorderRadius,
@ -1419,9 +1424,50 @@ class _SendViewState extends ConsumerState<SendView> {
), ),
Builder( Builder(
builder: (_) { builder: (_) {
final error = _updateInvalidAddressText( final String? error;
_address ?? "",
); if (_address == null || _address!.isEmpty) {
error = null;
} else if (isFiro) {
if (ref.watch(
publicPrivateBalanceStateProvider) ==
FiroType.lelantus) {
if (_data != null &&
_data!.contactLabel == _address) {
error = SparkInterface.validateSparkAddress(
address: _data!.address,
isTestNet: coin.isTestNet)
? "Unsupported"
: null;
} else if (ref
.watch(pValidSparkSendToAddress)) {
error = "Unsupported";
} else {
error = ref.watch(pValidSendToAddress)
? null
: "Invalid address";
}
} else {
if (_data != null &&
_data!.contactLabel == _address) {
error = null;
} else if (!ref.watch(pValidSendToAddress) &&
!ref.watch(pValidSparkSendToAddress)) {
error = "Invalid address";
} else {
error = null;
}
}
} else {
if (_data != null &&
_data!.contactLabel == _address) {
error = null;
} else if (!ref.watch(pValidSendToAddress)) {
error = "Invalid address";
} else {
error = null;
}
}
if (error == null || error.isEmpty) { if (error == null || error.isEmpty) {
return Container(); return Container();
@ -1737,65 +1783,7 @@ class _SendViewState extends ConsumerState<SendView> {
// ? newValue // ? newValue
// : oldValue), // : oldValue),
], ],
onChanged: (baseAmountString) { onChanged: _fiatFieldChanged,
final baseAmount = Amount.tryParseFiatString(
baseAmountString,
locale: locale,
);
if (baseAmount != null) {
final Decimal _price = ref
.read(priceAnd24hChangeNotifierProvider)
.getPrice(coin)
.item1;
if (_price == Decimal.zero) {
_amountToSend = 0.toAmountAsRaw(
fractionDigits: coin.decimals);
} else {
_amountToSend = baseAmount <= Amount.zero
? 0.toAmountAsRaw(
fractionDigits: coin.decimals)
: (baseAmount.decimal / _price)
.toDecimal(
scaleOnInfinitePrecision:
coin.decimals,
)
.toAmount(
fractionDigits: coin.decimals);
}
if (_cachedAmountToSend != null &&
_cachedAmountToSend == _amountToSend) {
return;
}
_cachedAmountToSend = _amountToSend;
Logging.instance.log(
"it changed $_amountToSend $_cachedAmountToSend",
level: LogLevel.Info);
final amountString =
ref.read(pAmountFormatter(coin)).format(
_amountToSend!,
withUnitName: false,
);
_cryptoAmountChangeLock = true;
cryptoAmountController.text = amountString;
_cryptoAmountChangeLock = false;
} else {
_amountToSend = 0.toAmountAsRaw(
fractionDigits: coin.decimals);
_cryptoAmountChangeLock = true;
cryptoAmountController.text = "";
_cryptoAmountChangeLock = false;
}
// setState(() {
// _calculateFeesFuture = calculateFees(
// Format.decimalAmountToSatoshis(
// _amountToSend!));
// });
_updatePreviewButtonState(
_address, _amountToSend);
},
decoration: InputDecoration( decoration: InputDecoration(
contentPadding: const EdgeInsets.only( contentPadding: const EdgeInsets.only(
top: 12, top: 12,
@ -1860,8 +1848,8 @@ class _SendViewState extends ConsumerState<SendView> {
.spendable; .spendable;
Amount? amount; Amount? amount;
if (_amountToSend != null) { if (ref.read(pSendAmount) != null) {
amount = _amountToSend!; amount = ref.read(pSendAmount)!;
if (spendable == amount) { if (spendable == amount) {
// this is now a send all // this is now a send all
@ -2075,7 +2063,8 @@ class _SendViewState extends ConsumerState<SendView> {
amount: (Decimal.tryParse( amount: (Decimal.tryParse(
cryptoAmountController cryptoAmountController
.text) ?? .text) ??
_amountToSend ref
.watch(pSendAmount)
?.decimal ?? ?.decimal ??
Decimal.zero) Decimal.zero)
.toAmount( .toAmount(
@ -2239,14 +2228,10 @@ class _SendViewState extends ConsumerState<SendView> {
height: 12, height: 12,
), ),
TextButton( TextButton(
onPressed: ref onPressed: ref.watch(pPreviewTxButtonEnabled(coin))
.watch(previewTxButtonStateProvider.state)
.state
? _previewTransaction ? _previewTransaction
: null, : null,
style: ref style: ref.watch(pPreviewTxButtonEnabled(coin))
.watch(previewTxButtonStateProvider.state)
.state
? Theme.of(context) ? Theme.of(context)
.extension<StackColors>()! .extension<StackColors>()!
.getPrimaryEnabledButtonStyle(context) .getPrimaryEnabledButtonStyle(context)

View file

@ -348,7 +348,7 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
.getWallet(walletId) .getWallet(walletId)
.cryptoCurrency .cryptoCurrency
.validateAddress(address ?? ""); .validateAddress(address ?? "");
ref.read(previewTxButtonStateProvider.state).state = ref.read(previewTokenTxButtonStateProvider.state).state =
(isValidAddress && amount != null && amount > Amount.zero); (isValidAddress && amount != null && amount > Amount.zero);
} }
@ -1227,12 +1227,14 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
), ),
TextButton( TextButton(
onPressed: ref onPressed: ref
.watch(previewTxButtonStateProvider.state) .watch(
previewTokenTxButtonStateProvider.state)
.state .state
? _previewTransaction ? _previewTransaction
: null, : null,
style: ref style: ref
.watch(previewTxButtonStateProvider.state) .watch(
previewTokenTxButtonStateProvider.state)
.state .state
? Theme.of(context) ? Theme.of(context)
.extension<StackColors>()! .extension<StackColors>()!

View file

@ -114,7 +114,6 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
String? _note; String? _note;
String? _onChainNote; String? _onChainNote;
Amount? _amountToSend;
Amount? _cachedAmountToSend; Amount? _cachedAmountToSend;
String? _address; String? _address;
@ -125,8 +124,6 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
bool get isPaynymSend => widget.accountLite != null; bool get isPaynymSend => widget.accountLite != null;
bool _isSparkAddress = false;
bool isCustomFee = false; bool isCustomFee = false;
int customFeeRate = 1; int customFeeRate = 1;
(FeeRateType, String?, String?)? feeSelectionResult; (FeeRateType, String?, String?)? feeSelectionResult;
@ -141,7 +138,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
Future<void> previewSend() async { Future<void> previewSend() async {
final wallet = ref.read(pWallets).getWallet(walletId); final wallet = ref.read(pWallets).getWallet(walletId);
final Amount amount = _amountToSend!; final Amount amount = ref.read(pSendAmount)!;
final Amount availableBalance; final Amount availableBalance;
if ((coin == Coin.firo || coin == Coin.firoTestNet)) { if ((coin == Coin.firo || coin == Coin.firoTestNet)) {
switch (ref.read(publicPrivateBalanceStateProvider.state).state) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) {
@ -321,7 +318,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
} else if (wallet is FiroWallet) { } else if (wallet is FiroWallet) {
switch (ref.read(publicPrivateBalanceStateProvider.state).state) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) {
case FiroType.public: case FiroType.public:
if (_isSparkAddress) { if (ref.read(pValidSparkSendToAddress)) {
txDataFuture = wallet.prepareSparkMintTransaction( txDataFuture = wallet.prepareSparkMintTransaction(
txData: TxData( txData: TxData(
sparkRecipients: [ sparkRecipients: [
@ -367,10 +364,10 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
case FiroType.spark: case FiroType.spark:
txDataFuture = wallet.prepareSendSpark( txDataFuture = wallet.prepareSendSpark(
txData: TxData( txData: TxData(
recipients: _isSparkAddress recipients: ref.read(pValidSparkSendToAddress)
? null ? null
: [(address: _address!, amount: amount)], : [(address: _address!, amount: amount)],
sparkRecipients: _isSparkAddress sparkRecipients: ref.read(pValidSparkSendToAddress)
? [ ? [
( (
address: _address!, address: _address!,
@ -533,21 +530,21 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
final cryptoAmount = ref.read(pAmountFormatter(coin)).tryParse( final cryptoAmount = ref.read(pAmountFormatter(coin)).tryParse(
cryptoAmountController.text, cryptoAmountController.text,
); );
final Amount? amount;
if (cryptoAmount != null) { if (cryptoAmount != null) {
_amountToSend = cryptoAmount; amount = cryptoAmount;
if (_cachedAmountToSend != null && if (_cachedAmountToSend != null && _cachedAmountToSend == amount) {
_cachedAmountToSend == _amountToSend) {
return; return;
} }
Logging.instance.log("it changed $_amountToSend $_cachedAmountToSend", Logging.instance.log("it changed $amount $_cachedAmountToSend",
level: LogLevel.Info); level: LogLevel.Info);
_cachedAmountToSend = _amountToSend; _cachedAmountToSend = amount;
final price = final price =
ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1; ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1;
if (price > Decimal.zero) { if (price > Decimal.zero) {
final String fiatAmountString = (_amountToSend!.decimal * price) final String fiatAmountString = (amount!.decimal * price)
.toAmount(fractionDigits: 2) .toAmount(fractionDigits: 2)
.fiatString( .fiatString(
locale: ref.read(localeServiceChangeNotifierProvider).locale, locale: ref.read(localeServiceChangeNotifierProvider).locale,
@ -556,52 +553,29 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
baseAmountController.text = fiatAmountString; baseAmountController.text = fiatAmountString;
} }
} else { } else {
_amountToSend = null; amount = null;
_cachedAmountToSend = null; _cachedAmountToSend = null;
baseAmountController.text = ""; baseAmountController.text = "";
} }
_updatePreviewButtonState(_address, _amountToSend); ref.read(pSendAmount.notifier).state = amount;
} }
} }
String? _updateInvalidAddressText(String address) { // String? _updateInvalidAddressText(String address) {
if (_data != null && _data!.contactLabel == address) { // if (_data != null && _data!.contactLabel == address) {
return null; // return null;
} // }
if (address.isNotEmpty && // if (address.isNotEmpty &&
!ref // !ref
.read(pWallets) // .read(pWallets)
.getWallet(walletId) // .getWallet(walletId)
.cryptoCurrency // .cryptoCurrency
.validateAddress(address)) { // .validateAddress(address)) {
return "Invalid address"; // return "Invalid address";
} // }
return null; // return null;
} // }
void _updatePreviewButtonState(String? address, Amount? amount) {
if (isPaynymSend) {
ref.read(previewTxButtonStateProvider.state).state =
(amount != null && amount > Amount.zero);
} else {
final walletCurrency =
ref.read(pWallets).getWallet(walletId).cryptoCurrency;
final isValidAddress = walletCurrency.validateAddress(address ?? "");
_isSparkAddress = isValidAddress &&
ref.read(publicPrivateBalanceStateProvider.state).state !=
FiroType.lelantus
? SparkInterface.validateSparkAddress(
address: address!,
isTestNet: walletCurrency.network == CryptoCurrencyNetwork.test,
)
: false;
ref.read(previewTxButtonStateProvider.state).state =
(isValidAddress && amount != null && amount > Amount.zero);
}
}
Future<void> scanQr() async { Future<void> scanQr() async {
try { try {
@ -639,10 +613,9 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
cryptoAmountController.text = ref cryptoAmountController.text = ref
.read(pAmountFormatter(coin)) .read(pAmountFormatter(coin))
.format(amount, withUnitName: false); .format(amount, withUnitName: false);
_amountToSend = amount; ref.read(pSendAmount.notifier).state = amount;
} }
_updatePreviewButtonState(_address, _amountToSend);
setState(() { setState(() {
_addressToggleFlag = sendToController.text.isNotEmpty; _addressToggleFlag = sendToController.text.isNotEmpty;
}); });
@ -656,7 +629,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
_address = qrResult.rawContent; _address = qrResult.rawContent;
sendToController.text = _address ?? ""; sendToController.text = _address ?? "";
_updatePreviewButtonState(_address, _amountToSend); _setValidAddressProviders(_address);
setState(() { setState(() {
_addressToggleFlag = sendToController.text.isNotEmpty; _addressToggleFlag = sendToController.text.isNotEmpty;
}); });
@ -670,6 +643,25 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
} }
} }
void _setValidAddressProviders(String? address) {
if (isPaynymSend) {
ref.read(pValidSendToAddress.notifier).state = true;
} else {
final wallet = ref.read(pWallets).getWallet(walletId);
if (wallet is SparkInterface) {
ref.read(pValidSparkSendToAddress.notifier).state =
SparkInterface.validateSparkAddress(
address: address ?? "",
isTestNet:
wallet.cryptoCurrency.network == CryptoCurrencyNetwork.test,
);
}
ref.read(pValidSendToAddress.notifier).state =
wallet.cryptoCurrency.validateAddress(address ?? "");
}
}
Future<void> pasteAddress() async { Future<void> pasteAddress() async {
final ClipboardData? data = await clipboard.getData(Clipboard.kTextPlain); final ClipboardData? data = await clipboard.getData(Clipboard.kTextPlain);
if (data?.text != null && data!.text!.isNotEmpty) { if (data?.text != null && data!.text!.isNotEmpty) {
@ -686,7 +678,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
sendToController.text = content; sendToController.text = content;
_address = content; _address = content;
_updatePreviewButtonState(_address, _amountToSend); _setValidAddressProviders(_address);
setState(() { setState(() {
_addressToggleFlag = sendToController.text.isNotEmpty; _addressToggleFlag = sendToController.text.isNotEmpty;
}); });
@ -715,28 +707,29 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
baseAmountString, baseAmountString,
locale: ref.read(localeServiceChangeNotifierProvider).locale, locale: ref.read(localeServiceChangeNotifierProvider).locale,
); );
final Amount? amount;
if (baseAmount != null) { if (baseAmount != null) {
final _price = final _price =
ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1; ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1;
if (_price == Decimal.zero) { if (_price == Decimal.zero) {
_amountToSend = Decimal.zero.toAmount(fractionDigits: coin.decimals); amount = Decimal.zero.toAmount(fractionDigits: coin.decimals);
} else { } else {
_amountToSend = baseAmount <= Amount.zero amount = baseAmount <= Amount.zero
? Decimal.zero.toAmount(fractionDigits: coin.decimals) ? Decimal.zero.toAmount(fractionDigits: coin.decimals)
: (baseAmount.decimal / _price) : (baseAmount.decimal / _price)
.toDecimal(scaleOnInfinitePrecision: coin.decimals) .toDecimal(scaleOnInfinitePrecision: coin.decimals)
.toAmount(fractionDigits: coin.decimals); .toAmount(fractionDigits: coin.decimals);
} }
if (_cachedAmountToSend != null && _cachedAmountToSend == _amountToSend) { if (_cachedAmountToSend != null && _cachedAmountToSend == amount) {
return; return;
} }
_cachedAmountToSend = _amountToSend; _cachedAmountToSend = amount;
Logging.instance.log("it changed $_amountToSend $_cachedAmountToSend", Logging.instance
level: LogLevel.Info); .log("it changed $amount $_cachedAmountToSend", level: LogLevel.Info);
final amountString = ref.read(pAmountFormatter(coin)).format( final amountString = ref.read(pAmountFormatter(coin)).format(
_amountToSend!, amount!,
withUnitName: false, withUnitName: false,
); );
@ -744,7 +737,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
cryptoAmountController.text = amountString; cryptoAmountController.text = amountString;
_cryptoAmountChangeLock = false; _cryptoAmountChangeLock = false;
} else { } else {
_amountToSend = Decimal.zero.toAmount(fractionDigits: coin.decimals); amount = Decimal.zero.toAmount(fractionDigits: coin.decimals);
_cryptoAmountChangeLock = true; _cryptoAmountChangeLock = true;
cryptoAmountController.text = ""; cryptoAmountController.text = "";
_cryptoAmountChangeLock = false; _cryptoAmountChangeLock = false;
@ -754,7 +747,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
// Format.decimalAmountToSatoshis( // Format.decimalAmountToSatoshis(
// _amountToSend!)); // _amountToSend!));
// }); // });
_updatePreviewButtonState(_address, _amountToSend); ref.read(pSendAmount.notifier).state = amount;
} }
Future<void> sendAllTapped() async { Future<void> sendAllTapped() async {
@ -784,11 +777,12 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
} }
void _showDesktopCoinControl() async { void _showDesktopCoinControl() async {
final amount = ref.read(pSendAmount);
await showDialog<void>( await showDialog<void>(
context: context, context: context,
builder: (context) => DesktopCoinControlUseDialog( builder: (context) => DesktopCoinControlUseDialog(
walletId: widget.walletId, walletId: widget.walletId,
amountToSend: _amountToSend, amountToSend: amount,
), ),
); );
} }
@ -797,7 +791,8 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
void initState() { void initState() {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
ref.refresh(feeSheetSessionCacheProvider); ref.refresh(feeSheetSessionCacheProvider);
ref.read(previewTxButtonStateProvider.state).state = false; ref.read(pValidSendToAddress.state).state = false;
ref.read(pValidSparkSendToAddress.state).state = false;
}); });
// _calculateFeesFuture = calculateFees(0); // _calculateFeesFuture = calculateFees(0);
@ -832,20 +827,20 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
_cryptoFocus.addListener(() { _cryptoFocus.addListener(() {
if (!_cryptoFocus.hasFocus && !_baseFocus.hasFocus) { if (!_cryptoFocus.hasFocus && !_baseFocus.hasFocus) {
if (_amountToSend == null) { if (ref.read(pSendAmount) == null) {
ref.refresh(sendAmountProvider); ref.refresh(sendAmountProvider);
} else { } else {
ref.read(sendAmountProvider.state).state = _amountToSend!; ref.read(sendAmountProvider.state).state = ref.read(pSendAmount)!;
} }
} }
}); });
_baseFocus.addListener(() { _baseFocus.addListener(() {
if (!_cryptoFocus.hasFocus && !_baseFocus.hasFocus) { if (!_cryptoFocus.hasFocus && !_baseFocus.hasFocus) {
if (_amountToSend == null) { if (ref.read(pSendAmount) == null) {
ref.refresh(sendAmountProvider); ref.refresh(sendAmountProvider);
} else { } else {
ref.read(sendAmountProvider.state).state = _amountToSend!; ref.read(sendAmountProvider.state).state = ref.read(pSendAmount)!;
} }
} }
}); });
@ -1263,7 +1258,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
), ),
onChanged: (newValue) { onChanged: (newValue) {
_address = newValue; _address = newValue;
_updatePreviewButtonState(_address, _amountToSend); _setValidAddressProviders(_address);
setState(() { setState(() {
_addressToggleFlag = newValue.isNotEmpty; _addressToggleFlag = newValue.isNotEmpty;
@ -1303,8 +1298,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
onTap: () { onTap: () {
sendToController.text = ""; sendToController.text = "";
_address = ""; _address = "";
_updatePreviewButtonState( _setValidAddressProviders(_address);
_address, _amountToSend);
setState(() { setState(() {
_addressToggleFlag = false; _addressToggleFlag = false;
}); });
@ -1365,10 +1359,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
_address = entry.address; _address = entry.address;
_updatePreviewButtonState( _setValidAddressProviders(_address);
_address,
_amountToSend,
);
setState(() { setState(() {
_addressToggleFlag = true; _addressToggleFlag = true;
@ -1393,9 +1384,44 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
if (!isPaynymSend) if (!isPaynymSend)
Builder( Builder(
builder: (_) { builder: (_) {
final error = _updateInvalidAddressText( final String? error;
_address ?? "",
); if (_address == null || _address!.isEmpty) {
error = null;
} else if (coin == Coin.firo || coin == Coin.firoTestNet) {
if (ref.watch(publicPrivateBalanceStateProvider) ==
FiroType.lelantus) {
if (_data != null && _data!.contactLabel == _address) {
error = SparkInterface.validateSparkAddress(
address: _data!.address, isTestNet: coin.isTestNet)
? "Lelantus to Spark not supported"
: null;
} else if (ref.watch(pValidSparkSendToAddress)) {
error = "Lelantus to Spark not supported";
} else {
error = ref.watch(pValidSendToAddress)
? null
: "Invalid address";
}
} else {
if (_data != null && _data!.contactLabel == _address) {
error = null;
} else if (!ref.watch(pValidSendToAddress) &&
!ref.watch(pValidSparkSendToAddress)) {
error = "Invalid address";
} else {
error = null;
}
}
} else {
if (_data != null && _data!.contactLabel == _address) {
error = null;
} else if (!ref.watch(pValidSendToAddress)) {
error = "Invalid address";
} else {
error = null;
}
}
if (error == null || error.isEmpty) { if (error == null || error.isEmpty) {
return Container(); return Container();
@ -1422,16 +1448,16 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
}, },
), ),
if (isStellar || if (isStellar ||
(_isSparkAddress && (ref.watch(pValidSparkSendToAddress) &&
ref.watch(publicPrivateBalanceStateProvider) != ref.watch(publicPrivateBalanceStateProvider) !=
FiroType.public)) FiroType.lelantus))
const SizedBox( const SizedBox(
height: 10, height: 10,
), ),
if (isStellar || if (isStellar ||
(_isSparkAddress && (ref.watch(pValidSparkSendToAddress) &&
ref.watch(publicPrivateBalanceStateProvider) != ref.watch(publicPrivateBalanceStateProvider) !=
FiroType.public)) FiroType.lelantus))
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular( borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius, Constants.size.circularBorderRadius,
@ -1727,10 +1753,9 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
PrimaryButton( PrimaryButton(
buttonHeight: ButtonHeight.l, buttonHeight: ButtonHeight.l,
label: "Preview send", label: "Preview send",
enabled: ref.watch(previewTxButtonStateProvider.state).state, enabled: ref.watch(pPreviewTxButtonEnabled(coin)),
onPressed: ref.watch(previewTxButtonStateProvider.state).state onPressed:
? previewSend ref.watch(pPreviewTxButtonEnabled(coin)) ? previewSend : null,
: null,
) )
], ],
); );

View file

@ -9,9 +9,32 @@
*/ */
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
final previewTxButtonStateProvider = StateProvider.autoDispose<bool>((_) { final pSendAmount = StateProvider.autoDispose<Amount?>((_) => null);
return false; final pValidSendToAddress = StateProvider.autoDispose<bool>((_) => false);
final pValidSparkSendToAddress = StateProvider.autoDispose<bool>((_) => false);
final pPreviewTxButtonEnabled =
Provider.autoDispose.family<bool, Coin>((ref, coin) {
final amount = ref.watch(pSendAmount) ?? Amount.zero;
// TODO [prio=low]: move away from Coin
if (coin == Coin.firo || coin == Coin.firoTestNet) {
if (ref.watch(publicPrivateBalanceStateProvider) == FiroType.lelantus) {
return ref.watch(pValidSendToAddress) &&
!ref.watch(pValidSparkSendToAddress) &&
amount > Amount.zero;
} else {
return (ref.watch(pValidSendToAddress) ||
ref.watch(pValidSparkSendToAddress)) &&
amount > Amount.zero;
}
} else {
return ref.watch(pValidSendToAddress) && amount > Amount.zero;
}
}); });
final previewTokenTxButtonStateProvider = StateProvider.autoDispose<bool>((_) { final previewTokenTxButtonStateProvider = StateProvider.autoDispose<bool>((_) {