diff --git a/__tests__/reducers/amountFormat.test.js b/__tests__/reducers/amountFormat.test.js
new file mode 100644
index 000000000..3354eb40d
--- /dev/null
+++ b/__tests__/reducers/amountFormat.test.js
@@ -0,0 +1,18 @@
+import { describe, it, expect } from '@jest/globals';
+import { reducer } from '../../src/reducers/reducer';
+import { setAmountFormat } from '../../src/actions';
+import { AMOUNT_FORMAT, AMOUNT_FORMAT_DEFAULT } from '../../src/constants';
+
+// initialState is not exported; derive it by initializing the reducer.
+const getInitialState = () => reducer(undefined, { type: '@@INIT' });
+
+describe('amountFormat reducer', () => {
+ it('defaults to the expanded format', () => {
+ expect(getInitialState().amountFormat).toBe(AMOUNT_FORMAT_DEFAULT);
+ });
+
+ it('sets the amount format', () => {
+ const next = reducer(getInitialState(), setAmountFormat(AMOUNT_FORMAT.COMPRESSED));
+ expect(next.amountFormat).toBe(AMOUNT_FORMAT.COMPRESSED);
+ });
+});
diff --git a/__tests__/utils/amountFormat.test.js b/__tests__/utils/amountFormat.test.js
new file mode 100644
index 000000000..2f433943d
--- /dev/null
+++ b/__tests__/utils/amountFormat.test.js
@@ -0,0 +1,149 @@
+import { describe, it, expect } from '@jest/globals';
+import {
+ compressAmountString, renderValue, getDisplayAmountFormat, normalizeAmountFormat,
+ capDecimalsByMagnitude, renderHomeValue,
+} from '../../src/utils';
+import { AMOUNT_FORMAT, AMOUNT_FORMAT_DEFAULT, AMOUNT_FORMAT_FEATURE_TOGGLE } from '../../src/constants';
+
+const stateWith = (flagOn, amountFormat) => ({
+ featureToggles: { [AMOUNT_FORMAT_FEATURE_TOGGLE]: flagOn },
+ amountFormat,
+});
+
+describe('compressAmountString', () => {
+ // Compresses only the leading fractional zero run into subscript notation and
+ // keeps every later digit verbatim (including inner and trailing zeros), so the
+ // compressed form shows the same significant digits as the Expanded form.
+ it.each([
+ ['0.0000005195', '0.0₆5195'],
+ ['0.000000012345', '0.0₇12345'],
+ ['0.00012', '0.0₃12'], // exactly 3 leading zeros (threshold)
+ ['-0.0000005195', '-0.0₆5195'], // keeps the negative sign
+ ['-0.00012', '-0.0₃12'],
+ ['0.0000005000', '0.0₆5000'], // trailing zeros are kept, not trimmed
+ ['0.00000000000050', '0.0₁₂50'], // deep run + trailing zero kept verbatim
+ ['0.000000000012', '0.0₁₀12'], // multi-digit subscript (10 zeros)
+ ['0.000000045600000123', '0.0₇45600000123'], // inner zeros kept verbatim
+ ])('compresses %s -> %s', (input, expected) => {
+ expect(compressAmountString(input)).toBe(expected);
+ });
+
+ it.each([
+ ['0.05'], // 1 leading zero, below threshold
+ ['0.0012'], // 2 leading zeros, below threshold
+ ['0.005195'], // 2 leading zeros, below threshold
+ ['1.0000005195'], // non-zero integer part
+ ['10.0000005'], // non-zero integer part
+ ['1,234.00005'], // non-zero (grouped) integer part
+ ['0.000000'], // effectively zero, nothing significant after the run
+ ['1,234'], // no fractional part
+ ['1234'], // no fractional part
+ ])('leaves %s unchanged', (input) => {
+ expect(compressAmountString(input)).toBe(input);
+ });
+});
+
+describe('capDecimalsByMagnitude', () => {
+ // < 1 -> 8, 1-999 -> 4, 1000-9999 -> 3, >= 10000 -> 2; truncated, not padded.
+ it.each([
+ ['0.12345678', '0.12345678'], // < 1, keeps up to 8
+ ['0.123456789', '0.12345678'], // < 1, truncated to 8
+ ['0.5', '0.5'], // < 1, no padding
+ ['1.12345678', '1.1234'], // 1-999 -> 4
+ ['999.98765', '999.9876'], // 999 -> 4
+ ['1000.12345', '1000.123'], // 1000-9999 -> 3
+ ['9999.9999', '9999.999'], // 9999 -> 3
+ ['10000.98765', '10000.98'], // >= 10000 -> 2
+ ['100000.12345678', '100000.12'],
+ ['9223372036854775808.12345678', '9223372036854775808.12'],
+ ['-0.123456789', '-0.12345678'], // sign preserved
+ ['1,234.56789', '1,234.567'], // grouping preserved, magnitude 1234 -> 3
+ ['1234', '1234'], // no fractional part unchanged
+ ])('%s -> %s', (input, expected) => {
+ expect(capDecimalsByMagnitude(input)).toBe(expected);
+ });
+});
+
+describe('renderValue with amount format', () => {
+ it('renders with the network decimal places it is given', () => {
+ // 12 base units at 8 decimals => 0.00000012
+ expect(renderValue(12n, false, 8)).toBe('0.00000012');
+ });
+
+ it('falls back to the lib default decimals when unset', () => {
+ // 1234 base units at default 2 decimals => 12.34
+ expect(renderValue(1234n, false)).toBe('12.34');
+ });
+
+ it('defaults to expanded (unchanged behavior)', () => {
+ expect(renderValue(1234n, false, 8)).toBe(renderValue(1234n, false, 8, AMOUNT_FORMAT.EXPANDED));
+ });
+
+ it('routes COMPRESSED output through compressAmountString', () => {
+ const amount = 500000n;
+ expect(renderValue(amount, false, 8, AMOUNT_FORMAT.COMPRESSED))
+ .toBe(compressAmountString(renderValue(amount, false, 8, AMOUNT_FORMAT.EXPANDED)));
+ });
+
+ it('compresses sub-1 amounts once the network precision allows it', () => {
+ // 5195 base units at 8 decimals => 0.00005195 => 0.0₄5195. At the lib
+ // default of 2 decimals there is no zero run long enough to compress.
+ expect(renderValue(5195n, false, 8, AMOUNT_FORMAT.COMPRESSED)).toBe('0.0₄5195');
+ });
+
+ it('is a no-op for NFT amounts (no fractional part)', () => {
+ expect(renderValue(12345n, true, 8, AMOUNT_FORMAT.COMPRESSED)).toBe('12,345');
+ });
+});
+
+describe('renderHomeValue (magnitude rule + network decimals)', () => {
+ it('renders small amounts with up to 8 decimals from the network decimal places', () => {
+ // 12 base units at 8 decimals => 0.00000012 (< 1 => 8 dp)
+ expect(renderHomeValue(12n, false, 8, AMOUNT_FORMAT.EXPANDED)).toBe('0.00000012');
+ });
+
+ it('caps large amounts to 2 decimals by magnitude', () => {
+ // 10000012345678 at 8 decimals => 100,000.12345678 (>= 10000 => 2 dp)
+ expect(renderHomeValue(10000012345678n, false, 8, AMOUNT_FORMAT.EXPANDED)).toBe('100,000.12');
+ });
+
+ it('applies Compressed notation after the magnitude cap', () => {
+ expect(renderHomeValue(12n, false, 8, AMOUNT_FORMAT.COMPRESSED)).toBe('0.0₆12');
+ });
+
+ it('falls back to the lib default decimals when unset', () => {
+ // 1234 base units at default 2 decimals => 12.34
+ expect(renderHomeValue(1234n, false)).toBe('12.34');
+ });
+});
+
+describe('getDisplayAmountFormat (flag gating)', () => {
+ it('returns the stored preference while the flag is on', () => {
+ expect(getDisplayAmountFormat(stateWith(true, AMOUNT_FORMAT.COMPRESSED)))
+ .toBe(AMOUNT_FORMAT.COMPRESSED);
+ expect(getDisplayAmountFormat(stateWith(true, AMOUNT_FORMAT.EXPANDED)))
+ .toBe(AMOUNT_FORMAT.EXPANDED);
+ });
+
+ it('forces Expanded when the flag is off, ignoring the stored preference', () => {
+ expect(getDisplayAmountFormat(stateWith(false, AMOUNT_FORMAT.COMPRESSED)))
+ .toBe(AMOUNT_FORMAT.EXPANDED);
+ });
+
+ it('falls back to the default when nothing is stored', () => {
+ expect(getDisplayAmountFormat(stateWith(true, undefined))).toBe(AMOUNT_FORMAT.EXPANDED);
+ });
+});
+
+describe('normalizeAmountFormat', () => {
+ it('passes through known enum values', () => {
+ expect(normalizeAmountFormat(AMOUNT_FORMAT.EXPANDED)).toBe(AMOUNT_FORMAT.EXPANDED);
+ expect(normalizeAmountFormat(AMOUNT_FORMAT.COMPRESSED)).toBe(AMOUNT_FORMAT.COMPRESSED);
+ });
+
+ it('falls back to the default for unknown, null or undefined values', () => {
+ expect(normalizeAmountFormat('bogus')).toBe(AMOUNT_FORMAT_DEFAULT);
+ expect(normalizeAmountFormat(null)).toBe(AMOUNT_FORMAT_DEFAULT);
+ expect(normalizeAmountFormat(undefined)).toBe(AMOUNT_FORMAT_DEFAULT);
+ });
+});
diff --git a/locale/da/texts.po b/locale/da/texts.po
index adef483ec..8cfdc6331 100644
--- a/locale/da/texts.po
+++ b/locale/da/texts.po
@@ -57,7 +57,7 @@ msgid "[Last] dddd [•] HH:mm"
msgstr "[Sidste] dddd [•] HH:mm"
#. See https://momentjs.com/docs/#/displaying/calendar-time/
-#: src/models.js:107 src/utils.js:665
+#: src/models.js:107 src/utils.js:752
msgid "DD MMM YYYY [•] HH:mm"
msgstr "DD MMM YYYY [•] HH:mm"
@@ -65,17 +65,17 @@ msgstr "DD MMM YYYY [•] HH:mm"
msgid "Invalid address"
msgstr "Ugyldig adresse"
-#: src/utils.js:650
+#: src/utils.js:737
msgid "CREATE DEPOSIT TOKEN"
msgstr ""
-#: src/utils.js:652
+#: src/utils.js:739
msgid "CREATE FEE TOKEN"
msgstr ""
#: src/screens/CreateTokenAmount.js:36 src/screens/CreateTokenConfirm.js:87
#: src/screens/CreateTokenDepositNotice.js:42
-#: src/screens/CreateTokenTypeNotice.js:94 src/utils.js:654
+#: src/screens/CreateTokenTypeNotice.js:94 src/utils.js:741
msgid "CREATE TOKEN"
msgstr "OPRET TOKEN"
@@ -215,7 +215,7 @@ msgid ""
"wallet are already in use."
msgstr ""
-#: src/screens/AddressMode.js:261
+#: src/screens/AddressMode.js:261 src/screens/AmountFormat.js:145
msgid "SAVE PREFERENCES"
msgstr ""
@@ -225,6 +225,47 @@ msgid ""
"address mode..."
msgstr ""
+#: src/screens/AmountFormat.js:83
+msgid "Default"
+msgstr ""
+
+#: src/screens/AmountFormat.js:96
+msgid "AMOUNT FORMAT"
+msgstr ""
+
+#: src/screens/AmountFormat.js:102
+msgid ""
+"Choose how amounts are displayed across your wallet. You can change your "
+"default anytime."
+msgstr ""
+
+#: src/screens/AmountFormat.js:108
+msgid "Expanded"
+msgstr ""
+
+#: src/screens/AmountFormat.js:109
+msgid ""
+"Standard notation. Leading zeros are written out in full (ex: 0.0000005195)."
+msgstr ""
+
+#: src/screens/AmountFormat.js:115
+msgid "Compressed"
+msgstr ""
+
+#: src/screens/AmountFormat.js:116
+msgid ""
+"Compresses leading zeros into a subscript count for shorter, easier-to-read "
+"small values (ex: 0.0₆5195)."
+msgstr ""
+
+#: src/screens/AmountFormat.js:121
+msgid "PREVIEW"
+msgstr ""
+
+#: src/screens/AmountFormat.js:123
+msgid "Full amount"
+msgstr ""
+
#: src/screens/BackupWords.js:184
msgid "To make sure you saved,"
msgstr "For at sikre dig, at du har gemt,"
@@ -704,7 +745,7 @@ msgid "There's been an error connecting to the server."
msgstr ""
#: src/screens/LoadWalletErrorScreen.js:28 src/screens/PinScreen.js:318
-#: src/screens/Settings.js:177
+#: src/screens/Settings.js:181
msgid "Reset wallet"
msgstr "Nulstil wallet"
@@ -712,31 +753,31 @@ msgstr "Nulstil wallet"
msgid "Connect to mainnet"
msgstr ""
-#: src/screens/MainScreen.js:136
+#: src/screens/MainScreen.js:138
msgid "No transactions"
msgstr "Ingen transaktioner"
-#: src/screens/MainScreen.js:139
+#: src/screens/MainScreen.js:141
msgid "|share:Share your address| with friends and start exchanging tokens"
msgstr "|share:Del din adresse| med venner og begynd at udveksle tokens"
-#: src/screens/MainScreen.js:156
+#: src/screens/MainScreen.js:158
msgid "Loading transactions"
msgstr "Indlæser dine transaktioner"
-#: src/screens/MainScreen.js:165
+#: src/screens/MainScreen.js:167
msgid "There was an error loading your transaction history"
msgstr ""
-#: src/screens/MainScreen.js:168
+#: src/screens/MainScreen.js:170
msgid "Please |tryAgain:try again|"
msgstr ""
-#: src/screens/MainScreen.js:551 src/screens/MainScreen.js:580
+#: src/screens/MainScreen.js:564 src/screens/MainScreen.js:597
msgid "Available Balance"
msgstr "Tilgængelig saldo"
-#: src/screens/MainScreen.js:560
+#: src/screens/MainScreen.js:573
msgid "Locked"
msgstr "Låst"
@@ -1114,15 +1155,19 @@ msgstr ""
msgid "Address Mode"
msgstr ""
-#: src/screens/Settings.js:185
+#: src/screens/Settings.js:177
+msgid "Amount format"
+msgstr ""
+
+#: src/screens/Settings.js:189
msgid "About"
msgstr "Om"
-#: src/screens/Settings.js:193
+#: src/screens/Settings.js:197
msgid "Unique app identifier"
msgstr ""
-#: src/screens/Settings.js:206
+#: src/screens/Settings.js:210
msgid "Connected to"
msgstr "Forbundet til"
@@ -1620,21 +1665,21 @@ msgstr "Transaktion"
msgid "Open"
msgstr "Åben"
-#: src/sagas/wallet.js:908
+#: src/sagas/wallet.js:917
msgid "Wallet is not ready to load addresses."
msgstr ""
#. This will show the message in the feedback content at SelectAddressModal
-#: src/sagas/wallet.js:922
+#: src/sagas/wallet.js:931
msgid "There was an error while loading wallet addresses. Try again."
msgstr ""
-#: src/sagas/wallet.js:932
+#: src/sagas/wallet.js:941
msgid "Wallet is not ready to load the first address."
msgstr ""
#. This will show the message in the feedback content
-#: src/sagas/wallet.js:948
+#: src/sagas/wallet.js:957
msgid "There was an error while loading first wallet address. Try again."
msgstr ""
diff --git a/locale/pt-br/texts.po b/locale/pt-br/texts.po
index e56992ea1..6b3c352d3 100644
--- a/locale/pt-br/texts.po
+++ b/locale/pt-br/texts.po
@@ -57,7 +57,7 @@ msgid "[Last] dddd [•] HH:mm"
msgstr "[Última] dddd [•] HH:mm"
#. See https://momentjs.com/docs/#/displaying/calendar-time/
-#: src/models.js:107 src/utils.js:665
+#: src/models.js:107 src/utils.js:752
msgid "DD MMM YYYY [•] HH:mm"
msgstr "DD MMM YYYY [•] HH:mm"
@@ -65,17 +65,17 @@ msgstr "DD MMM YYYY [•] HH:mm"
msgid "Invalid address"
msgstr "Endereço inválido"
-#: src/utils.js:650
+#: src/utils.js:737
msgid "CREATE DEPOSIT TOKEN"
msgstr "CRIAR TOKEN DE DEPÓSITO"
-#: src/utils.js:652
+#: src/utils.js:739
msgid "CREATE FEE TOKEN"
msgstr "CRIAR TOKEN DE TAXA"
#: src/screens/CreateTokenAmount.js:36 src/screens/CreateTokenConfirm.js:87
#: src/screens/CreateTokenDepositNotice.js:42
-#: src/screens/CreateTokenTypeNotice.js:94 src/utils.js:654
+#: src/screens/CreateTokenTypeNotice.js:94 src/utils.js:741
msgid "CREATE TOKEN"
msgstr "CRIAR TOKEN"
@@ -226,7 +226,7 @@ msgstr ""
"Você não pode mudar para o modo de endereço único porque outros endereços da "
"sua carteira já estão em uso."
-#: src/screens/AddressMode.js:261
+#: src/screens/AddressMode.js:261 src/screens/AmountFormat.js:145
msgid "SAVE PREFERENCES"
msgstr "SALVAR PREFERÊNCIAS"
@@ -238,6 +238,53 @@ msgstr ""
"Alterando\n"
"modo de endereço..."
+#: src/screens/AmountFormat.js:83
+msgid "Default"
+msgstr "Padrão"
+
+#: src/screens/AmountFormat.js:96
+msgid "AMOUNT FORMAT"
+msgstr "FORMATO DE VALOR"
+
+#: src/screens/AmountFormat.js:102
+msgid ""
+"Choose how amounts are displayed across your wallet. You can change your "
+"default anytime."
+msgstr ""
+"Escolha como os valores são exibidos em toda a sua carteira. Você pode "
+"alterar seu padrão a qualquer momento."
+
+#: src/screens/AmountFormat.js:108
+msgid "Expanded"
+msgstr "Expandido"
+
+#: src/screens/AmountFormat.js:109
+msgid ""
+"Standard notation. Leading zeros are written out in full (ex: 0.0000005195)."
+msgstr ""
+"Notação padrão. Os zeros à esquerda são escritos por extenso (ex: "
+"0.0000005195)."
+
+#: src/screens/AmountFormat.js:115
+msgid "Compressed"
+msgstr "Comprimido"
+
+#: src/screens/AmountFormat.js:116
+msgid ""
+"Compresses leading zeros into a subscript count for shorter, easier-to-read "
+"small values (ex: 0.0₆5195)."
+msgstr ""
+"Comprime os zeros à esquerda em uma contagem em subscrito, tornando valores "
+"pequenos mais curtos e fáceis de ler (ex: 0.0₆5195)."
+
+#: src/screens/AmountFormat.js:121
+msgid "PREVIEW"
+msgstr "PRÉVIA"
+
+#: src/screens/AmountFormat.js:123
+msgid "Full amount"
+msgstr "Valor completo"
+
#: src/screens/BackupWords.js:184
msgid "To make sure you saved,"
msgstr "Para conferir que você salvou,"
@@ -728,7 +775,7 @@ msgid "There's been an error connecting to the server."
msgstr "Ocorreu um erro ao conectar com o servidor."
#: src/screens/LoadWalletErrorScreen.js:28 src/screens/PinScreen.js:318
-#: src/screens/Settings.js:177
+#: src/screens/Settings.js:181
msgid "Reset wallet"
msgstr "Resetar Wallet"
@@ -736,33 +783,33 @@ msgstr "Resetar Wallet"
msgid "Connect to mainnet"
msgstr "Conectar à mainnet"
-#: src/screens/MainScreen.js:136
+#: src/screens/MainScreen.js:138
msgid "No transactions"
msgstr "Nenhuma transação"
-#: src/screens/MainScreen.js:139
+#: src/screens/MainScreen.js:141
msgid "|share:Share your address| with friends and start exchanging tokens"
msgstr ""
"|share:Compartilhe seu endereço| com seus amigos e comece a trocar seus "
"tokens"
-#: src/screens/MainScreen.js:156
+#: src/screens/MainScreen.js:158
msgid "Loading transactions"
msgstr "Carregando transações"
-#: src/screens/MainScreen.js:165
+#: src/screens/MainScreen.js:167
msgid "There was an error loading your transaction history"
msgstr "Ocorreu um erro ao carregar seu histórico de transações"
-#: src/screens/MainScreen.js:168
+#: src/screens/MainScreen.js:170
msgid "Please |tryAgain:try again|"
msgstr "Por favor |tryAgain:tente novamente|"
-#: src/screens/MainScreen.js:551 src/screens/MainScreen.js:580
+#: src/screens/MainScreen.js:564 src/screens/MainScreen.js:597
msgid "Available Balance"
msgstr "Saldo Disponível"
-#: src/screens/MainScreen.js:560
+#: src/screens/MainScreen.js:573
msgid "Locked"
msgstr "Bloqueado"
@@ -1143,15 +1190,19 @@ msgstr "Configurações de Rede"
msgid "Address Mode"
msgstr "Modo de Endereço"
-#: src/screens/Settings.js:185
+#: src/screens/Settings.js:177
+msgid "Amount format"
+msgstr "Formato de Valor"
+
+#: src/screens/Settings.js:189
msgid "About"
msgstr "Sobre"
-#: src/screens/Settings.js:193
+#: src/screens/Settings.js:197
msgid "Unique app identifier"
msgstr "Identificador único do aplicativo"
-#: src/screens/Settings.js:206
+#: src/screens/Settings.js:210
msgid "Connected to"
msgstr "Conectado ao servidor"
@@ -1663,21 +1714,21 @@ msgstr "Transação"
msgid "Open"
msgstr "Abrir"
-#: src/sagas/wallet.js:908
+#: src/sagas/wallet.js:917
msgid "Wallet is not ready to load addresses."
msgstr "A wallet não está pronta para carregar os endereços."
#. This will show the message in the feedback content at SelectAddressModal
-#: src/sagas/wallet.js:922
+#: src/sagas/wallet.js:931
msgid "There was an error while loading wallet addresses. Try again."
msgstr "Ocorreu um erro ao carregar os endereços da wallet. Tente novamente."
-#: src/sagas/wallet.js:932
+#: src/sagas/wallet.js:941
msgid "Wallet is not ready to load the first address."
msgstr "A wallet não está pronta para carregar o primeiro endereço."
#. This will show the message in the feedback content
-#: src/sagas/wallet.js:948
+#: src/sagas/wallet.js:957
msgid "There was an error while loading first wallet address. Try again."
msgstr ""
"Ocorreu um erro ao carregar o primeiro endereço da wallet. Tente novamente."
diff --git a/locale/ru-ru/texts.po b/locale/ru-ru/texts.po
index fdac4baaf..ebabbd04c 100644
--- a/locale/ru-ru/texts.po
+++ b/locale/ru-ru/texts.po
@@ -58,7 +58,7 @@ msgid "[Last] dddd [•] HH:mm"
msgstr "[Последний] dddd [•] HH:mm"
#. See https://momentjs.com/docs/#/displaying/calendar-time/
-#: src/models.js:107 src/utils.js:665
+#: src/models.js:107 src/utils.js:752
msgid "DD MMM YYYY [•] HH:mm"
msgstr "DD MMM YYYY [•] HH:mm"
@@ -66,17 +66,17 @@ msgstr "DD MMM YYYY [•] HH:mm"
msgid "Invalid address"
msgstr "Неправильный адрес"
-#: src/utils.js:650
+#: src/utils.js:737
msgid "CREATE DEPOSIT TOKEN"
msgstr ""
-#: src/utils.js:652
+#: src/utils.js:739
msgid "CREATE FEE TOKEN"
msgstr ""
#: src/screens/CreateTokenAmount.js:36 src/screens/CreateTokenConfirm.js:87
#: src/screens/CreateTokenDepositNotice.js:42
-#: src/screens/CreateTokenTypeNotice.js:94 src/utils.js:654
+#: src/screens/CreateTokenTypeNotice.js:94 src/utils.js:741
msgid "CREATE TOKEN"
msgstr "СОЗДАТЬ ТОКЕН"
@@ -216,7 +216,7 @@ msgid ""
"wallet are already in use."
msgstr ""
-#: src/screens/AddressMode.js:261
+#: src/screens/AddressMode.js:261 src/screens/AmountFormat.js:145
msgid "SAVE PREFERENCES"
msgstr ""
@@ -226,6 +226,47 @@ msgid ""
"address mode..."
msgstr ""
+#: src/screens/AmountFormat.js:83
+msgid "Default"
+msgstr ""
+
+#: src/screens/AmountFormat.js:96
+msgid "AMOUNT FORMAT"
+msgstr ""
+
+#: src/screens/AmountFormat.js:102
+msgid ""
+"Choose how amounts are displayed across your wallet. You can change your "
+"default anytime."
+msgstr ""
+
+#: src/screens/AmountFormat.js:108
+msgid "Expanded"
+msgstr ""
+
+#: src/screens/AmountFormat.js:109
+msgid ""
+"Standard notation. Leading zeros are written out in full (ex: 0.0000005195)."
+msgstr ""
+
+#: src/screens/AmountFormat.js:115
+msgid "Compressed"
+msgstr ""
+
+#: src/screens/AmountFormat.js:116
+msgid ""
+"Compresses leading zeros into a subscript count for shorter, easier-to-read "
+"small values (ex: 0.0₆5195)."
+msgstr ""
+
+#: src/screens/AmountFormat.js:121
+msgid "PREVIEW"
+msgstr ""
+
+#: src/screens/AmountFormat.js:123
+msgid "Full amount"
+msgstr ""
+
#: src/screens/BackupWords.js:184
msgid "To make sure you saved,"
msgstr "Чтобы убедиться в том, что вы сохранили,"
@@ -705,7 +746,7 @@ msgid "There's been an error connecting to the server."
msgstr ""
#: src/screens/LoadWalletErrorScreen.js:28 src/screens/PinScreen.js:318
-#: src/screens/Settings.js:177
+#: src/screens/Settings.js:181
msgid "Reset wallet"
msgstr "Сбросить кошелек"
@@ -713,32 +754,32 @@ msgstr "Сбросить кошелек"
msgid "Connect to mainnet"
msgstr ""
-#: src/screens/MainScreen.js:136
+#: src/screens/MainScreen.js:138
msgid "No transactions"
msgstr "Нет транзакций"
-#: src/screens/MainScreen.js:139
+#: src/screens/MainScreen.js:141
msgid "|share:Share your address| with friends and start exchanging tokens"
msgstr ""
"|share:Поделитесь своим адресом| с друзьями и начните обменивать свои токены"
-#: src/screens/MainScreen.js:156
+#: src/screens/MainScreen.js:158
msgid "Loading transactions"
msgstr "Загрузка ваших транзакций"
-#: src/screens/MainScreen.js:165
+#: src/screens/MainScreen.js:167
msgid "There was an error loading your transaction history"
msgstr ""
-#: src/screens/MainScreen.js:168
+#: src/screens/MainScreen.js:170
msgid "Please |tryAgain:try again|"
msgstr ""
-#: src/screens/MainScreen.js:551 src/screens/MainScreen.js:580
+#: src/screens/MainScreen.js:564 src/screens/MainScreen.js:597
msgid "Available Balance"
msgstr "Доступный Баланс"
-#: src/screens/MainScreen.js:560
+#: src/screens/MainScreen.js:573
msgid "Locked"
msgstr "Заблокированный"
@@ -1117,15 +1158,19 @@ msgstr ""
msgid "Address Mode"
msgstr ""
-#: src/screens/Settings.js:185
+#: src/screens/Settings.js:177
+msgid "Amount format"
+msgstr ""
+
+#: src/screens/Settings.js:189
msgid "About"
msgstr "О нас"
-#: src/screens/Settings.js:193
+#: src/screens/Settings.js:197
msgid "Unique app identifier"
msgstr ""
-#: src/screens/Settings.js:206
+#: src/screens/Settings.js:210
msgid "Connected to"
msgstr "Подключены к"
@@ -1625,21 +1670,21 @@ msgstr ""
msgid "Open"
msgstr "Открыть"
-#: src/sagas/wallet.js:908
+#: src/sagas/wallet.js:917
msgid "Wallet is not ready to load addresses."
msgstr ""
#. This will show the message in the feedback content at SelectAddressModal
-#: src/sagas/wallet.js:922
+#: src/sagas/wallet.js:931
msgid "There was an error while loading wallet addresses. Try again."
msgstr ""
-#: src/sagas/wallet.js:932
+#: src/sagas/wallet.js:941
msgid "Wallet is not ready to load the first address."
msgstr ""
#. This will show the message in the feedback content
-#: src/sagas/wallet.js:948
+#: src/sagas/wallet.js:957
msgid "There was an error while loading first wallet address. Try again."
msgstr ""
diff --git a/locale/texts.pot b/locale/texts.pot
index cb3022d20..1b889287a 100644
--- a/locale/texts.pot
+++ b/locale/texts.pot
@@ -49,7 +49,7 @@ msgid "[Last] dddd [•] HH:mm"
msgstr ""
#: src/models.js:107
-#: src/utils.js:665
+#: src/utils.js:752
#. See https://momentjs.com/docs/#/displaying/calendar-time/
msgid "DD MMM YYYY [•] HH:mm"
msgstr ""
@@ -58,11 +58,11 @@ msgstr ""
msgid "Invalid address"
msgstr ""
-#: src/utils.js:650
+#: src/utils.js:737
msgid "CREATE DEPOSIT TOKEN"
msgstr ""
-#: src/utils.js:652
+#: src/utils.js:739
msgid "CREATE FEE TOKEN"
msgstr ""
@@ -70,7 +70,7 @@ msgstr ""
#: src/screens/CreateTokenConfirm.js:87
#: src/screens/CreateTokenDepositNotice.js:42
#: src/screens/CreateTokenTypeNotice.js:94
-#: src/utils.js:654
+#: src/utils.js:741
msgid "CREATE TOKEN"
msgstr ""
@@ -211,6 +211,7 @@ msgid ""
msgstr ""
#: src/screens/AddressMode.js:261
+#: src/screens/AmountFormat.js:145
msgid "SAVE PREFERENCES"
msgstr ""
@@ -220,6 +221,46 @@ msgid ""
"address mode..."
msgstr ""
+#: src/screens/AmountFormat.js:83
+msgid "Default"
+msgstr ""
+
+#: src/screens/AmountFormat.js:96
+msgid "AMOUNT FORMAT"
+msgstr ""
+
+#: src/screens/AmountFormat.js:102
+msgid ""
+"Choose how amounts are displayed across your wallet. You can change your "
+"default anytime."
+msgstr ""
+
+#: src/screens/AmountFormat.js:108
+msgid "Expanded"
+msgstr ""
+
+#: src/screens/AmountFormat.js:109
+msgid "Standard notation. Leading zeros are written out in full (ex: 0.0000005195)."
+msgstr ""
+
+#: src/screens/AmountFormat.js:115
+msgid "Compressed"
+msgstr ""
+
+#: src/screens/AmountFormat.js:116
+msgid ""
+"Compresses leading zeros into a subscript count for shorter, easier-to-read "
+"small values (ex: 0.0₆5195)."
+msgstr ""
+
+#: src/screens/AmountFormat.js:121
+msgid "PREVIEW"
+msgstr ""
+
+#: src/screens/AmountFormat.js:123
+msgid "Full amount"
+msgstr ""
+
#: src/screens/BackupWords.js:184
msgid "To make sure you saved,"
msgstr ""
@@ -701,7 +742,7 @@ msgstr ""
#: src/screens/LoadWalletErrorScreen.js:28
#: src/screens/PinScreen.js:318
-#: src/screens/Settings.js:177
+#: src/screens/Settings.js:181
msgid "Reset wallet"
msgstr ""
@@ -709,32 +750,32 @@ msgstr ""
msgid "Connect to mainnet"
msgstr ""
-#: src/screens/MainScreen.js:136
+#: src/screens/MainScreen.js:138
msgid "No transactions"
msgstr ""
-#: src/screens/MainScreen.js:139
+#: src/screens/MainScreen.js:141
msgid "|share:Share your address| with friends and start exchanging tokens"
msgstr ""
-#: src/screens/MainScreen.js:156
+#: src/screens/MainScreen.js:158
msgid "Loading transactions"
msgstr ""
-#: src/screens/MainScreen.js:165
+#: src/screens/MainScreen.js:167
msgid "There was an error loading your transaction history"
msgstr ""
-#: src/screens/MainScreen.js:168
+#: src/screens/MainScreen.js:170
msgid "Please |tryAgain:try again|"
msgstr ""
-#: src/screens/MainScreen.js:551
-#: src/screens/MainScreen.js:580
+#: src/screens/MainScreen.js:564
+#: src/screens/MainScreen.js:597
msgid "Available Balance"
msgstr ""
-#: src/screens/MainScreen.js:560
+#: src/screens/MainScreen.js:573
msgid "Locked"
msgstr ""
@@ -1125,15 +1166,19 @@ msgstr ""
msgid "Address Mode"
msgstr ""
-#: src/screens/Settings.js:185
+#: src/screens/Settings.js:177
+msgid "Amount format"
+msgstr ""
+
+#: src/screens/Settings.js:189
msgid "About"
msgstr ""
-#: src/screens/Settings.js:193
+#: src/screens/Settings.js:197
msgid "Unique app identifier"
msgstr ""
-#: src/screens/Settings.js:206
+#: src/screens/Settings.js:210
msgid "Connected to"
msgstr ""
@@ -1633,20 +1678,20 @@ msgstr ""
msgid "Open"
msgstr ""
-#: src/sagas/wallet.js:908
+#: src/sagas/wallet.js:917
msgid "Wallet is not ready to load addresses."
msgstr ""
-#: src/sagas/wallet.js:922
+#: src/sagas/wallet.js:931
#. This will show the message in the feedback content at SelectAddressModal
msgid "There was an error while loading wallet addresses. Try again."
msgstr ""
-#: src/sagas/wallet.js:932
+#: src/sagas/wallet.js:941
msgid "Wallet is not ready to load the first address."
msgstr ""
-#: src/sagas/wallet.js:948
+#: src/sagas/wallet.js:957
#. This will show the message in the feedback content
msgid "There was an error while loading first wallet address. Try again."
msgstr ""
diff --git a/src/App.js b/src/App.js
index c03748554..7852685f2 100644
--- a/src/App.js
+++ b/src/App.js
@@ -74,6 +74,7 @@ import CreateTokenName from './screens/CreateTokenName';
import CreateTokenSymbol from './screens/CreateTokenSymbol';
import About from './screens/About';
import AddressMode from './screens/AddressMode';
+import AmountFormat from './screens/AmountFormat';
import Security from './screens/Security';
import PushNotification from './screens/PushNotification';
import ChangePin from './screens/ChangePin';
@@ -466,6 +467,7 @@ const AppStack = () => {
case 'RegisterOptions':
case 'RegisterTokenManual':
case 'AddressMode':
+ case 'AmountFormat':
case 'ImportTokensScreen':
case 'ConfirmImportScreen':
newEdges = ['bottom'];
@@ -499,6 +501,7 @@ const AppStack = () => {
+
diff --git a/src/actions.js b/src/actions.js
index 20f64ee30..e9751cd20 100644
--- a/src/actions.js
+++ b/src/actions.js
@@ -104,6 +104,7 @@ export const types = {
WALLET_REFRESH_SHARED_ADDRESS: 'WALLET_REFRESH_SHARED_ADDRESS',
SHARED_ADDRESS_UPDATE: 'SHARED_ADDRESS_UPDATE',
SET_ADDRESS_MODE: 'SET_ADDRESS_MODE',
+ SET_AMOUNT_FORMAT: 'SET_AMOUNT_FORMAT',
EXCEPTION_CAPTURED: 'EXCEPTION_CAPTURED',
SET_FEATURE_TOGGLES: 'SET_FEATURE_TOGGLES',
// Feature Toggle actions
@@ -988,6 +989,15 @@ export const setAddressMode = (mode) => ({
payload: mode,
});
+/**
+ * Set the wallet-wide amount display format
+ * @param {'expanded'|'compressed'} format
+ */
+export const setAmountFormat = (format) => ({
+ type: types.SET_AMOUNT_FORMAT,
+ payload: format,
+});
+
/**
* Stores the reown instance on the redux store
*
diff --git a/src/components/NanoContract/NanoContractTransactionActionListItem.js b/src/components/NanoContract/NanoContractTransactionActionListItem.js
index 7cc4a02a8..f60d2d12f 100644
--- a/src/components/NanoContract/NanoContractTransactionActionListItem.js
+++ b/src/components/NanoContract/NanoContractTransactionActionListItem.js
@@ -111,7 +111,8 @@ const ContentWrapper = ({ title, isAuthorityAction: isAuthority }) => {
*/
const TokenAmount = ({ amount, isNft, type }) => {
const isReceivingToken = type === NANO_CONTRACT_ACTION.withdrawal;
- const amountToRender = renderValue(amount, isNft);
+ const decimalPlaces = useSelector((state) => state.serverInfo?.decimal_places);
+ const amountToRender = renderValue(amount, isNft, decimalPlaces);
return (
diff --git a/src/components/PushTxDetailsModal.js b/src/components/PushTxDetailsModal.js
index 823563578..79600452d 100644
--- a/src/components/PushTxDetailsModal.js
+++ b/src/components/PushTxDetailsModal.js
@@ -39,11 +39,14 @@ const getTimestampFormat = (tx) => {
};
const getTokenTitle = (token) => `${token.symbol} - ${token.name}`;
-const getTokenBalance = (token, isNFT) => renderValue(token.balance, isNFT);
+const getTokenBalance = (token, isNFT, decimalPlaces) => (
+ renderValue(token.balance, isNFT, decimalPlaces)
+);
export default function PushTxDetailsModal(props) {
const { tx, tokens } = props;
const tokenMetadata = useSelector((state) => state.tokenMetadata);
+ const decimalPlaces = useSelector((state) => state.serverInfo?.decimal_places);
const dispatch = useDispatch();
const idStr = getShortHash(tx.txId, 12);
@@ -70,7 +73,9 @@ export default function PushTxDetailsModal(props) {
title={getTokenTitle(token)}
titleStyle={token.isRegistered && styles.registeredToken}
button={(
- {getTokenBalance(token, isTokenNFT(token.uid, tokenMetadata))}
+
+ {getTokenBalance(token, isTokenNFT(token.uid, tokenMetadata), decimalPlaces)}
+
)}
/>
))}
diff --git a/src/components/TokenSelect.js b/src/components/TokenSelect.js
index b3d950a42..7e66e4d04 100644
--- a/src/components/TokenSelect.js
+++ b/src/components/TokenSelect.js
@@ -15,7 +15,7 @@ import { faCircleExclamation } from '@fortawesome/free-solid-svg-icons';
import Spinner from './Spinner';
import TokenAvatar from './TokenAvatar';
-import { renderValue, isTokenNFT } from '../utils';
+import { renderValue, renderHomeValue, isTokenNFT } from '../utils';
import { TOKEN_DOWNLOAD_STATUS } from '../sagas/tokens';
import { COLORS } from '../styles/themes';
import { HathorFlatList } from './HathorFlatList';
@@ -35,12 +35,23 @@ import { HathorFlatList } from './HathorFlatList';
* @param {unknown} props.header
* @param {function} props.onItemPress
* @param {boolean} [props.ignoreLoading]
+ * @param {boolean} [props.useHomeDisplay] Apply the Home display rules
+ * (magnitude decimal cap + amount-format preference). Off elsewhere, so
+ * token-list reuse (swap, change-token) keeps regular precision.
+ * @param {string} [props.amountFormat] AMOUNT_FORMAT for the Home display
+ * (defaults to Expanded; ignored unless useHomeDisplay).
+ * @param {number} [props.decimalPlaces] Network decimal places (serverInfo.decimal_places),
+ * used by both display modes.
*/
const TokenSelect = (props) => {
const tokens = Object.values(props.tokens);
const renderItem = ({ item }) => {
const balance = get(props.tokensBalance, `${item.uid}.data.available`, 0);
const tokenState = get(props.tokensBalance, `${item.uid}.status`, props.ignoreLoading ? 'ready' : 'loading');
+ const isNFT = isTokenNFT(item.uid, props.tokenMetadata);
+ const balanceLabel = props.useHomeDisplay
+ ? renderHomeValue(balance, isNFT, props.decimalPlaces, props.amountFormat)
+ : renderValue(balance, isNFT, props.decimalPlaces);
return (
{
{tokenState === TOKEN_DOWNLOAD_STATUS.READY && (
- {renderValue(balance, isTokenNFT(item.uid, props.tokenMetadata))}
+ {balanceLabel}
)}
{tokenState === TOKEN_DOWNLOAD_STATUS.FAILED && (
diff --git a/src/components/TxDetailsModal.js b/src/components/TxDetailsModal.js
index 16a02f979..dd0dd7a84 100644
--- a/src/components/TxDetailsModal.js
+++ b/src/components/TxDetailsModal.js
@@ -106,7 +106,13 @@ class TxDetailsModal extends Component {
swipeThreshold={100}
>
-
+
@@ -159,8 +165,8 @@ class BalanceView extends Component {
});
render() {
- const { tx, isNFT } = this.props;
- const balanceStr = renderValue(tx.balance, isNFT);
+ const { tx, isNFT, amountFormat, decimalPlaces } = this.props;
+ const balanceStr = renderValue(tx.balance, isNFT, decimalPlaces, amountFormat);
return (
`${ADDRESS_MODE_KEY_PREFIX}${network}`;
+/**
+ * Amount display format (wallet-wide, network-independent).
+ */
+export const AMOUNT_FORMAT = {
+ EXPANDED: 'expanded',
+ COMPRESSED: 'compressed',
+};
+
+export const AMOUNT_FORMAT_DEFAULT = AMOUNT_FORMAT.EXPANDED;
+
+// 'wallet:' prefix: STORE.clearItems(true) sweeps it on resetWallet, so it resets to Expanded.
+export const AMOUNT_FORMAT_KEY = 'wallet:amount_format';
+
/**
* this is the message key for localization of new transaction when show amount is enabled
*/
@@ -197,6 +210,7 @@ export const SAFE_BIOMETRY_MODE_FEATURE_TOGGLE = 'safe-biometry-mode.rollout'
export const TOKEN_SWAP_FEATURE_TOGGLE = 'token-swap.rollout';
export const FBT_FEATURE_TOGGLE = 'fee-based-tokens.rollout';
export const SINGLE_ADDRESS_FEATURE_TOGGLE = 'single-address-mobile.rollout';
+export const AMOUNT_FORMAT_FEATURE_TOGGLE = 'amount-format-mobile.rollout';
/**
* Default feature toggle values.
@@ -216,6 +230,7 @@ export const FEATURE_TOGGLE_DEFAULTS = {
[TOKEN_SWAP_FEATURE_TOGGLE]: false,
[FBT_FEATURE_TOGGLE]: false,
[SINGLE_ADDRESS_FEATURE_TOGGLE]: false,
+ [AMOUNT_FORMAT_FEATURE_TOGGLE]: false,
};
// Project id configured in https://walletconnect.com
diff --git a/src/reducers/reducer.js b/src/reducers/reducer.js
index 1b1430e5f..8c0da6c3c 100644
--- a/src/reducers/reducer.js
+++ b/src/reducers/reducer.js
@@ -22,6 +22,7 @@ import {
TOKEN_SWAP_ALLOWED_TOKEN_STATUS,
TOKEN_SWAP_QUOTE_STATUS,
TOKEN_IMPORT_MODAL_STATE,
+ AMOUNT_FORMAT_DEFAULT,
} from '../constants';
import { types } from '../actions';
import { TOKEN_DOWNLOAD_STATUS } from '../sagas/tokens';
@@ -270,6 +271,7 @@ const initialState = {
lastSharedAddress: null,
lastSharedIndex: null,
addressMode: null,
+ amountFormat: AMOUNT_FORMAT_DEFAULT,
reown: {
client: null,
modal: {
@@ -694,6 +696,8 @@ export const reducer = (state = initialState, action) => {
return onSharedAddressUpdate(state, action);
case types.SET_ADDRESS_MODE:
return onSetAddressMode(state, action);
+ case types.SET_AMOUNT_FORMAT:
+ return onSetAmountFormat(state, action);
case types.SET_UNLEASH_CLIENT:
return onSetUnleashClient(state, action);
case types.SET_FEATURE_TOGGLES:
@@ -1544,6 +1548,14 @@ const onSetAddressMode = (state, action) => ({
addressMode: action.payload,
});
+/**
+ * @param {'expanded'|'compressed'} action.payload The amount display format
+ */
+const onSetAmountFormat = (state, action) => ({
+ ...state,
+ amountFormat: action.payload,
+});
+
/**
* @param {Reown} action.payload The wallet connect instance
*/
diff --git a/src/sagas/wallet.js b/src/sagas/wallet.js
index ee81ed8cd..0c2f4a83e 100644
--- a/src/sagas/wallet.js
+++ b/src/sagas/wallet.js
@@ -42,6 +42,7 @@ import {
ADDRESS_MODE,
addressModeKey,
networkSettingsKeyMap,
+ AMOUNT_FORMAT_KEY,
} from '../constants';
import { STORE } from '../store';
import {
@@ -77,6 +78,7 @@ import {
setFullNodeNetworkName,
setAddressMode,
tokenImportFetchRequested,
+ setAmountFormat,
} from '../actions';
import { fetchTokenData } from './tokens';
import {
@@ -93,6 +95,7 @@ import { monitorFeatureFlags } from './featureToggle';
import {
getAllAddresses,
getFirstAddress,
+ normalizeAmountFormat,
} from '../utils';
import { logger } from '../logger';
@@ -194,6 +197,11 @@ export function* startWallet(action) {
yield call([storage.store, storage.store.cleanMetadata]); // clean metadata on memory
yield call([storage, storage.cleanStorage], true); // clean transaction history
+ // startWallet re-runs on every unlock, so re-apply the persisted amount format
+ // onto the freshly-initialized redux state (which defaults to Expanded).
+ const storedAmountFormat = normalizeAmountFormat(STORE.getItem(AMOUNT_FORMAT_KEY));
+ yield put(setAmountFormat(storedAmountFormat));
+
// As this is a core setting for the wallet, it should be loaded first.
// Network settings either from store or redux state
let networkSettings;
diff --git a/src/screens/AmountFormat.js b/src/screens/AmountFormat.js
new file mode 100644
index 000000000..06f7b5131
--- /dev/null
+++ b/src/screens/AmountFormat.js
@@ -0,0 +1,282 @@
+/**
+ * Copyright (c) Hathor Labs and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import React, { useState } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ TouchableOpacity,
+} from 'react-native';
+import { useSelector, useDispatch } from 'react-redux';
+import { t } from 'ttag';
+import HathorHeader from '../components/HathorHeader';
+import { COLORS } from '../styles/themes';
+import { STORE } from '../store';
+import {
+ AMOUNT_FORMAT,
+ AMOUNT_FORMAT_DEFAULT,
+ AMOUNT_FORMAT_KEY,
+ DEFAULT_TOKEN,
+} from '../constants';
+import { setAmountFormat } from '../actions';
+import { compressAmountString } from '../utils';
+
+const PREVIEW_SAMPLE = '0.0000005195';
+
+/**
+ * Screen that lets the user pick the wallet-wide amount display format
+ * (Expanded or Compressed) and persist it.
+ *
+ * @param {Object} props
+ * @param {Object} props.navigation - React Navigation navigation object
+ */
+export default function AmountFormat({ navigation }) {
+ const dispatch = useDispatch();
+ const currentFormat = useSelector((state) => state.amountFormat ?? AMOUNT_FORMAT_DEFAULT);
+ const nativeTokenSymbol = useSelector(
+ (state) => state.serverInfo?.native_token?.symbol ?? DEFAULT_TOKEN.symbol
+ );
+ const [selectedFormat, setSelectedFormat] = useState(currentFormat);
+
+ const isSaveDisabled = selectedFormat === currentFormat;
+
+ const onSave = () => {
+ if (isSaveDisabled) return;
+ STORE.setItem(AMOUNT_FORMAT_KEY, selectedFormat);
+ dispatch(setAmountFormat(selectedFormat));
+ navigation.goBack();
+ };
+
+ const previewValue = selectedFormat === AMOUNT_FORMAT.COMPRESSED
+ ? compressAmountString(PREVIEW_SAMPLE)
+ : PREVIEW_SAMPLE;
+
+ const renderRadioIcon = (isSelected) => (
+
+ {isSelected && }
+
+ );
+
+ const renderOption = (format, label, description, showDefaultBadge) => {
+ const isSelected = selectedFormat === format;
+ return (
+ setSelectedFormat(format)}
+ style={styles.option}
+ >
+ {renderRadioIcon(isSelected)}
+
+
+ {label}
+ {showDefaultBadge && (
+
+ {t`Default`}
+
+ )}
+
+ {description}
+
+
+ );
+ };
+
+ return (
+
+ navigation.goBack()}
+ />
+
+
+
+ {t`Choose how amounts are displayed across your wallet. You can change your default anytime.`}
+
+
+
+ {renderOption(
+ AMOUNT_FORMAT.EXPANDED,
+ t`Expanded`,
+ t`Standard notation. Leading zeros are written out in full (ex: 0.0000005195).`,
+ true,
+ )}
+
+ {renderOption(
+ AMOUNT_FORMAT.COMPRESSED,
+ t`Compressed`,
+ t`Compresses leading zeros into a subscript count for shorter, easier-to-read small values (ex: 0.0₆5195).`,
+ false,
+ )}
+
+
+ {t`PREVIEW`}
+
+ {t`Full amount`}
+ {`${previewValue} ${nativeTokenSymbol}`}
+
+
+
+
+
+
+
+
+ {t`SAVE PREFERENCES`}
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: COLORS.white,
+ },
+ content: {
+ marginHorizontal: 16,
+ marginTop: 16,
+ },
+ intro: {
+ fontSize: 14,
+ lineHeight: 20,
+ color: COLORS.black,
+ },
+ card: {
+ marginTop: 24,
+ borderWidth: 1,
+ borderColor: COLORS.borderColor,
+ borderRadius: 16,
+ paddingHorizontal: 16,
+ paddingVertical: 24,
+ },
+ option: {
+ flexDirection: 'row',
+ alignItems: 'flex-start',
+ },
+ optionTextWrapper: {
+ flex: 1,
+ marginLeft: 16,
+ },
+ optionTitleRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ },
+ optionTitle: {
+ fontSize: 16,
+ fontWeight: '600',
+ color: COLORS.black,
+ },
+ defaultBadge: {
+ backgroundColor: COLORS.lowContrastDetail,
+ borderRadius: 12,
+ paddingHorizontal: 10,
+ paddingVertical: 2,
+ },
+ defaultBadgeText: {
+ fontSize: 12,
+ color: COLORS.darkContrastDetail,
+ },
+ optionDescription: {
+ marginTop: 4,
+ fontSize: 12,
+ lineHeight: 20,
+ color: COLORS.midContrastDetail,
+ },
+ divider: {
+ height: 1,
+ backgroundColor: COLORS.borderColor,
+ marginVertical: 24,
+ },
+ previewHeader: {
+ marginTop: 24,
+ fontSize: 14,
+ fontWeight: '600',
+ color: COLORS.textColorShadow,
+ },
+ previewPanel: {
+ marginTop: 24,
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ backgroundColor: COLORS.lowContrastDetail,
+ borderRadius: 16,
+ padding: 16,
+ },
+ previewLabel: {
+ fontSize: 14,
+ color: COLORS.darkContrastDetail,
+ },
+ previewValue: {
+ fontSize: 14,
+ fontWeight: '500',
+ color: COLORS.black,
+ },
+ bottomSpacer: {
+ flex: 1,
+ },
+ buttonContainer: {
+ marginHorizontal: 16,
+ marginBottom: 32,
+ },
+ saveButton: {
+ borderRadius: 8,
+ paddingVertical: 16,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ saveButtonDisabled: {
+ backgroundColor: COLORS.borderColorMid,
+ },
+ saveButtonActive: {
+ backgroundColor: COLORS.black,
+ },
+ saveButtonText: {
+ fontSize: 14,
+ fontWeight: '600',
+ },
+ saveButtonTextDisabled: {
+ color: COLORS.darkContrastDetail,
+ },
+ saveButtonTextActive: {
+ color: COLORS.white,
+ },
+ radioOuter: {
+ width: 24,
+ height: 24,
+ borderRadius: 12,
+ borderWidth: 2,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ radioInner: {
+ width: 12,
+ height: 12,
+ borderRadius: 6,
+ backgroundColor: COLORS.primary,
+ },
+});
diff --git a/src/screens/ChangeToken.js b/src/screens/ChangeToken.js
index 9a9036426..138d26ca1 100644
--- a/src/screens/ChangeToken.js
+++ b/src/screens/ChangeToken.js
@@ -25,6 +25,7 @@ const mapStateToProps = (state) => ({
tokens: state.tokens,
tokensBalance: state.tokensBalance,
tokenMetadata: state.tokenMetadata,
+ decimalPlaces: state.serverInfo?.decimal_places,
});
/**
@@ -61,6 +62,7 @@ class ChangeToken extends React.Component {
tokens={this.props.tokens}
tokensBalance={this.props.tokensBalance}
tokenMetadata={this.props.tokenMetadata}
+ decimalPlaces={this.props.decimalPlaces}
/>
);
}
diff --git a/src/screens/ConfirmImportScreen.js b/src/screens/ConfirmImportScreen.js
index ba87b0333..3e5ee8005 100644
--- a/src/screens/ConfirmImportScreen.js
+++ b/src/screens/ConfirmImportScreen.js
@@ -12,7 +12,7 @@ import {
import { useDispatch, useSelector } from 'react-redux';
import { useNavigation } from '@react-navigation/native';
import { t } from 'ttag';
-import { numberUtils, constants } from '@hathor/wallet-lib';
+import { numberUtils } from '@hathor/wallet-lib';
import HathorHeader from '../components/HathorHeader';
import NewHathorButton from '../components/NewHathorButton';
import ImportTokensModal from '../components/ImportTokensModal';
@@ -37,6 +37,7 @@ const ConfirmImportScreen = () => {
const { tokens } = useParams();
const importStatus = useSelector((state) => state.tokenImport.importStatus);
const tokensBalance = useSelector((state) => state.tokensBalance);
+ const decimalPlaces = useSelector((state) => state.serverInfo?.decimal_places);
const [showModal, setShowModal] = useState(false);
@@ -71,7 +72,7 @@ const ConfirmImportScreen = () => {
return `— ${token.symbol}`;
}
const available = entry.data?.available ?? 0n;
- return `${numberUtils.prettyValue(available, constants.DECIMAL_PLACES)} ${token.symbol}`;
+ return `${numberUtils.prettyValue(available, decimalPlaces)} ${token.symbol}`;
};
const renderTokenItem = ({ item, index }) => (
diff --git a/src/screens/CreateTokenAmount.js b/src/screens/CreateTokenAmount.js
index 75eeb2532..a4de7a1c0 100644
--- a/src/screens/CreateTokenAmount.js
+++ b/src/screens/CreateTokenAmount.js
@@ -148,7 +148,7 @@ const CreateTokenAmount = () => {
const amountStyle = getAmountStyle();
const amountAvailableText = (
- {hathorLib.numberUtils.prettyValue(balance.available)} {nativeSymbol}
+ {hathorLib.numberUtils.prettyValue(balance.available, decimalPlaces)} {nativeSymbol}
);
@@ -163,7 +163,7 @@ const CreateTokenAmount = () => {
if (tokenVersion === TokenVersion.FEE) {
return [
{t`Network fee:`}
- {hathorLib.numberUtils.prettyValue(networkFee)} {nativeSymbol}
+ {hathorLib.numberUtils.prettyValue(networkFee, decimalPlaces)} {nativeSymbol}
,
availableText,
{t`A small fee will be applied to each future transaction of this token.`},
@@ -172,7 +172,7 @@ const CreateTokenAmount = () => {
return [
{t`Deposit:`}
- {hathorLib.numberUtils.prettyValue(networkFee)} {nativeSymbol}
+ {hathorLib.numberUtils.prettyValue(networkFee, decimalPlaces)} {nativeSymbol}
,
availableText,
];
diff --git a/src/screens/CreateTokenConfirm.js b/src/screens/CreateTokenConfirm.js
index 28d32640c..da5774cdc 100644
--- a/src/screens/CreateTokenConfirm.js
+++ b/src/screens/CreateTokenConfirm.js
@@ -224,7 +224,7 @@ const CreateTokenConfirm = () => {
{
)}
@@ -256,7 +256,7 @@ const CreateTokenConfirm = () => {
)}
diff --git a/src/screens/Dashboard.js b/src/screens/Dashboard.js
index a235ecf48..7646f1378 100644
--- a/src/screens/Dashboard.js
+++ b/src/screens/Dashboard.js
@@ -25,7 +25,7 @@ import TokenImportBanner from '../components/TokenImportBanner';
import { COLORS } from '../styles/themes';
import { TOKEN_DOWNLOAD_STATUS } from '../sagas/tokens';
import { NanoContractsList } from '../components/NanoContract/NanoContractsList';
-import { isNanoContractsEnabled } from '../utils';
+import { isNanoContractsEnabled, getDisplayAmountFormat } from '../utils';
import ShowPushNotificationTxDetails from '../components/ShowPushNotificationTxDetails';
/**
@@ -96,6 +96,8 @@ export const Dashboard = () => {
tokensMetadata,
} = useSelector(getTokensState);
const isNanoEnabled = useSelector(isNanoContractsEnabled);
+ const amountFormat = useSelector(getDisplayAmountFormat);
+ const decimalPlaces = useSelector((state) => state.serverInfo?.decimal_places);
const [currList, selectList] = useState(listOption.tokens);
const navigation = useNavigation();
@@ -160,6 +162,9 @@ export const Dashboard = () => {
tokens={tokens}
tokensBalance={tokensBalance}
tokenMetadata={tokensMetadata}
+ useHomeDisplay
+ amountFormat={amountFormat}
+ decimalPlaces={decimalPlaces}
/>
)
}
diff --git a/src/screens/ImportTokensScreen.js b/src/screens/ImportTokensScreen.js
index 5587d4260..62f072bf9 100644
--- a/src/screens/ImportTokensScreen.js
+++ b/src/screens/ImportTokensScreen.js
@@ -18,7 +18,7 @@ import {
import { useSelector } from 'react-redux';
import { useNavigation } from '@react-navigation/native';
import { t } from 'ttag';
-import { numberUtils, constants } from '@hathor/wallet-lib';
+import { numberUtils } from '@hathor/wallet-lib';
import HathorHeader from '../components/HathorHeader';
import NewHathorButton from '../components/NewHathorButton';
import TokenListCard from '../components/TokenListCard';
@@ -40,6 +40,7 @@ const ImportTokensScreen = () => {
const loading = useSelector((state) => state.tokenImport.loading);
const tokensBalance = useSelector((state) => state.tokensBalance);
const explorerUrl = useSelector((state) => state.networkSettings.explorerUrl);
+ const decimalPlaces = useSelector((state) => state.serverInfo?.decimal_places);
// Selection lives locally; the visible list is derived from the store so any
// newly detected unregistered tokens appear without needing to re-enter the screen.
@@ -114,7 +115,7 @@ const ImportTokensScreen = () => {
return `— ${token.symbol}`;
}
const available = entry.data?.available ?? 0n;
- return `${numberUtils.prettyValue(available, constants.DECIMAL_PLACES)} ${token.symbol}`;
+ return `${numberUtils.prettyValue(available, decimalPlaces)} ${token.symbol}`;
};
const renderTokenItem = ({ item, index }) => (
diff --git a/src/screens/MainScreen.js b/src/screens/MainScreen.js
index 9ce38dc4a..164bfba92 100644
--- a/src/screens/MainScreen.js
+++ b/src/screens/MainScreen.js
@@ -27,7 +27,9 @@ import SimpleButton from '../components/SimpleButton';
import TxDetailsModal from '../components/TxDetailsModal';
import OfflineBar from '../components/OfflineBar';
import { HathorList } from '../components/HathorList';
-import { Strong, str2jsx, renderValue, isTokenNFT } from '../utils';
+import {
+ Strong, str2jsx, renderValue, isTokenNFT, getDisplayAmountFormat, renderHomeValue,
+} from '../utils';
import chevronUp from '../assets/icons/chevron-up.png';
import chevronDown from '../assets/icons/chevron-down.png';
import { IS_MULTI_TOKEN } from '../constants';
@@ -54,6 +56,8 @@ const mapStateToProps = (state) => ({
isOnline: state.isOnline,
wallet: state.wallet,
tokenMetadata: state.tokenMetadata,
+ amountFormat: getDisplayAmountFormat(state),
+ decimalPlaces: state.serverInfo?.decimal_places,
});
const mapDispatchToProps = (dispatch) => ({
@@ -114,6 +118,8 @@ class MainScreen extends React.Component {
token={this.props.selectedToken}
onRequestClose={this.closeTxDetails}
isNFT={this.isNFT()}
+ amountFormat={this.props.amountFormat}
+ decimalPlaces={this.props.decimalPlaces}
/>
);
this.setState({ modal: txDetailsModal });
@@ -197,6 +203,8 @@ class MainScreen extends React.Component {
wallet={this.props.wallet}
updateTokenHistory={this.props.updateTokenHistory}
isNFT={this.isNFT()}
+ amountFormat={this.props.amountFormat}
+ decimalPlaces={this.props.decimalPlaces}
/>
);
}
@@ -235,6 +243,8 @@ class MainScreen extends React.Component {
balance={this.props.balance}
token={this.props.selectedToken}
isNFT={this.isNFT()}
+ amountFormat={this.props.amountFormat}
+ decimalPlaces={this.props.decimalPlaces}
/>
{renderTxHistory()}
@@ -260,6 +270,8 @@ class TxHistoryView extends React.Component {
token={this.props.token}
onTxPress={this.props.onTxPress}
isNFT={this.props.isNFT}
+ amountFormat={this.props.amountFormat}
+ decimalPlaces={this.props.decimalPlaces}
/>
);
}
@@ -327,6 +339,7 @@ class TxListItem extends React.Component {
},
middleView: {
flex: 1,
+ maxWidth: '45%',
},
icon: {
marginLeft: 16,
@@ -340,6 +353,9 @@ class TxListItem extends React.Component {
balance: {
fontSize: 16,
marginRight: 16,
+ maxWidth: '45%',
+ marginLeft: 'auto',
+ textAlign: 'right',
},
description: {
fontSize: 14,
@@ -469,7 +485,12 @@ class TxListItem extends React.Component {
const style = this.getStyle(item);
const image = this.getImage(item);
- const balanceStr = renderValue(item.balance, this.props.isNFT);
+ const balanceStr = renderValue(
+ item.balance,
+ this.props.isNFT,
+ this.props.decimalPlaces,
+ this.props.amountFormat
+ );
const description = item.getDescription(this.props.token);
const { timestamp } = this.state;
return (
@@ -484,7 +505,7 @@ class TxListItem extends React.Component {
{timestamp}
{item.getVersionInfo()}
- {balanceStr}
+ {balanceStr}
);
@@ -534,8 +555,18 @@ class BalanceView extends React.Component {
}
renderExpanded() {
- const availableStr = renderValue(this.props.balance.available, this.props.isNFT);
- const lockedStr = renderValue(this.props.balance.locked, this.props.isNFT);
+ const availableStr = renderHomeValue(
+ this.props.balance.available,
+ this.props.isNFT,
+ this.props.decimalPlaces,
+ this.props.amountFormat
+ );
+ const lockedStr = renderHomeValue(
+ this.props.balance.locked,
+ this.props.isNFT,
+ this.props.decimalPlaces,
+ this.props.amountFormat
+ );
const { token } = this.props;
const { style } = this;
return (
@@ -564,7 +595,12 @@ class BalanceView extends React.Component {
}
renderSimple() {
- const availableStr = renderValue(this.props.balance.available, this.props.isNFT);
+ const availableStr = renderHomeValue(
+ this.props.balance.available,
+ this.props.isNFT,
+ this.props.decimalPlaces,
+ this.props.amountFormat
+ );
const { token } = this.props;
const { style } = this;
return (
diff --git a/src/screens/Settings.js b/src/screens/Settings.js
index 1af9644bf..0030a51d5 100644
--- a/src/screens/Settings.js
+++ b/src/screens/Settings.js
@@ -23,6 +23,7 @@ import {
NETWORK_SETTINGS_FEATURE_TOGGLE,
REOWN_FEATURE_TOGGLE,
SINGLE_ADDRESS_FEATURE_TOGGLE,
+ AMOUNT_FORMAT_FEATURE_TOGGLE,
} from '../constants';
import CopyClipboard from '../components/CopyClipboard';
import { COLORS } from '../styles/themes';
@@ -55,6 +56,7 @@ const mapStateToProps = (state) => {
reownEnabled: state.featureToggles[REOWN_FEATURE_TOGGLE] && isNanoContractsEnabled(state),
networkSettingsEnabled: state.featureToggles[NETWORK_SETTINGS_FEATURE_TOGGLE],
singleAddressEnabled: state.featureToggles[SINGLE_ADDRESS_FEATURE_TOGGLE],
+ amountFormatEnabled: state.featureToggles[AMOUNT_FORMAT_FEATURE_TOGGLE],
addressMode: state.addressMode,
};
};
@@ -173,6 +175,13 @@ export class Settings extends React.Component {
onPress={() => this.props.navigation.navigate('AddressMode')}
/>
)}
+ {this.props.amountFormatEnabled
+ && (
+ this.props.navigation.navigate('AmountFormat')}
+ />
+ )}
this.props.navigation.navigate('ResetWallet')}
diff --git a/src/utils.js b/src/utils.js
index 0fb779512..214d14d62 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -16,7 +16,7 @@ import { Linking, Platform, Text } from 'react-native';
import { getStatusBarHeight } from 'react-native-status-bar-height';
import moment from 'moment';
import baseStyle from './styles/init';
-import { KEYCHAIN_USER, NETWORK_MAINNET, NANO_CONTRACT_FEATURE_TOGGLE, SAFE_BIOMETRY_MODE_FEATURE_TOGGLE, TOKEN_SWAP_FEATURE_TOGGLE, FBT_FEATURE_TOGGLE } from './constants';
+import { KEYCHAIN_USER, NETWORK_MAINNET, NANO_CONTRACT_FEATURE_TOGGLE, SAFE_BIOMETRY_MODE_FEATURE_TOGGLE, TOKEN_SWAP_FEATURE_TOGGLE, FBT_FEATURE_TOGGLE, AMOUNT_FORMAT, AMOUNT_FORMAT_DEFAULT, AMOUNT_FORMAT_FEATURE_TOGGLE } from './constants';
import { STORE, IS_BIOMETRY_ENABLED_KEY, IS_OLD_BIOMETRY_ENABLED_KEY, SUPPORTED_BIOMETRY_KEY, SAFE_BIOMETRY_FEATURE_FLAG_KEY, FEATURE_TOGGLES_LAST_KNOWN_VALUES_KEY } from './store';
import { TxHistory } from './models';
import { COLORS, STYLE } from './styles/themes';
@@ -372,21 +372,190 @@ export const getLightBackground = (alpha) => {
return `${COLORS.primary}${hex}`;
};
+const SUBSCRIPT_DIGITS = ['₀', '₁', '₂', '₃', '₄', '₅', '₆', '₇', '₈', '₉'];
+
+// Shortest zero run worth compressing.
+const COMPRESS_MIN_ZERO_RUN = 3;
+
/**
- * Render value in a formatted way for display
+ * Render a non-negative integer as a Unicode subscript string (e.g. 12 -> "₁₂").
+ *
+ * @param {number} n
+ * @return {string}
+ */
+const toSubscript = (n) => String(n)
+ .split('')
+ .map((digit) => SUBSCRIPT_DIGITS[Number(digit)])
+ .join('');
+
+/**
+ * Collapse the leading run of fractional zeros into subscript notation,
+ * e.g. "0.0000005195" -> "0.0₆5195". Only that leading run is compressed; every
+ * later digit is kept verbatim, including trailing zeros ("0.0000005000" ->
+ * "0.0₆5000"), so the significant digits match the Expanded form. Returns the
+ * input unchanged when nothing qualifies: value >= 1, integer, zero, or a run
+ * shorter than COMPRESS_MIN_ZERO_RUN.
+ *
+ * @param {string} formatted Amount string from prettyValue
+ * @return {string}
+ */
+export const compressAmountString = (formatted) => {
+ const isNegative = formatted.startsWith('-');
+ const unsigned = isNegative ? formatted.slice(1) : formatted;
+
+ const dotIndex = unsigned.indexOf('.');
+ if (dotIndex === -1) {
+ return formatted;
+ }
+
+ const integerPart = unsigned.slice(0, dotIndex);
+ const decimalPart = unsigned.slice(dotIndex + 1);
+
+ // Only sub-1 values have a leading-zero run worth compressing.
+ if (integerPart.replace(/,/g, '') !== '0') {
+ return formatted;
+ }
+
+ // Count the leading zero run only (trailing zeros are intentionally preserved).
+ let leadingZeros = 0;
+ while (leadingZeros < decimalPart.length && decimalPart[leadingZeros] === '0') {
+ leadingZeros += 1;
+ }
+
+ // Nothing significant after the run (value is zero) or the run is too short.
+ const significant = decimalPart.slice(leadingZeros);
+ if (significant === '' || leadingZeros < COMPRESS_MIN_ZERO_RUN) {
+ return formatted;
+ }
+
+ const sign = isNegative ? '-' : '';
+ return `${sign}0.0${toSubscript(leadingZeros)}${significant}`;
+};
+
+/**
+ * Render value in a formatted way for display.
*
* @param {bigint} amount The token amount as BigInt
- * @param {boolean} isInteger Whether the token is an NFT or regular token
+ * @param {boolean} isNFT Whether the token is an NFT (rendered as an integer)
+ * @param {number} [decimalPlaces] Network decimal places (serverInfo.decimal_places).
+ * Callers with store access should always pass it: the lib default it falls back
+ * to is 2, which under-renders every network that uses more precision (and, with
+ * only two fractional digits, makes the Compressed format a no-op).
+ * @param {string} [amountFormat] AMOUNT_FORMAT.EXPANDED (default) or COMPRESSED
* @return {string} Formatted value for display
*/
-export const renderValue = (amount, isInteger) => {
- if (isInteger) {
- return hathorLib.numberUtils.prettyValue(amount, 0);
+export const renderValue = (
+ amount,
+ isNFT,
+ decimalPlaces,
+ amountFormat = AMOUNT_FORMAT.EXPANDED,
+) => {
+ const formatted = isNFT
+ ? hathorLib.numberUtils.prettyValue(amount, 0)
+ : hathorLib.numberUtils.prettyValue(amount, decimalPlaces);
+
+ if (amountFormat === AMOUNT_FORMAT.COMPRESSED) {
+ return compressAmountString(formatted);
}
- return hathorLib.numberUtils.prettyValue(amount);
+ return formatted;
};
+/**
+ * Home display precision rule: cap the shown fractional digits by the value's
+ * integer magnitude — < 1 -> 8, 1-999 -> 4, 1000-9999 -> 3, >= 10000 -> 2.
+ * Excess digits are truncated (not rounded, to never overstate a balance);
+ * shorter fractions are left as-is (no padding). Operates on prettyValue's output
+ * string, so it composes with compressAmountString.
+ *
+ * @param {string} formatted Amount string from prettyValue
+ * @return {string}
+ */
+export const capDecimalsByMagnitude = (formatted) => {
+ const isNegative = formatted.startsWith('-');
+ const unsigned = isNegative ? formatted.slice(1) : formatted;
+
+ const dotIndex = unsigned.indexOf('.');
+ if (dotIndex === -1) {
+ return formatted;
+ }
+
+ const integerPart = unsigned.slice(0, dotIndex);
+ const decimalPart = unsigned.slice(dotIndex + 1);
+ const integerDigits = integerPart.replace(/,/g, '');
+
+ let maxDecimals;
+ if (integerDigits === '0') {
+ // Assumes network precision <= 8: on a network with decimal_places > 8, a
+ // balance below 1e-8 truncates to all-zeros and renders as 0.
+ maxDecimals = 8;
+ } else if (integerDigits.length <= 3) {
+ maxDecimals = 4;
+ } else if (integerDigits.length === 4) {
+ maxDecimals = 3;
+ } else {
+ maxDecimals = 2;
+ }
+
+ const capped = decimalPart.slice(0, maxDecimals);
+ const sign = isNegative ? '-' : '';
+ return capped === '' ? `${sign}${integerPart}` : `${sign}${integerPart}.${capped}`;
+};
+
+/**
+ * Home value display: format the amount via prettyValue using the network's
+ * decimal places, cap the shown decimals by magnitude, then apply the Compressed
+ * subscript notation when selected.
+ *
+ * @param {bigint} amount The token amount as BigInt
+ * @param {boolean} isNFT Whether the token is an NFT (rendered as an integer)
+ * @param {number} [decimalPlaces] Network decimal places (serverInfo.decimal_places);
+ * falls back to the lib default when unset
+ * @param {string} [amountFormat] AMOUNT_FORMAT.EXPANDED (default) or COMPRESSED
+ * @return {string}
+ */
+export const renderHomeValue = (
+ amount,
+ isNFT,
+ decimalPlaces,
+ amountFormat = AMOUNT_FORMAT.EXPANDED,
+) => {
+ const formatted = isNFT
+ ? hathorLib.numberUtils.prettyValue(amount, 0)
+ : hathorLib.numberUtils.prettyValue(amount, decimalPlaces);
+ const capped = capDecimalsByMagnitude(formatted);
+ return amountFormat === AMOUNT_FORMAT.COMPRESSED ? compressAmountString(capped) : capped;
+};
+
+/**
+ * Coerce a persisted amount-format value to a known enum member, defaulting for
+ * anything unrecognized (e.g. a corrupted storage read). Guards the hydration
+ * path, where the value comes from untrusted storage rather than the picker.
+ *
+ * @param {unknown} value
+ * @return {string} A valid AMOUNT_FORMAT value
+ */
+export const normalizeAmountFormat = (value) => (
+ Object.values(AMOUNT_FORMAT).includes(value) ? value : AMOUNT_FORMAT_DEFAULT
+);
+
+/**
+ * Amount format to render with, gated by the feature flag.
+ *
+ * `state.amountFormat` is the user's stored preference; this returns it only
+ * while the flag is on, else forces Expanded. That stops a remote flag rollback
+ * from stranding the wallet in Compressed with the Settings entry hidden (no
+ * in-app way back). Display sites should read this, not state.amountFormat.
+ *
+ * @param {Object} state Redux store state
+ * @return {string} AMOUNT_FORMAT.EXPANDED or AMOUNT_FORMAT.COMPRESSED
+ */
+export const getDisplayAmountFormat = (state) => (
+ state.featureToggles[AMOUNT_FORMAT_FEATURE_TOGGLE]
+ ? (state.amountFormat ?? AMOUNT_FORMAT_DEFAULT)
+ : AMOUNT_FORMAT.EXPANDED
+);
+
/**
* Format a BigInt amount as a decimal string without thousand separators.
* Useful for input fields where thousand separators are not allowed.
diff --git a/src/utils/tokenSwap.js b/src/utils/tokenSwap.js
index 6b9908539..9645e676d 100644
--- a/src/utils/tokenSwap.js
+++ b/src/utils/tokenSwap.js
@@ -355,9 +355,10 @@ export function selectTokenSwapContractId(state) {
* Render amount and token symbol for UI components.
* @param {number|bigint} amount
* @param {TokenData} token
+ * @param {number} [decimalPlaces] Network decimal places (serverInfo.decimal_places)
*/
-export function renderAmountAndSymbol(amount, token) {
- return `${renderValue(amount, false)} ${token.symbol}`;
+export function renderAmountAndSymbol(amount, token, decimalPlaces) {
+ return `${renderValue(amount, false, decimalPlaces)} ${token.symbol}`;
}
/**
@@ -367,10 +368,17 @@ export function renderAmountAndSymbol(amount, token) {
* @param {number|bigint} amount
* @param {TokenData} token
* @param {number} slippage
+ * @param {number} [decimalPlaces] Network decimal places (serverInfo.decimal_places)
*/
-export function renderAmountAndSymbolWithSlippage(direction, amount, token, slippage) {
+export function renderAmountAndSymbolWithSlippage(
+ direction,
+ amount,
+ token,
+ slippage,
+ decimalPlaces,
+) {
const newAmount = calcAmountWithSlippage(direction, amount, slippage);
- return renderAmountAndSymbol(newAmount, token);
+ return renderAmountAndSymbol(newAmount, token, decimalPlaces);
}
/**
@@ -378,21 +386,25 @@ export function renderAmountAndSymbolWithSlippage(direction, amount, token, slip
* @param {TokenSwapQuote} quote
* @param {TokenData} inToken
* @param {TokenData} outToken
+ * @param {number} [decimalPlaces] Network decimal places (serverInfo.decimal_places)
*/
-export function renderConversionRate(quote, inToken, outToken) {
+export function renderConversionRate(quote, inToken, outToken, decimalPlaces) {
if (!quote) {
return null;
}
+ // Reference amount the rate is quoted against: one whole token in the
+ // network's smallest-unit scale.
+ const oneToken = 10 ** (decimalPlaces ?? hathorLib.constants.DECIMAL_PLACES);
if ((!quote.amount_in) || quote.amount_in === 0) {
// Invalid but we should avoid rendering errors
- return `${renderAmountAndSymbol(100, outToken)} = ${renderAmountAndSymbol(0, inToken)}`;
+ return `${renderAmountAndSymbol(oneToken, outToken, decimalPlaces)} = ${renderAmountAndSymbol(0, inToken, decimalPlaces)}`;
}
- let exchangeRate = (100 * Number(quote.amount_out)) / Number(quote.amount_in);
+ let exchangeRate = (oneToken * Number(quote.amount_out)) / Number(quote.amount_in);
let extraRate = 1;
/**
* This loop will gradually increase the reference so we do not use fractional rates.
- * The cutoff for the reference value is 1000.00
+ * The cutoff for the reference is 1000 whole tokens.
*/
while (!Number.isInteger(exchangeRate)) {
if (extraRate === 1000) {
@@ -402,7 +414,7 @@ export function renderConversionRate(quote, inToken, outToken) {
extraRate *= 10;
exchangeRate *= 10;
}
- return `${renderAmountAndSymbol(exchangeRate, outToken)} = ${renderAmountAndSymbol(100 * extraRate, inToken)}`;
+ return `${renderAmountAndSymbol(exchangeRate, outToken, decimalPlaces)} = ${renderAmountAndSymbol(oneToken * extraRate, inToken, decimalPlaces)}`;
}
/**
diff --git a/src/workers/pushNotificationHandler.js b/src/workers/pushNotificationHandler.js
index 043a84fec..0bffc869d 100644
--- a/src/workers/pushNotificationHandler.js
+++ b/src/workers/pushNotificationHandler.js
@@ -38,6 +38,11 @@ export function setNotificationError(error) {
/**
* Render the balance value in decimal format.
+ *
+ * Runs in the background message handler, where the Redux store is not
+ * available, so it cannot pass the network's decimal places and falls back to
+ * the wallet-lib default.
+ *
* @param {string} tokenBalance
* @returns {string} - the rendered balance value with the token symbol.
* @example