From 7bcfc87f4d9c2843ed18bab081728bc30be515ba Mon Sep 17 00:00:00 2001
From: julian <julian@cypherstack.com>
Date: Fri, 16 Jun 2023 10:43:20 -0600
Subject: [PATCH 1/5] feat: amount string group separator based on locale and
 parse those formatted strings back to amounts based on localized decimal
 pattern

---
 lib/utilities/amount/amount_formatter.dart  |  9 +++
 lib/utilities/amount/amount_unit.dart       | 62 ++++++++++++--
 test/utilities/amount/amount_unit_test.dart | 89 ++++++++++++++++++---
 3 files changed, 146 insertions(+), 14 deletions(-)

diff --git a/lib/utilities/amount/amount_formatter.dart b/lib/utilities/amount/amount_formatter.dart
index 3e556c95f..5f8f5c99b 100644
--- a/lib/utilities/amount/amount_formatter.dart
+++ b/lib/utilities/amount/amount_formatter.dart
@@ -63,4 +63,13 @@ class AmountFormatter {
       tokenContract: ethContract,
     );
   }
+
+  Amount? amountFrom(
+    String string, {
+    required String locale,
+    required Coin coin,
+    EthContract? ethContract,
+  }) {
+    return unit.tryParse(string, locale: locale, coin: coin);
+  }
 }
diff --git a/lib/utilities/amount/amount_unit.dart b/lib/utilities/amount/amount_unit.dart
index ee54f6d1a..79b36130f 100644
--- a/lib/utilities/amount/amount_unit.dart
+++ b/lib/utilities/amount/amount_unit.dart
@@ -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({
     required Amount amount,
     required String locale,
@@ -191,6 +236,17 @@ extension AmountUnitExt on AmountUnit {
     // start building the return value with just the whole value
     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
     bool didLosePrecision = false;
 
@@ -239,11 +295,7 @@ extension AmountUnitExt on AmountUnit {
       }
 
       // get decimal separator based on locale
-      final String separator =
-          (numberFormatSymbols[locale] as NumberSymbols?)?.DECIMAL_SEP ??
-              (numberFormatSymbols[locale.substring(0, 2)] as NumberSymbols?)
-                  ?.DECIMAL_SEP ??
-              ".";
+      final String separator = numberSymbols?.DECIMAL_SEP ?? ".";
 
       // append separator and fractional amount
       returnValue += "$separator$remainder";
diff --git a/test/utilities/amount/amount_unit_test.dart b/test/utilities/amount/amount_unit_test.dart
index 7af988def..7591473b5 100644
--- a/test/utilities/amount/amount_unit_test.dart
+++ b/test/utilities/amount/amount_unit_test.dart
@@ -28,7 +28,7 @@ void main() {
         coin: Coin.bitcoin,
         maxDecimalPlaces: 8,
       ),
-      "10123.45678 mBTC",
+      "10,123.45678 mBTC",
     );
 
     expect(
@@ -38,7 +38,7 @@ void main() {
         coin: Coin.bitcoin,
         maxDecimalPlaces: 8,
       ),
-      "10123456.78 µBTC",
+      "10,123,456.78 µBTC",
     );
 
     expect(
@@ -48,7 +48,7 @@ void main() {
         coin: Coin.bitcoin,
         maxDecimalPlaces: 8,
       ),
-      "1012345678 sats",
+      "1,012,345,678 sats",
     );
     final dec = Decimal.parse("10.123456789123456789");
 
@@ -98,7 +98,7 @@ void main() {
         coin: Coin.ethereum,
         maxDecimalPlaces: 9,
       ),
-      "~10123.456789123 mETH",
+      "~10,123.456789123 mETH",
     );
 
     expect(
@@ -108,7 +108,7 @@ void main() {
         coin: Coin.ethereum,
         maxDecimalPlaces: 8,
       ),
-      "~10123456.78912345 µETH",
+      "~10,123,456.78912345 µETH",
     );
 
     expect(
@@ -118,7 +118,7 @@ void main() {
         coin: Coin.ethereum,
         maxDecimalPlaces: 1,
       ),
-      "~10123456789.1 gwei",
+      "~10,123,456,789.1 gwei",
     );
 
     expect(
@@ -128,7 +128,7 @@ void main() {
         coin: Coin.ethereum,
         maxDecimalPlaces: 18,
       ),
-      "10123456789123.456789 mwei",
+      "10,123,456,789,123.456789 mwei",
     );
 
     expect(
@@ -138,7 +138,7 @@ void main() {
         coin: Coin.ethereum,
         maxDecimalPlaces: 4,
       ),
-      "10123456789123456.789 kwei",
+      "10,123,456,789,123,456.789 kwei",
     );
 
     expect(
@@ -148,7 +148,78 @@ void main() {
         coin: Coin.ethereum,
         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,
     );
   });
 }

From f88d7f200ca7910daca3279257b884fcc101b5c8 Mon Sep 17 00:00:00 2001
From: julian <julian@cypherstack.com>
Date: Fri, 16 Jun 2023 13:42:37 -0600
Subject: [PATCH 2/5] fiat amount string formatting and parsing and more WIP
 usages thereof

---
 lib/pages/send_view/send_view.dart         | 45 +++++-------------
 lib/pages/send_view/token_send_view.dart   | 41 ++++++-----------
 lib/utilities/amount/amount.dart           | 53 +++++++++++++++++++---
 lib/utilities/amount/amount_formatter.dart |  2 +-
 4 files changed, 74 insertions(+), 67 deletions(-)

diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart
index 5ce6ccbb0..941c2cb5a 100644
--- a/lib/pages/send_view/send_view.dart
+++ b/lib/pages/send_view/send_view.dart
@@ -10,7 +10,6 @@
 
 import 'dart:async';
 import 'dart:io';
-import 'dart:math';
 
 import 'package:bip47/bip47.dart';
 import 'package:cw_core/monero_transaction_priority.dart';
@@ -127,27 +126,13 @@ class _SendViewState extends ConsumerState<SendView> {
 
   void _cryptoAmountChanged() async {
     if (!_cryptoAmountChangeLock) {
-      String cryptoAmount = cryptoAmountController.text;
-      if (cryptoAmount.isNotEmpty &&
-          cryptoAmount != "." &&
-          cryptoAmount != ",") {
-        if (cryptoAmount.startsWith("~")) {
-          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);
+      final cryptoAmount = ref.read(pAmountFormatter(coin)).tryParse(
+            cryptoAmountController.text,
+            locale: ref.read(localeServiceChangeNotifierProvider).locale,
+            coin: coin,
+          );
+      if (cryptoAmount != null) {
+        _amountToSend = cryptoAmount;
         if (_cachedAmountToSend != null &&
             _cachedAmountToSend == _amountToSend) {
           return;
@@ -1623,17 +1608,11 @@ class _SendViewState extends ConsumerState<SendView> {
                                         : oldValue),
                               ],
                               onChanged: (baseAmountString) {
-                                if (baseAmountString.isNotEmpty &&
-                                    baseAmountString != "." &&
-                                    baseAmountString != ",") {
-                                  final Amount baseAmount =
-                                      baseAmountString.contains(",")
-                                          ? Decimal.parse(baseAmountString
-                                                  .replaceFirst(",", "."))
-                                              .toAmount(fractionDigits: 2)
-                                          : Decimal.parse(baseAmountString)
-                                              .toAmount(fractionDigits: 2);
-
+                                final baseAmount = Amount.tryParseFiatString(
+                                  baseAmountString,
+                                  locale: locale,
+                                );
+                                if (baseAmount != null) {
                                   final Decimal _price = ref
                                       .read(priceAnd24hChangeNotifierProvider)
                                       .getPrice(coin)
diff --git a/lib/pages/send_view/token_send_view.dart b/lib/pages/send_view/token_send_view.dart
index 4aa3a2225..ad845e9db 100644
--- a/lib/pages/send_view/token_send_view.dart
+++ b/lib/pages/send_view/token_send_view.dart
@@ -218,16 +218,11 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
   }
 
   void _onFiatAmountFieldChanged(String baseAmountString) {
-    if (baseAmountString.isNotEmpty &&
-        baseAmountString != "." &&
-        baseAmountString != ",") {
-      final baseAmount = Amount.fromDecimal(
-        baseAmountString.contains(",")
-            ? Decimal.parse(baseAmountString.replaceFirst(",", "."))
-            : Decimal.parse(baseAmountString),
-        fractionDigits: tokenContract.decimals,
-      );
-
+    final baseAmount = Amount.tryParseFiatString(
+      baseAmountString,
+      locale: ref.read(localeServiceChangeNotifierProvider).locale,
+    );
+    if (baseAmount != null) {
       final _price = ref
           .read(priceAnd24hChangeNotifierProvider)
           .getTokenPrice(tokenContract.address)
@@ -272,22 +267,14 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
 
   void _cryptoAmountChanged() async {
     if (!_cryptoAmountChangeLock) {
-      String cryptoAmount = cryptoAmountController.text;
-      if (cryptoAmount.isNotEmpty &&
-          cryptoAmount != "." &&
-          cryptoAmount != ",") {
-        if (cryptoAmount.startsWith("~")) {
-          cryptoAmount = cryptoAmount.substring(1);
-        }
-        if (cryptoAmount.contains(" ")) {
-          cryptoAmount = cryptoAmount.split(" ").first;
-        }
-
-        _amountToSend = Amount.fromDecimal(
-            cryptoAmount.contains(",")
-                ? Decimal.parse(cryptoAmount.replaceFirst(",", "."))
-                : Decimal.parse(cryptoAmount),
-            fractionDigits: tokenContract.decimals);
+      final cryptoAmount = ref.read(pAmountFormatter(coin)).tryParse(
+            cryptoAmountController.text,
+            locale: ref.read(localeServiceChangeNotifierProvider).locale,
+            coin: coin,
+            ethContract: tokenContract,
+          );
+      if (cryptoAmount != null) {
+        _amountToSend = cryptoAmount;
         if (_cachedAmountToSend != null &&
             _cachedAmountToSend == _amountToSend) {
           return;
@@ -1185,7 +1172,7 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
                                                       ConnectionState.done &&
                                                   snapshot.hasData) {
                                                 return Text(
-                                                  "~${snapshot.data! as String}",
+                                                  "~${snapshot.data!}",
                                                   style:
                                                       STextStyles.itemSubtitle(
                                                           context),
diff --git a/lib/utilities/amount/amount.dart b/lib/utilities/amount/amount.dart
index e0225d34f..0e8790064 100644
--- a/lib/utilities/amount/amount.dart
+++ b/lib/utilities/amount/amount.dart
@@ -32,6 +32,39 @@ class Amount {
       : assert(fractionDigits >= 0),
         _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 ===============================================
 
@@ -67,15 +100,23 @@ class Amount {
   }) {
     final wholeNumber = decimal.truncate();
 
-    final String separator =
-        (numberFormatSymbols[locale] as NumberSymbols?)?.DECIMAL_SEP ??
-            (numberFormatSymbols[locale.substring(0, 2)] as NumberSymbols?)
-                ?.DECIMAL_SEP ??
-            ".";
+    // get number symbols for decimal place and group separator
+    final numberSymbols = numberFormatSymbols[locale] as NumberSymbols? ??
+        numberFormatSymbols[locale.substring(0, 2)] as NumberSymbols?;
+
+    final String separator = numberSymbols?.DECIMAL_SEP ?? ".";
 
     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({
   //   required String locale,
diff --git a/lib/utilities/amount/amount_formatter.dart b/lib/utilities/amount/amount_formatter.dart
index 5f8f5c99b..f9cd3f94d 100644
--- a/lib/utilities/amount/amount_formatter.dart
+++ b/lib/utilities/amount/amount_formatter.dart
@@ -64,7 +64,7 @@ class AmountFormatter {
     );
   }
 
-  Amount? amountFrom(
+  Amount? tryParse(
     String string, {
     required String locale,
     required Coin coin,

From c0eb85ce5a046c7ec79c7885d5974aed75809ca1 Mon Sep 17 00:00:00 2001
From: julian <julian@cypherstack.com>
Date: Fri, 16 Jun 2023 16:47:03 -0600
Subject: [PATCH 3/5] insert and handle localised group and decimal separators
 in textfield amounts

---
 lib/pages/send_view/send_view.dart            | 39 +++++----
 lib/pages/send_view/token_send_view.dart      | 39 +++++----
 .../transaction_search_filter_view.dart       | 22 +++--
 .../wallet_view/sub_widgets/desktop_send.dart | 33 +++++---
 .../sub_widgets/desktop_token_send.dart       | 45 ++++++----
 lib/utilities/amount/amount_formatter.dart    |  2 -
 .../amount/amount_input_formatter.dart        | 82 +++++++++++++++++++
 .../textfields/exchange_textfield.dart        | 27 +++---
 8 files changed, 213 insertions(+), 76 deletions(-)
 create mode 100644 lib/utilities/amount/amount_input_formatter.dart

diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart
index 941c2cb5a..3f48015be 100644
--- a/lib/pages/send_view/send_view.dart
+++ b/lib/pages/send_view/send_view.dart
@@ -40,6 +40,7 @@ import 'package:stackwallet/themes/stack_colors.dart';
 import 'package:stackwallet/utilities/address_utils.dart';
 import 'package:stackwallet/utilities/amount/amount.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/assets.dart';
 import 'package:stackwallet/utilities/barcode_scanner_interface.dart';
@@ -128,8 +129,6 @@ class _SendViewState extends ConsumerState<SendView> {
     if (!_cryptoAmountChangeLock) {
       final cryptoAmount = ref.read(pAmountFormatter(coin)).tryParse(
             cryptoAmountController.text,
-            locale: ref.read(localeServiceChangeNotifierProvider).locale,
-            coin: coin,
           );
       if (cryptoAmount != null) {
         _amountToSend = cryptoAmount;
@@ -1538,13 +1537,20 @@ class _SendViewState extends ConsumerState<SendView> {
                                   ),
                             textAlign: TextAlign.right,
                             inputFormatters: [
+                              AmountInputFormatter(
+                                decimals: coin.decimals,
+                                locale: 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),
+                              // TextInputFormatter.withFunction((oldValue,
+                              //         newValue) =>
+                              //     // RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$')
+                              //     // RegExp(r'^\d{1,3}([,\.]\d+)?|[,\.\d]+$')
+                              //     getAmountRegex(locale, coin.decimals)
+                              //             .hasMatch(newValue.text)
+                              //         ? newValue
+                              //         : oldValue),
                             ],
                             decoration: InputDecoration(
                               contentPadding: const EdgeInsets.only(
@@ -1599,13 +1605,18 @@ class _SendViewState extends ConsumerState<SendView> {
                                     ),
                               textAlign: TextAlign.right,
                               inputFormatters: [
+                                AmountInputFormatter(
+                                  decimals: 2,
+                                  locale: locale,
+                                ),
                                 // 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),
+                                // TextInputFormatter.withFunction((oldValue,
+                                //         newValue) =>
+                                //     // RegExp(r'^([0-9]*[,.]?[0-9]{0,2}|[,.][0-9]{0,2})$')
+                                //     getAmountRegex(locale, 2)
+                                //             .hasMatch(newValue.text)
+                                //         ? newValue
+                                //         : oldValue),
                               ],
                               onChanged: (baseAmountString) {
                                 final baseAmount = Amount.tryParseFiatString(
diff --git a/lib/pages/send_view/token_send_view.dart b/lib/pages/send_view/token_send_view.dart
index ad845e9db..7a2a9f424 100644
--- a/lib/pages/send_view/token_send_view.dart
+++ b/lib/pages/send_view/token_send_view.dart
@@ -31,6 +31,7 @@ import 'package:stackwallet/themes/stack_colors.dart';
 import 'package:stackwallet/utilities/address_utils.dart';
 import 'package:stackwallet/utilities/amount/amount.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/barcode_scanner_interface.dart';
 import 'package:stackwallet/utilities/clipboard_interface.dart';
@@ -269,8 +270,6 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
     if (!_cryptoAmountChangeLock) {
       final cryptoAmount = ref.read(pAmountFormatter(coin)).tryParse(
             cryptoAmountController.text,
-            locale: ref.read(localeServiceChangeNotifierProvider).locale,
-            coin: coin,
             ethContract: tokenContract,
           );
       if (cryptoAmount != null) {
@@ -937,13 +936,17 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
                                   ),
                             textAlign: TextAlign.right,
                             inputFormatters: [
-                              // 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),
+                              AmountInputFormatter(
+                                decimals: tokenContract.decimals,
+                                locale: 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),
                             ],
                             decoration: InputDecoration(
                               contentPadding: const EdgeInsets.only(
@@ -996,13 +999,17 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
                                     ),
                               textAlign: TextAlign.right,
                               inputFormatters: [
-                                // 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),
+                                AmountInputFormatter(
+                                  decimals: 2,
+                                  locale: locale,
+                                ),
+                                // // 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: _onFiatAmountFieldChanged,
                               decoration: InputDecoration(
diff --git a/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart b/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart
index b41177ab9..e4c3e84a7 100644
--- a/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart
+++ b/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart
@@ -10,16 +10,17 @@
 
 import 'package:decimal/decimal.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:flutter_rounded_date_picker/flutter_rounded_date_picker.dart';
 import 'package:flutter_svg/svg.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/themes/stack_colors.dart';
 import 'package:stackwallet/themes/theme_providers.dart';
 import 'package:stackwallet/utilities/amount/amount.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/constants.dart';
 import 'package:stackwallet/utilities/enums/coin_enum.dart';
@@ -757,12 +758,19 @@ class _TransactionSearchViewState
                       decimal: true,
                     ),
               inputFormatters: [
-                // 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),
+                AmountInputFormatter(
+                  decimals: widget.coin.decimals,
+                  locale: ref.watch(
+                    localeServiceChangeNotifierProvider
+                        .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
                   ? STextStyles.desktopTextExtraSmall(context).copyWith(
diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart
index 44c292555..1559d6007 100644
--- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart
+++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart
@@ -39,6 +39,7 @@ import 'package:stackwallet/themes/stack_colors.dart';
 import 'package:stackwallet/utilities/address_utils.dart';
 import 'package:stackwallet/utilities/amount/amount.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/assets.dart';
 import 'package:stackwallet/utilities/barcode_scanner_interface.dart';
@@ -1040,12 +1041,16 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
                 ),
           textAlign: TextAlign.right,
           inputFormatters: [
-            // 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),
+            AmountInputFormatter(
+              decimals: coin.decimals,
+              locale: 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),
           ],
           onChanged: (newValue) {},
           decoration: InputDecoration(
@@ -1097,12 +1102,16 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
                   ),
             textAlign: TextAlign.right,
             inputFormatters: [
-              // 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),
+              AmountInputFormatter(
+                decimals: 2,
+                locale: locale,
+              ),
+              // // 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,
             decoration: InputDecoration(
diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart
index 90a130476..6842708b3 100644
--- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart
+++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart
@@ -31,6 +31,7 @@ import 'package:stackwallet/themes/stack_colors.dart';
 import 'package:stackwallet/utilities/address_utils.dart';
 import 'package:stackwallet/utilities/amount/amount.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/clipboard_interface.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/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 {
   const DesktopTokenSend({
@@ -717,15 +718,22 @@ class _DesktopTokenSendState extends ConsumerState<DesktopTokenSend> {
                 ),
           textAlign: TextAlign.right,
           inputFormatters: [
+            AmountInputFormatter(
+              decimals: tokenContract.decimals,
+              locale: ref.watch(
+                localeServiceChangeNotifierProvider
+                    .select((value) => value.locale),
+              ),
+            ),
             // regex to validate a crypto amount with 8 decimal places
-            TextInputFormatter.withFunction((oldValue, newValue) => RegExp(
-                  _kCryptoAmountRegex.replaceAll(
-                    "0,8",
-                    "0,${tokenContract.decimals}",
-                  ),
-                ).hasMatch(newValue.text)
-                    ? newValue
-                    : oldValue),
+            // TextInputFormatter.withFunction((oldValue, newValue) => RegExp(
+            //       _kCryptoAmountRegex.replaceAll(
+            //         "0,8",
+            //         "0,${tokenContract.decimals}",
+            //       ),
+            //     ).hasMatch(newValue.text)
+            //         ? newValue
+            //         : oldValue),
           ],
           onChanged: (newValue) {},
           decoration: InputDecoration(
@@ -777,12 +785,19 @@ class _DesktopTokenSendState extends ConsumerState<DesktopTokenSend> {
                   ),
             textAlign: TextAlign.right,
             inputFormatters: [
-              // 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),
+              AmountInputFormatter(
+                decimals: 2,
+                locale: ref.watch(
+                  localeServiceChangeNotifierProvider
+                      .select((value) => value.locale),
+                ),
+              ),
+              // // 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,
             decoration: InputDecoration(
diff --git a/lib/utilities/amount/amount_formatter.dart b/lib/utilities/amount/amount_formatter.dart
index f9cd3f94d..7c5a8f29b 100644
--- a/lib/utilities/amount/amount_formatter.dart
+++ b/lib/utilities/amount/amount_formatter.dart
@@ -66,8 +66,6 @@ class AmountFormatter {
 
   Amount? tryParse(
     String string, {
-    required String locale,
-    required Coin coin,
     EthContract? ethContract,
   }) {
     return unit.tryParse(string, locale: locale, coin: coin);
diff --git a/lib/utilities/amount/amount_input_formatter.dart b/lib/utilities/amount/amount_input_formatter.dart
new file mode 100644
index 000000000..954187648
--- /dev/null
+++ b/lib/utilities/amount/amount_input_formatter.dart
@@ -0,0 +1,82 @@
+import 'package:flutter/services.dart';
+import 'package:intl/number_symbols.dart';
+import 'package:intl/number_symbols_data.dart';
+
+class AmountInputFormatter extends TextInputFormatter {
+  final int decimals;
+  final String locale;
+
+  AmountInputFormatter({required this.decimals, required this.locale});
+
+  @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 = "";
+      }
+
+      if (fraction.length > decimals) {
+        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,
+      ),
+    );
+  }
+}
diff --git a/lib/widgets/textfields/exchange_textfield.dart b/lib/widgets/textfields/exchange_textfield.dart
index f981f023a..4abcb6671 100644
--- a/lib/widgets/textfields/exchange_textfield.dart
+++ b/lib/widgets/textfields/exchange_textfield.dart
@@ -9,17 +9,19 @@
  */
 
 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:stackwallet/models/exchange/aggregate_currency.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/utilities/amount/amount_input_formatter.dart';
 import 'package:stackwallet/utilities/assets.dart';
 import 'package:stackwallet/utilities/text_styles.dart';
 import 'package:stackwallet/utilities/util.dart';
 import 'package:stackwallet/widgets/loading_indicator.dart';
 
-class ExchangeTextField extends StatefulWidget {
+class ExchangeTextField extends ConsumerStatefulWidget {
   const ExchangeTextField({
     Key? key,
     this.borderRadius = 0,
@@ -55,10 +57,10 @@ class ExchangeTextField extends StatefulWidget {
   final AggregateCurrency? currency;
 
   @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 FocusNode focusNode;
   late final TextStyle textStyle;
@@ -130,12 +132,17 @@ class _ExchangeTextFieldState extends State<ExchangeTextField> {
                   ),
                 ),
                 inputFormatters: [
-                  // 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),
+                  AmountInputFormatter(
+                    decimals: 8, // todo change this
+                    locale: ref.watch(localeServiceChangeNotifierProvider
+                        .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),
                 ],
               ),
             ),

From 346a6a7a0178215bb8daaeb891021b1905b31b16 Mon Sep 17 00:00:00 2001
From: julian <julian@cypherstack.com>
Date: Fri, 16 Jun 2023 17:04:39 -0600
Subject: [PATCH 4/5] account for units setting

---
 lib/pages/send_view/send_view.dart                |  1 +
 lib/pages/send_view/token_send_view.dart          |  1 +
 .../transaction_search_filter_view.dart           |  1 +
 .../wallet_view/sub_widgets/desktop_send.dart     |  1 +
 .../sub_widgets/desktop_token_send.dart           |  1 +
 lib/utilities/amount/amount_input_formatter.dart  | 15 +++++++++++++--
 6 files changed, 18 insertions(+), 2 deletions(-)

diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart
index 3f48015be..ed502f307 100644
--- a/lib/pages/send_view/send_view.dart
+++ b/lib/pages/send_view/send_view.dart
@@ -1539,6 +1539,7 @@ class _SendViewState extends ConsumerState<SendView> {
                             inputFormatters: [
                               AmountInputFormatter(
                                 decimals: coin.decimals,
+                                unit: ref.watch(pAmountUnit(coin)),
                                 locale: locale,
                               ),
 
diff --git a/lib/pages/send_view/token_send_view.dart b/lib/pages/send_view/token_send_view.dart
index 7a2a9f424..49773e92b 100644
--- a/lib/pages/send_view/token_send_view.dart
+++ b/lib/pages/send_view/token_send_view.dart
@@ -938,6 +938,7 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
                             inputFormatters: [
                               AmountInputFormatter(
                                 decimals: tokenContract.decimals,
+                                unit: ref.watch(pAmountUnit(coin)),
                                 locale: locale,
                               ),
                               // // regex to validate a crypto amount with 8 decimal places
diff --git a/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart b/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart
index e4c3e84a7..1a1513c1d 100644
--- a/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart
+++ b/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart
@@ -760,6 +760,7 @@ class _TransactionSearchViewState
               inputFormatters: [
                 AmountInputFormatter(
                   decimals: widget.coin.decimals,
+                  unit: ref.watch(pAmountUnit(widget.coin)),
                   locale: ref.watch(
                     localeServiceChangeNotifierProvider
                         .select((value) => value.locale),
diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart
index 1559d6007..d7839d98e 100644
--- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart
+++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart
@@ -1043,6 +1043,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
           inputFormatters: [
             AmountInputFormatter(
               decimals: coin.decimals,
+              unit: ref.watch(pAmountUnit(coin)),
               locale: locale,
             ),
             // // regex to validate a crypto amount with 8 decimal places
diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart
index 6842708b3..f6effead6 100644
--- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart
+++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart
@@ -720,6 +720,7 @@ class _DesktopTokenSendState extends ConsumerState<DesktopTokenSend> {
           inputFormatters: [
             AmountInputFormatter(
               decimals: tokenContract.decimals,
+              unit: ref.watch(pAmountUnit(coin)),
               locale: ref.watch(
                 localeServiceChangeNotifierProvider
                     .select((value) => value.locale),
diff --git a/lib/utilities/amount/amount_input_formatter.dart b/lib/utilities/amount/amount_input_formatter.dart
index 954187648..c3157c6fa 100644
--- a/lib/utilities/amount/amount_input_formatter.dart
+++ b/lib/utilities/amount/amount_input_formatter.dart
@@ -1,12 +1,20 @@
+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});
+  AmountInputFormatter({
+    required this.decimals,
+    required this.locale,
+    this.unit,
+  });
 
   @override
   TextEditingValue formatEditUpdate(
@@ -46,7 +54,10 @@ class AmountInputFormatter extends TextInputFormatter {
         fraction = "";
       }
 
-      if (fraction.length > decimals) {
+      final fractionDigits =
+          unit == null ? decimals : max(decimals - unit!.shift, 0);
+
+      if (fraction.length > fractionDigits) {
         return oldValue;
       }
     }

From cfd73be0c3aadbe91a868147e944e353ea7ae5d9 Mon Sep 17 00:00:00 2001
From: julian <julian@cypherstack.com>
Date: Fri, 16 Jun 2023 17:11:11 -0600
Subject: [PATCH 5/5] listen to locale changes while app is running and update
 accordingly

---
 lib/main.dart | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/lib/main.dart b/lib/main.dart
index 852ffc9c0..b39f9db64 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -463,6 +463,12 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
     super.dispose();
   }
 
+  @override
+  void didChangeLocales(List<Locale>? locales) {
+    ref.read(localeServiceChangeNotifierProvider).loadLocale();
+    super.didChangeLocales(locales);
+  }
+
   @override
   void didChangeAppLifecycleState(AppLifecycleState state) async {
     debugPrint("didChangeAppLifecycleState: ${state.name}");