Merge remote-tracking branch 'origin_SW/staging' into custom_fee_selection

This commit is contained in:
julian 2023-06-19 10:09:16 -06:00
commit f60dee0304
12 changed files with 448 additions and 150 deletions

View file

@ -463,6 +463,12 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
super.dispose(); super.dispose();
} }
@override
void didChangeLocales(List<Locale>? locales) {
ref.read(localeServiceChangeNotifierProvider).loadLocale();
super.didChangeLocales(locales);
}
@override @override
void didChangeAppLifecycleState(AppLifecycleState state) async { void didChangeAppLifecycleState(AppLifecycleState state) async {
debugPrint("didChangeAppLifecycleState: ${state.name}"); debugPrint("didChangeAppLifecycleState: ${state.name}");

View file

@ -10,7 +10,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:math';
import 'package:bip47/bip47.dart'; import 'package:bip47/bip47.dart';
import 'package:cw_core/monero_transaction_priority.dart'; import 'package:cw_core/monero_transaction_priority.dart';
@ -41,6 +40,7 @@ import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/address_utils.dart'; import 'package:stackwallet/utilities/address_utils.dart';
import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/amount/amount_formatter.dart'; import 'package:stackwallet/utilities/amount/amount_formatter.dart';
import 'package:stackwallet/utilities/amount/amount_input_formatter.dart';
import 'package:stackwallet/utilities/amount/amount_unit.dart'; import 'package:stackwallet/utilities/amount/amount_unit.dart';
import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/barcode_scanner_interface.dart'; import 'package:stackwallet/utilities/barcode_scanner_interface.dart';
@ -128,27 +128,11 @@ class _SendViewState extends ConsumerState<SendView> {
void _cryptoAmountChanged() async { void _cryptoAmountChanged() async {
if (!_cryptoAmountChangeLock) { if (!_cryptoAmountChangeLock) {
String cryptoAmount = cryptoAmountController.text; final cryptoAmount = ref.read(pAmountFormatter(coin)).tryParse(
if (cryptoAmount.isNotEmpty && cryptoAmountController.text,
cryptoAmount != "." && );
cryptoAmount != ",") { if (cryptoAmount != null) {
if (cryptoAmount.startsWith("~")) { _amountToSend = cryptoAmount;
cryptoAmount = cryptoAmount.substring(1);
}
if (cryptoAmount.contains(" ")) {
cryptoAmount = cryptoAmount.split(" ").first;
}
// ensure we don't shift past minimum atomic value
final shift = min(ref.read(pAmountUnit(coin)).shift, coin.decimals);
_amountToSend = cryptoAmount.contains(",")
? Decimal.parse(cryptoAmount.replaceFirst(",", "."))
.shift(0 - shift)
.toAmount(fractionDigits: coin.decimals)
: Decimal.parse(cryptoAmount)
.shift(0 - shift)
.toAmount(fractionDigits: coin.decimals);
if (_cachedAmountToSend != null && if (_cachedAmountToSend != null &&
_cachedAmountToSend == _amountToSend) { _cachedAmountToSend == _amountToSend) {
return; return;
@ -1567,13 +1551,21 @@ class _SendViewState extends ConsumerState<SendView> {
), ),
textAlign: TextAlign.right, textAlign: TextAlign.right,
inputFormatters: [ inputFormatters: [
AmountInputFormatter(
decimals: coin.decimals,
unit: ref.watch(pAmountUnit(coin)),
locale: locale,
),
// regex to validate a crypto amount with 8 decimal places // regex to validate a crypto amount with 8 decimal places
TextInputFormatter.withFunction((oldValue, // TextInputFormatter.withFunction((oldValue,
newValue) => // newValue) =>
RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$') // // RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$')
.hasMatch(newValue.text) // // RegExp(r'^\d{1,3}([,\.]\d+)?|[,\.\d]+$')
? newValue // getAmountRegex(locale, coin.decimals)
: oldValue), // .hasMatch(newValue.text)
// ? newValue
// : oldValue),
], ],
decoration: InputDecoration( decoration: InputDecoration(
contentPadding: const EdgeInsets.only( contentPadding: const EdgeInsets.only(
@ -1628,26 +1620,25 @@ class _SendViewState extends ConsumerState<SendView> {
), ),
textAlign: TextAlign.right, textAlign: TextAlign.right,
inputFormatters: [ inputFormatters: [
AmountInputFormatter(
decimals: 2,
locale: locale,
),
// regex to validate a fiat amount with 2 decimal places // regex to validate a fiat amount with 2 decimal places
TextInputFormatter.withFunction((oldValue, // TextInputFormatter.withFunction((oldValue,
newValue) => // newValue) =>
RegExp(r'^([0-9]*[,.]?[0-9]{0,2}|[,.][0-9]{0,2})$') // // RegExp(r'^([0-9]*[,.]?[0-9]{0,2}|[,.][0-9]{0,2})$')
.hasMatch(newValue.text) // getAmountRegex(locale, 2)
? newValue // .hasMatch(newValue.text)
: oldValue), // ? newValue
// : oldValue),
], ],
onChanged: (baseAmountString) { onChanged: (baseAmountString) {
if (baseAmountString.isNotEmpty && final baseAmount = Amount.tryParseFiatString(
baseAmountString != "." && baseAmountString,
baseAmountString != ",") { locale: locale,
final Amount baseAmount = );
baseAmountString.contains(",") if (baseAmount != null) {
? Decimal.parse(baseAmountString
.replaceFirst(",", "."))
.toAmount(fractionDigits: 2)
: Decimal.parse(baseAmountString)
.toAmount(fractionDigits: 2);
final Decimal _price = ref final Decimal _price = ref
.read(priceAnd24hChangeNotifierProvider) .read(priceAnd24hChangeNotifierProvider)
.getPrice(coin) .getPrice(coin)

View file

@ -31,6 +31,7 @@ import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/address_utils.dart'; import 'package:stackwallet/utilities/address_utils.dart';
import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/amount/amount_formatter.dart'; import 'package:stackwallet/utilities/amount/amount_formatter.dart';
import 'package:stackwallet/utilities/amount/amount_input_formatter.dart';
import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/barcode_scanner_interface.dart'; import 'package:stackwallet/utilities/barcode_scanner_interface.dart';
import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart';
@ -218,16 +219,11 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
} }
void _onFiatAmountFieldChanged(String baseAmountString) { void _onFiatAmountFieldChanged(String baseAmountString) {
if (baseAmountString.isNotEmpty && final baseAmount = Amount.tryParseFiatString(
baseAmountString != "." && baseAmountString,
baseAmountString != ",") { locale: ref.read(localeServiceChangeNotifierProvider).locale,
final baseAmount = Amount.fromDecimal( );
baseAmountString.contains(",") if (baseAmount != null) {
? Decimal.parse(baseAmountString.replaceFirst(",", "."))
: Decimal.parse(baseAmountString),
fractionDigits: tokenContract.decimals,
);
final _price = ref final _price = ref
.read(priceAnd24hChangeNotifierProvider) .read(priceAnd24hChangeNotifierProvider)
.getTokenPrice(tokenContract.address) .getTokenPrice(tokenContract.address)
@ -272,22 +268,12 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
void _cryptoAmountChanged() async { void _cryptoAmountChanged() async {
if (!_cryptoAmountChangeLock) { if (!_cryptoAmountChangeLock) {
String cryptoAmount = cryptoAmountController.text; final cryptoAmount = ref.read(pAmountFormatter(coin)).tryParse(
if (cryptoAmount.isNotEmpty && cryptoAmountController.text,
cryptoAmount != "." && ethContract: tokenContract,
cryptoAmount != ",") { );
if (cryptoAmount.startsWith("~")) { if (cryptoAmount != null) {
cryptoAmount = cryptoAmount.substring(1); _amountToSend = cryptoAmount;
}
if (cryptoAmount.contains(" ")) {
cryptoAmount = cryptoAmount.split(" ").first;
}
_amountToSend = Amount.fromDecimal(
cryptoAmount.contains(",")
? Decimal.parse(cryptoAmount.replaceFirst(",", "."))
: Decimal.parse(cryptoAmount),
fractionDigits: tokenContract.decimals);
if (_cachedAmountToSend != null && if (_cachedAmountToSend != null &&
_cachedAmountToSend == _amountToSend) { _cachedAmountToSend == _amountToSend) {
return; return;
@ -952,13 +938,18 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
), ),
textAlign: TextAlign.right, textAlign: TextAlign.right,
inputFormatters: [ inputFormatters: [
// regex to validate a crypto amount with 8 decimal places AmountInputFormatter(
TextInputFormatter.withFunction((oldValue, decimals: tokenContract.decimals,
newValue) => unit: ref.watch(pAmountUnit(coin)),
RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$') locale: locale,
.hasMatch(newValue.text) ),
? newValue // // regex to validate a crypto amount with 8 decimal places
: oldValue), // TextInputFormatter.withFunction((oldValue,
// newValue) =>
// RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$')
// .hasMatch(newValue.text)
// ? newValue
// : oldValue),
], ],
decoration: InputDecoration( decoration: InputDecoration(
contentPadding: const EdgeInsets.only( contentPadding: const EdgeInsets.only(
@ -1011,13 +1002,17 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
), ),
textAlign: TextAlign.right, textAlign: TextAlign.right,
inputFormatters: [ inputFormatters: [
// regex to validate a fiat amount with 2 decimal places AmountInputFormatter(
TextInputFormatter.withFunction((oldValue, decimals: 2,
newValue) => locale: locale,
RegExp(r'^([0-9]*[,.]?[0-9]{0,2}|[,.][0-9]{0,2})$') ),
.hasMatch(newValue.text) // // regex to validate a fiat amount with 2 decimal places
? newValue // TextInputFormatter.withFunction((oldValue,
: oldValue), // newValue) =>
// RegExp(r'^([0-9]*[,.]?[0-9]{0,2}|[,.][0-9]{0,2})$')
// .hasMatch(newValue.text)
// ? newValue
// : oldValue),
], ],
onChanged: _onFiatAmountFieldChanged, onChanged: _onFiatAmountFieldChanged,
decoration: InputDecoration( decoration: InputDecoration(
@ -1187,7 +1182,7 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
ConnectionState.done && ConnectionState.done &&
snapshot.hasData) { snapshot.hasData) {
return Text( return Text(
"~${snapshot.data! as String}", "~${snapshot.data!}",
style: style:
STextStyles.itemSubtitle( STextStyles.itemSubtitle(
context), context),

View file

@ -10,16 +10,17 @@
import 'package:decimal/decimal.dart'; import 'package:decimal/decimal.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_rounded_date_picker/flutter_rounded_date_picker.dart'; import 'package:flutter_rounded_date_picker/flutter_rounded_date_picker.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/models/transaction_filter.dart'; import 'package:stackwallet/models/transaction_filter.dart';
import 'package:stackwallet/providers/global/locale_provider.dart';
import 'package:stackwallet/providers/ui/transaction_filter_provider.dart'; import 'package:stackwallet/providers/ui/transaction_filter_provider.dart';
import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/themes/theme_providers.dart'; import 'package:stackwallet/themes/theme_providers.dart';
import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/amount/amount_formatter.dart'; import 'package:stackwallet/utilities/amount/amount_formatter.dart';
import 'package:stackwallet/utilities/amount/amount_input_formatter.dart';
import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart';
@ -757,12 +758,20 @@ class _TransactionSearchViewState
decimal: true, decimal: true,
), ),
inputFormatters: [ inputFormatters: [
// regex to validate a crypto amount with 8 decimal places AmountInputFormatter(
TextInputFormatter.withFunction((oldValue, newValue) => decimals: widget.coin.decimals,
RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$') unit: ref.watch(pAmountUnit(widget.coin)),
.hasMatch(newValue.text) locale: ref.watch(
? newValue localeServiceChangeNotifierProvider
: oldValue), .select((value) => value.locale),
),
),
// // regex to validate a crypto amount with 8 decimal places
// TextInputFormatter.withFunction((oldValue, newValue) =>
// RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$')
// .hasMatch(newValue.text)
// ? newValue
// : oldValue),
], ],
style: isDesktop style: isDesktop
? STextStyles.desktopTextExtraSmall(context).copyWith( ? STextStyles.desktopTextExtraSmall(context).copyWith(

View file

@ -40,6 +40,7 @@ import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/address_utils.dart'; import 'package:stackwallet/utilities/address_utils.dart';
import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/amount/amount_formatter.dart'; import 'package:stackwallet/utilities/amount/amount_formatter.dart';
import 'package:stackwallet/utilities/amount/amount_input_formatter.dart';
import 'package:stackwallet/utilities/amount/amount_unit.dart'; import 'package:stackwallet/utilities/amount/amount_unit.dart';
import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/barcode_scanner_interface.dart'; import 'package:stackwallet/utilities/barcode_scanner_interface.dart';
@ -1054,12 +1055,17 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
), ),
textAlign: TextAlign.right, textAlign: TextAlign.right,
inputFormatters: [ inputFormatters: [
// regex to validate a crypto amount with 8 decimal places AmountInputFormatter(
TextInputFormatter.withFunction((oldValue, newValue) => decimals: coin.decimals,
RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$') unit: ref.watch(pAmountUnit(coin)),
.hasMatch(newValue.text) locale: locale,
? newValue ),
: oldValue), // // regex to validate a crypto amount with 8 decimal places
// TextInputFormatter.withFunction((oldValue, newValue) =>
// RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$')
// .hasMatch(newValue.text)
// ? newValue
// : oldValue),
], ],
onChanged: (newValue) {}, onChanged: (newValue) {},
decoration: InputDecoration( decoration: InputDecoration(
@ -1111,12 +1117,16 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
), ),
textAlign: TextAlign.right, textAlign: TextAlign.right,
inputFormatters: [ inputFormatters: [
// regex to validate a fiat amount with 2 decimal places AmountInputFormatter(
TextInputFormatter.withFunction((oldValue, newValue) => decimals: 2,
RegExp(r'^([0-9]*[,.]?[0-9]{0,2}|[,.][0-9]{0,2})$') locale: locale,
.hasMatch(newValue.text) ),
? newValue // // regex to validate a fiat amount with 2 decimal places
: oldValue), // TextInputFormatter.withFunction((oldValue, newValue) =>
// RegExp(r'^([0-9]*[,.]?[0-9]{0,2}|[,.][0-9]{0,2})$')
// .hasMatch(newValue.text)
// ? newValue
// : oldValue),
], ],
onChanged: fiatTextFieldOnChanged, onChanged: fiatTextFieldOnChanged,
decoration: InputDecoration( decoration: InputDecoration(

View file

@ -31,6 +31,7 @@ import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/address_utils.dart'; import 'package:stackwallet/utilities/address_utils.dart';
import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/amount/amount_formatter.dart'; import 'package:stackwallet/utilities/amount/amount_formatter.dart';
import 'package:stackwallet/utilities/amount/amount_input_formatter.dart';
import 'package:stackwallet/utilities/barcode_scanner_interface.dart'; import 'package:stackwallet/utilities/barcode_scanner_interface.dart';
import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart';
import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/constants.dart';
@ -50,7 +51,7 @@ import 'package:stackwallet/widgets/icon_widgets/x_icon.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';
const _kCryptoAmountRegex = r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$'; // const _kCryptoAmountRegex = r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$';
class DesktopTokenSend extends ConsumerStatefulWidget { class DesktopTokenSend extends ConsumerStatefulWidget {
const DesktopTokenSend({ const DesktopTokenSend({
@ -717,15 +718,23 @@ class _DesktopTokenSendState extends ConsumerState<DesktopTokenSend> {
), ),
textAlign: TextAlign.right, textAlign: TextAlign.right,
inputFormatters: [ inputFormatters: [
AmountInputFormatter(
decimals: tokenContract.decimals,
unit: ref.watch(pAmountUnit(coin)),
locale: ref.watch(
localeServiceChangeNotifierProvider
.select((value) => value.locale),
),
),
// regex to validate a crypto amount with 8 decimal places // regex to validate a crypto amount with 8 decimal places
TextInputFormatter.withFunction((oldValue, newValue) => RegExp( // TextInputFormatter.withFunction((oldValue, newValue) => RegExp(
_kCryptoAmountRegex.replaceAll( // _kCryptoAmountRegex.replaceAll(
"0,8", // "0,8",
"0,${tokenContract.decimals}", // "0,${tokenContract.decimals}",
), // ),
).hasMatch(newValue.text) // ).hasMatch(newValue.text)
? newValue // ? newValue
: oldValue), // : oldValue),
], ],
onChanged: (newValue) {}, onChanged: (newValue) {},
decoration: InputDecoration( decoration: InputDecoration(
@ -777,12 +786,19 @@ class _DesktopTokenSendState extends ConsumerState<DesktopTokenSend> {
), ),
textAlign: TextAlign.right, textAlign: TextAlign.right,
inputFormatters: [ inputFormatters: [
// regex to validate a fiat amount with 2 decimal places AmountInputFormatter(
TextInputFormatter.withFunction((oldValue, newValue) => decimals: 2,
RegExp(r'^([0-9]*[,.]?[0-9]{0,2}|[,.][0-9]{0,2})$') locale: ref.watch(
.hasMatch(newValue.text) localeServiceChangeNotifierProvider
? newValue .select((value) => value.locale),
: oldValue), ),
),
// // regex to validate a fiat amount with 2 decimal places
// TextInputFormatter.withFunction((oldValue, newValue) =>
// RegExp(r'^([0-9]*[,.]?[0-9]{0,2}|[,.][0-9]{0,2})$')
// .hasMatch(newValue.text)
// ? newValue
// : oldValue),
], ],
onChanged: fiatTextFieldOnChanged, onChanged: fiatTextFieldOnChanged,
decoration: InputDecoration( decoration: InputDecoration(

View file

@ -32,6 +32,39 @@ class Amount {
: assert(fractionDigits >= 0), : assert(fractionDigits >= 0),
_value = amount.shift(fractionDigits).toBigInt(); _value = amount.shift(fractionDigits).toBigInt();
static Amount? tryParseFiatString(
String value, {
required String locale,
}) {
final parts = value.split(" ");
if (parts.first.isEmpty) {
return null;
}
String str = parts.first;
if (str.startsWith(RegExp(r'[+-]'))) {
str = str.substring(1);
}
if (str.isEmpty) {
return null;
}
// get number symbols for decimal place and group separator
final numberSymbols = numberFormatSymbols[locale] as NumberSymbols? ??
numberFormatSymbols[locale.substring(0, 2)] as NumberSymbols?;
final groupSeparator = numberSymbols?.GROUP_SEP ?? ",";
final decimalSeparator = numberSymbols?.DECIMAL_SEP ?? ".";
str = str.replaceAll(groupSeparator, "");
final decimalString = str.replaceFirst(decimalSeparator, ".");
return Decimal.tryParse(decimalString)?.toAmount(fractionDigits: 2);
}
// =========================================================================== // ===========================================================================
// ======= Instance properties =============================================== // ======= Instance properties ===============================================
@ -67,15 +100,23 @@ class Amount {
}) { }) {
final wholeNumber = decimal.truncate(); final wholeNumber = decimal.truncate();
final String separator = // get number symbols for decimal place and group separator
(numberFormatSymbols[locale] as NumberSymbols?)?.DECIMAL_SEP ?? final numberSymbols = numberFormatSymbols[locale] as NumberSymbols? ??
(numberFormatSymbols[locale.substring(0, 2)] as NumberSymbols?) numberFormatSymbols[locale.substring(0, 2)] as NumberSymbols?;
?.DECIMAL_SEP ??
"."; final String separator = numberSymbols?.DECIMAL_SEP ?? ".";
final fraction = decimal - wholeNumber; final fraction = decimal - wholeNumber;
return "${wholeNumber.toStringAsFixed(0)}$separator${fraction.toStringAsFixed(2).substring(2)}"; String wholeNumberString = wholeNumber.toStringAsFixed(0);
// insert group separator
final regex = RegExp(r'\B(?=(\d{3})+(?!\d))');
wholeNumberString = wholeNumberString.replaceAllMapped(
regex,
(m) => "${m.group(0)}${numberSymbols?.GROUP_SEP ?? ","}",
);
return "$wholeNumberString$separator${fraction.toStringAsFixed(2).substring(2)}";
} }
// String localizedStringAsFixed({ // String localizedStringAsFixed({
// required String locale, // required String locale,

View file

@ -63,4 +63,11 @@ class AmountFormatter {
tokenContract: ethContract, tokenContract: ethContract,
); );
} }
Amount? tryParse(
String string, {
EthContract? ethContract,
}) {
return unit.tryParse(string, locale: locale, coin: coin);
}
} }

View file

@ -0,0 +1,93 @@
import 'dart:math';
import 'package:flutter/services.dart';
import 'package:intl/number_symbols.dart';
import 'package:intl/number_symbols_data.dart';
import 'package:stackwallet/utilities/amount/amount_unit.dart';
class AmountInputFormatter extends TextInputFormatter {
final int decimals;
final String locale;
final AmountUnit? unit;
AmountInputFormatter({
required this.decimals,
required this.locale,
this.unit,
});
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue, TextEditingValue newValue) {
// get number symbols for decimal place and group separator
final numberSymbols = numberFormatSymbols[locale] as NumberSymbols? ??
numberFormatSymbols[locale.substring(0, 2)] as NumberSymbols?;
final decimalSeparator = numberSymbols?.DECIMAL_SEP ?? ".";
final groupSeparator = numberSymbols?.GROUP_SEP ?? ",";
String newText = newValue.text.replaceAll(groupSeparator, "");
final selectionIndexFromTheRight =
newValue.text.length - newValue.selection.end;
String? fraction;
if (newText.contains(decimalSeparator)) {
final parts = newText.split(decimalSeparator);
if (parts.length > 2) {
return oldValue;
}
if (newText.startsWith(decimalSeparator)) {
return TextEditingValue(
text: newText,
selection: TextSelection.collapsed(
offset: newText.length - selectionIndexFromTheRight,
),
);
}
newText = parts.first;
if (parts.length == 2) {
fraction = parts.last;
} else {
fraction = "";
}
final fractionDigits =
unit == null ? decimals : max(decimals - unit!.shift, 0);
if (fraction.length > fractionDigits) {
return oldValue;
}
}
if (newText.trim() == '' || newText.trim() == '0') {
return newValue.copyWith(text: '');
} else if (BigInt.parse(newText) < BigInt.one) {
return newValue.copyWith(text: '');
}
// insert group separator
final regex = RegExp(r'\B(?=(\d{3})+(?!\d))');
String newString = newText.replaceAllMapped(
regex,
(m) => "${m.group(0)}${numberSymbols?.GROUP_SEP ?? ","}",
);
if (fraction != null) {
newString += decimalSeparator;
if (fraction.isNotEmpty) {
newString += fraction;
}
}
return TextEditingValue(
text: newString,
selection: TextSelection.collapsed(
offset: newString.length - selectionIndexFromTheRight,
),
);
}
}

View file

@ -164,6 +164,51 @@ extension AmountUnitExt on AmountUnit {
} }
} }
Amount? tryParse(
String value, {
required String locale,
required Coin coin,
EthContract? tokenContract,
}) {
final precisionLost = value.startsWith("~");
final parts = (precisionLost ? value.substring(1) : value).split(" ");
if (parts.first.isEmpty) {
return null;
}
String str = parts.first;
if (str.startsWith(RegExp(r'[+-]'))) {
str = str.substring(1);
}
if (str.isEmpty) {
return null;
}
// get number symbols for decimal place and group separator
final numberSymbols = numberFormatSymbols[locale] as NumberSymbols? ??
numberFormatSymbols[locale.substring(0, 2)] as NumberSymbols?;
final groupSeparator = numberSymbols?.GROUP_SEP ?? ",";
final decimalSeparator = numberSymbols?.DECIMAL_SEP ?? ".";
str = str.replaceAll(groupSeparator, "");
final decimalString = str.replaceFirst(decimalSeparator, ".");
final Decimal? decimal = Decimal.tryParse(decimalString);
if (decimal == null) {
return null;
}
final decimalPlaces = tokenContract?.decimals ?? coin.decimals;
final realShift = math.min(shift, decimalPlaces);
return decimal.shift(0 - realShift).toAmount(fractionDigits: decimalPlaces);
}
String displayAmount({ String displayAmount({
required Amount amount, required Amount amount,
required String locale, required String locale,
@ -191,6 +236,17 @@ extension AmountUnitExt on AmountUnit {
// start building the return value with just the whole value // start building the return value with just the whole value
String returnValue = wholeNumber.toString(); String returnValue = wholeNumber.toString();
// get number symbols for decimal place and group separator
final numberSymbols = numberFormatSymbols[locale] as NumberSymbols? ??
numberFormatSymbols[locale.substring(0, 2)] as NumberSymbols?;
// insert group separator
final regex = RegExp(r'\B(?=(\d{3})+(?!\d))');
returnValue = returnValue.replaceAllMapped(
regex,
(m) => "${m.group(0)}${numberSymbols?.GROUP_SEP ?? ","}",
);
// if true and withUnitName is true, we will show "~" prepended on amount // if true and withUnitName is true, we will show "~" prepended on amount
bool didLosePrecision = false; bool didLosePrecision = false;
@ -239,11 +295,7 @@ extension AmountUnitExt on AmountUnit {
} }
// get decimal separator based on locale // get decimal separator based on locale
final String separator = final String separator = numberSymbols?.DECIMAL_SEP ?? ".";
(numberFormatSymbols[locale] as NumberSymbols?)?.DECIMAL_SEP ??
(numberFormatSymbols[locale.substring(0, 2)] as NumberSymbols?)
?.DECIMAL_SEP ??
".";
// append separator and fractional amount // append separator and fractional amount
returnValue += "$separator$remainder"; returnValue += "$separator$remainder";

View file

@ -9,17 +9,19 @@
*/ */
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/models/exchange/aggregate_currency.dart'; import 'package:stackwallet/models/exchange/aggregate_currency.dart';
import 'package:stackwallet/pages/buy_view/sub_widgets/crypto_selection_view.dart'; import 'package:stackwallet/pages/buy_view/sub_widgets/crypto_selection_view.dart';
import 'package:stackwallet/providers/global/locale_provider.dart';
import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/amount/amount_input_formatter.dart';
import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/loading_indicator.dart';
class ExchangeTextField extends StatefulWidget { class ExchangeTextField extends ConsumerStatefulWidget {
const ExchangeTextField({ const ExchangeTextField({
Key? key, Key? key,
this.borderRadius = 0, this.borderRadius = 0,
@ -55,10 +57,10 @@ class ExchangeTextField extends StatefulWidget {
final AggregateCurrency? currency; final AggregateCurrency? currency;
@override @override
State<ExchangeTextField> createState() => _ExchangeTextFieldState(); ConsumerState<ExchangeTextField> createState() => _ExchangeTextFieldState();
} }
class _ExchangeTextFieldState extends State<ExchangeTextField> { class _ExchangeTextFieldState extends ConsumerState<ExchangeTextField> {
late final TextEditingController controller; late final TextEditingController controller;
late final FocusNode focusNode; late final FocusNode focusNode;
late final TextStyle textStyle; late final TextStyle textStyle;
@ -130,12 +132,17 @@ class _ExchangeTextFieldState extends State<ExchangeTextField> {
), ),
), ),
inputFormatters: [ inputFormatters: [
// regex to validate a crypto amount with 8 decimal places AmountInputFormatter(
TextInputFormatter.withFunction((oldValue, newValue) => decimals: 8, // todo change this
RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$') locale: ref.watch(localeServiceChangeNotifierProvider
.hasMatch(newValue.text) .select((value) => value.locale)),
? newValue ),
: oldValue), // // regex to validate a crypto amount with 8 decimal places
// TextInputFormatter.withFunction((oldValue, newValue) =>
// RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$')
// .hasMatch(newValue.text)
// ? newValue
// : oldValue),
], ],
), ),
), ),

View file

@ -28,7 +28,7 @@ void main() {
coin: Coin.bitcoin, coin: Coin.bitcoin,
maxDecimalPlaces: 8, maxDecimalPlaces: 8,
), ),
"10123.45678 mBTC", "10,123.45678 mBTC",
); );
expect( expect(
@ -38,7 +38,7 @@ void main() {
coin: Coin.bitcoin, coin: Coin.bitcoin,
maxDecimalPlaces: 8, maxDecimalPlaces: 8,
), ),
"10123456.78 µBTC", "10,123,456.78 µBTC",
); );
expect( expect(
@ -48,7 +48,7 @@ void main() {
coin: Coin.bitcoin, coin: Coin.bitcoin,
maxDecimalPlaces: 8, maxDecimalPlaces: 8,
), ),
"1012345678 sats", "1,012,345,678 sats",
); );
final dec = Decimal.parse("10.123456789123456789"); final dec = Decimal.parse("10.123456789123456789");
@ -98,7 +98,7 @@ void main() {
coin: Coin.ethereum, coin: Coin.ethereum,
maxDecimalPlaces: 9, maxDecimalPlaces: 9,
), ),
"~10123.456789123 mETH", "~10,123.456789123 mETH",
); );
expect( expect(
@ -108,7 +108,7 @@ void main() {
coin: Coin.ethereum, coin: Coin.ethereum,
maxDecimalPlaces: 8, maxDecimalPlaces: 8,
), ),
"~10123456.78912345 µETH", "~10,123,456.78912345 µETH",
); );
expect( expect(
@ -118,7 +118,7 @@ void main() {
coin: Coin.ethereum, coin: Coin.ethereum,
maxDecimalPlaces: 1, maxDecimalPlaces: 1,
), ),
"~10123456789.1 gwei", "~10,123,456,789.1 gwei",
); );
expect( expect(
@ -128,7 +128,7 @@ void main() {
coin: Coin.ethereum, coin: Coin.ethereum,
maxDecimalPlaces: 18, maxDecimalPlaces: 18,
), ),
"10123456789123.456789 mwei", "10,123,456,789,123.456789 mwei",
); );
expect( expect(
@ -138,7 +138,7 @@ void main() {
coin: Coin.ethereum, coin: Coin.ethereum,
maxDecimalPlaces: 4, maxDecimalPlaces: 4,
), ),
"10123456789123456.789 kwei", "10,123,456,789,123,456.789 kwei",
); );
expect( expect(
@ -148,7 +148,78 @@ void main() {
coin: Coin.ethereum, coin: Coin.ethereum,
maxDecimalPlaces: 1, maxDecimalPlaces: 1,
), ),
"10123456789123456789 wei", "10,123,456,789,123,456,789 wei",
);
});
test("parse eth string to amount", () {
final Amount amount = Amount.fromDecimal(
Decimal.parse("10.123456789123456789"),
fractionDigits: Coin.ethereum.decimals,
);
expect(
AmountUnit.nano.tryParse(
"~10,123,456,789.1 gwei",
locale: "en_US",
coin: Coin.ethereum,
),
Amount.fromDecimal(
Decimal.parse("10.1234567891"),
fractionDigits: Coin.ethereum.decimals,
),
);
expect(
AmountUnit.atto.tryParse(
"10,123,456,789,123,456,789 wei",
locale: "en_US",
coin: Coin.ethereum,
),
amount,
);
});
test("parse btc string to amount", () {
final Amount amount = Amount(
rawValue: BigInt.from(1012345678),
fractionDigits: 8,
);
expect(
AmountUnit.normal.tryParse(
"10.12345678 BTC",
locale: "en_US",
coin: Coin.bitcoin,
),
amount,
);
expect(
AmountUnit.milli.tryParse(
"10,123.45678 mBTC",
locale: "en_US",
coin: Coin.bitcoin,
),
amount,
);
expect(
AmountUnit.micro.tryParse(
"10,123,456.7822 µBTC",
locale: "en_US",
coin: Coin.bitcoin,
),
amount,
);
expect(
AmountUnit.nano.tryParse(
"1,012,345,678 sats",
locale: "en_US",
coin: Coin.bitcoin,
),
amount,
); );
}); });
} }