Page MenuHomePhabricator

[Cashtab] Locale thousands separators and decimals in number inputs
ClosedPublic

Authored by bytesofman on Jul 15 2026, 19:45.

Details

Summary

Format amount/price fields with locale grouping and decimal
markers (like apps/firma), and normalize locale input before validation
and send math so non-en-US users can enter amounts naturally.

Test Plan

npm test

  • Manually: in en-US type 1234.5 → see 1,234.5; in de-DE type 1234,5 → see 1.234,5; max/bip21 amounts still send correctly

Diff Detail

Repository
rABC Bitcoin ABC
Lint
Lint Not Applicable
Unit
Tests Not Applicable

Event Timeline

Failed tests logs:

====== CashTab Unit Tests: <OrderBook /> We can see a rendered offer ======
TestingLibraryElementError: Unable to find an element with the text: 1.7 CACHET. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.

Ignored nodes: comments, script, style
<body>
  <div>
    <section
      aria-atomic="false"
      aria-label="Notifications"
      aria-live="polite"
      aria-relevant="additions text"
      class="Toastify"
    />
    <div>
      Test
    </div>
    <div
      class="sc-elJkPf lcZeYn"
    >
      <div
        class="sc-jtRfpW jPFUbw"
      >
        <button
          aria-label="View larger icon for Cachet"
          class="sc-dfVpRl jYFaTN"
          size="64"
          title="aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1"
        />
        <div
          class="sc-dqBHgY eekOiO"
        >
          <a
            class="sc-gxMtzJ hiCoVB"
            font-size="var(--text-2xl)"
            href="#/token/aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1"
          >
            Cachet
          </a>
          <span>
            CACHET
          </span>
          <div
            class="sc-lhVmIH cfuLzI"
          >
            aed
            ...
            cb1
            <button
              aria-label="Copy Token ID"
              class="sc-jqCOkK jKBkhX"
            >
              <svg
                title="copy-paste"
              />
            </button>
          </div>
        </div>
        <div
          class="sc-kTUwUJ elfyii"
        >
          <div>
            1
             Offer
          </div>
          <div
            class="sc-fAjcbJ fOpxfV"
          >
            <div
              class="sc-caSCKo gYoTCj"
            >
              <input
                class="sc-TOsTZ SvZry"
                id="Toggle price for aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1"
                name="Toggle price for aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1"
                title="Toggle price for aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1"
                type="checkbox"
              />
              <label
                class="sc-gisBJw bNwAUh"
                for="Toggle price for aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1"
              >
                <span
                  class="sc-kjoXOD kybLSq"
                  data-off="XEC"
                  data-on="usd"
                />
                <span
                  class="sc-cHGsZl fYqMqO"
                />
              </label>
            </div>
          </div>
        </div>
      </div>
      <div
        class="sc-gzOgki kgmzuS"
      >
        <div
          class="sc-iyvyFf hbSTMH"
        >
          <button
            class="sc-hwwEjo OCCfh"
            data-tooltip-content="100.00 CACHET (100.00 total)"
            data-tooltip-id="cashtab-tooltip"
          >
            <div
              class="sc-kfGgVZ fMVgTU"
            />
            <div
              class="sc-esjQYD evNLSa"
            />
            <div
              class="sc-kPVwWT fePPDZ"
            >
              $0.3300 USD
            </div>
          </button>
        </div>
        <div
          class="sc-epnACN dDVIbm"
        >
          <span
            class="sc-kIPQKe dENjKC"
          >
            Quantity
          </span>
          <div
            class="sc-iQNlJl jfOdIZ"
          >
            <div
              class="sc-cSHVUG fLNrXC"
            >
              <input
                aria-labelledby="Select buy qty aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1"
                class="sc-iRbamj deoJtU"
                max="100.00"
                min=".10"
                name="Select buy qty aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1"
                step="0.01"
                type="range"
                value="1.70"
              />
              <div
                class="sc-csuQGl bEcGlB"
              >
                <input
                  class="sc-jlyJG kRLVJP"
                  inputmode="decimal"
                  name="Select buy qty aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1-typed"
                  placeholder="Select buy qty aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1"
                  type="text"
                  value="1.70"
                />
              </div>
              <div
                class="sc-eHgmQL doLYEs"
              >
                Buy price (17k XEC) exceeds available balance (4.2k XEC).
              </div>
            </div>
          </div>
        </div>
        <div
          class="sc-eXEjpC kkNpfk"
        >
          <div
            class="sc-ibxdXY hGKGGM"
          >
            <span
              class="sc-RefOD kjaTsL"
            >
              Buying
            </span>
            <span
              class="sc-iQKALj kpBiQm"
            >
              1.70
               
              CACHET
            </span>
          </div>
          <div
            class="sc-ibxdXY hGKGGM"
          >
            <span
              class="sc-RefOD kjaTsL"
            >
              For
            </span>
            <span
              class="sc-bwCtUz fFRXGH"
            >
              $0.5611 USD
            </span>
          </div>
          <button
            class="sc-hmzhuo sc-jbKcbu idWQse"
          >
            Cancel your offer
          </button>
        </div>
      </div>
    </div>
  </div>
</body>
    at Object.getElementError (/work/node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/dist/config.js:37:19)
    at /work/node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/dist/query-helpers.js:76:38
    at /work/node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/dist/query-helpers.js:52:17
    at /work/node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/dist/query-helpers.js:95:19
    at Object.getByText (/work/cashtab/src/components/Agora/OrderBook/__tests__/index.test.tsx:295:23)
====== CashTab Unit Tests: <OrderBook /> We can see a rendered offer in an OrderBook with noIcon ======
TestingLibraryElementError: Unable to find an element with the text: 1.7 CACHET. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.

Ignored nodes: comments, script, style
<body>
  <div>
    <section
      aria-atomic="false"
      aria-label="Notifications"
      aria-live="polite"
      aria-relevant="additions text"
      class="Toastify"
    />
    <div>
      Test
    </div>
    <div
      class="sc-elJkPf jgTkeW"
    >
      <div
        class="sc-jtRfpW jTrLnW"
      >
        <div
          class="sc-kTUwUJ kCgeGF"
        >
          <div>
            1
             Offer
          </div>
          <div
            class="sc-fAjcbJ fOpxfV"
          >
            <div
              class="sc-caSCKo gYoTCj"
            >
              <input
                class="sc-TOsTZ SvZry"
                id="Toggle price for aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1"
                name="Toggle price for aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1"
                title="Toggle price for aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1"
                type="checkbox"
              />
              <label
                class="sc-gisBJw bNwAUh"
                for="Toggle price for aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1"
              >
                <span
                  class="sc-kjoXOD kybLSq"
                  data-off="XEC"
                  data-on="usd"
                />
                <span
                  class="sc-cHGsZl fYqMqO"
                />
              </label>
            </div>
          </div>
        </div>
      </div>
      <div
        class="sc-gzOgki kgmzuS"
      >
        <div
          class="sc-iyvyFf cBHUNz"
        >
          <button
            class="sc-hwwEjo OCCfh"
            data-tooltip-content="100.00 CACHET (100.00 total)"
            data-tooltip-id="cashtab-tooltip"
          >
            <div
              class="sc-kfGgVZ fMVgTU"
            />
            <div
              class="sc-esjQYD evNLSa"
            />
            <div
              class="sc-kPVwWT fePPDZ"
            >
              $0.3300 USD
            </div>
          </button>
        </div>
        <div
          class="sc-epnACN dDVIbm"
        >
          <span
            class="sc-kIPQKe dENjKC"
          >
            Quantity
          </span>
          <div
            class="sc-iQNlJl jfOdIZ"
          >
            <div
              class="sc-cSHVUG fLNrXC"
            >
              <input
                aria-labelledby="Select buy qty aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1"
                class="sc-iRbamj deoJtU"
                max="100.00"
                min=".10"
                name="Select buy qty aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1"
                step="0.01"
                type="range"
                value="1.70"
              />
              <div
                class="sc-csuQGl bEcGlB"
              >
                <input
                  class="sc-jlyJG kRLVJP"
                  inputmode="decimal"
                  name="Select buy qty aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1-typed"
                  placeholder="Select buy qty aed861a31b96934b88c0252ede135cb9700d7649f69191235087a3030e553cb1"
                  type="text"
                  value="1.70"
                />
              </div>
              <div
                class="sc-eHgmQL doLYEs"
              >
                Buy price (17k XEC) exceeds available balance (4.2k XEC).
              </div>
            </div>
          </div>
        </div>
        <div
          class="sc-eXEjpC kkNpfk"
        >
          <div
            class="sc-ibxdXY hGKGGM"
          >
            <span
              class="sc-RefOD kjaTsL"
            >
              Buying
            </span>
            <span
              class="sc-iQKALj kpBiQm"
            >
              1.70
               
              CACHET
            </span>
          </div>
          <div
            class="sc-ibxdXY hGKGGM"
          >
            <span
              class="sc-RefOD kjaTsL"
            >
              For
            </span>
            <span
              class="sc-bwCtUz fFRXGH"
            >
              $0.5611 USD
            </span>
          </div>
          <button
            class="sc-hmzhuo sc-jbKcbu idWQse"
          >
            Cancel your offer
          </button>
        </div>
      </div>
    </div>
  </div>
</body>
    at Object.getElementError (/work/node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/dist/config.js:37:19)
    at /work/node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/dist/query-helpers.js:76:38
    at /work/node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/dist/query-helpers.js:52:17
    at /work/node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/dist/query-helpers.js:95:19
    at Object.getByText (/work/cashtab/src/components/Agora/OrderBook/__tests__/index.test.tsx:424:23)
====== CashTab Unit Tests: <OrderBook /> We can buy an offer ======
Error: expect(element).toHaveValue(0.1)

Expected the element to have value:
  0.1 (number)
Received:
  .10 (string)
    at Object.toHaveValue (/work/cashtab/src/components/Agora/OrderBook/__tests__/index.test.tsx:702:38)
====== CashTab Unit Tests: <OrderBook /> We can type input and see a previewed offer of actual input with a delta from our typed input ======
Error: expect(element).toHaveValue(960000)

Expected the element to have value:
  960000 (number)
Received:
  960000.00 (string)
    at Object.toHaveValue (/work/cashtab/src/components/Agora/OrderBook/__tests__/index.test.tsx:946:36)
====== CashTab Unit Tests: <App /> Clicking "reply" on a Cashtab Msg correctly populates the SendXec to address and amount fields ======
Error: expect(element).toHaveValue(5.46)

Expected the element to have value:
  5.46 (number)
Received:
  5.46 (string)
    at Object.toHaveValue (/work/cashtab/src/components/App/__tests__/App.test.js:449:55)
====== CashTab Unit Tests: <CreateTokenForm /> Validation works as expected for ALP and SLP max supply ======
Error: expect(element).toHaveValue(18446744073709551615)

Expected the element to have value:
  18446744073709551615
Received:
  18,446,744,073,709,551,615
    at Object.toHaveValue (/work/cashtab/src/components/Etokens/__tests__/CreateTokenForm.test.tsx:711:38)
====== CashTab Unit Tests: <Token /> available actions rendered We can list an SLP1 fungible token ======
Error: expect(element).toHaveValue(5460)

Expected the element to have value:
  5460 (number)
Received:
  5460 (string)
    at Object.toHaveValue (/work/cashtab/src/components/Etokens/__tests__/TokenActions.test.js:333:29)
====== CashTab Unit Tests: <Token /> available actions rendered We can list an SLP1 NFT ======
Error: expect(element).toHaveValue(null)

Expected the element to have value:
  null
Received:

    at Object.toHaveValue (/work/cashtab/src/components/Etokens/__tests__/TokenActions.test.js:1036:28)
====== CashTab Unit Tests: <Token /> available actions rendered We can list an ALP fungible token ======
Error: expect(element).toHaveValue(5460)

Expected the element to have value:
  5460 (number)
Received:
  5460 (string)
    at Object.toHaveValue (/work/cashtab/src/components/Etokens/__tests__/TokenActions.test.js:1927:29)
====== CashTab Unit Tests: <Token /> available actions rendered We can redeem XECX for XEC 1:1 using a workflow unique to XECX ======
Error: expect(element).toHaveValue(10000)

Expected the element to have value:
  10000
Received:
  10,000
    at Object.toHaveValue (/work/cashtab/src/components/Etokens/__tests__/TokenActions.test.js:2145:58)
====== CashTab Unit Tests: <Token /> available actions rendered We see expected alert in XECX redemption workflow for hot wallet balance ======
Error: expect(element).toHaveValue(10000)

Expected the element to have value:
  10000
Received:
  10,000
    at Object.toHaveValue (/work/cashtab/src/components/Etokens/__tests__/TokenActions.test.js:2253:58)
====== CashTab Unit Tests: <Token /> available actions rendered We DO NOT see expected alert in XECX redemption workflow for hot wallet balance if there is some error determining the hot wallet balance ======
Error: expect(element).toHaveValue(10000)

Expected the element to have value:
  10000
Received:
  10,000
    at Object.toHaveValue (/work/cashtab/src/components/Etokens/__tests__/TokenActions.test.js:2346:58)
====== CashTab Unit Tests: <Token /> available actions rendered We can redeem 1 Firma for $1 of XEC using a workflow unique to Firma ======
Error: expect(element).toHaveValue(10)

Expected the element to have value:
  10 (number)
Received:
  10 (string)
    at Object.toHaveValue (/work/cashtab/src/components/Etokens/__tests__/TokenActions.test.js:2480:58)
====== CashTab Unit Tests: <Token /> available actions rendered We show expected error if we are unable to get FIRMA bid price from API ======
Error: expect(element).toHaveValue(10)

Expected the element to have value:
  10 (number)
Received:
  10 (string)
    at Object.toHaveValue (/work/cashtab/src/components/Etokens/__tests__/TokenActions.test.js:2741:58)
====== CashTab Unit Tests: <Token /> available actions rendered We can list a MINT VAULT fungible token ======
Error: expect(element).toHaveValue(6)

Expected the element to have value:
  6 (number)
Received:
  6 (string)
    at Object.toHaveValue (/work/cashtab/src/components/Etokens/__tests__/TokenActions.test.js:2993:29)

Each failure log is accessible here:
CashTab Unit Tests: <OrderBook /> We can see a rendered offer
CashTab Unit Tests: <OrderBook /> We can see a rendered offer in an OrderBook with noIcon
CashTab Unit Tests: <OrderBook /> We can buy an offer
CashTab Unit Tests: <OrderBook /> We can type input and see a previewed offer of actual input with a delta from our typed input
CashTab Unit Tests: <App /> Clicking "reply" on a Cashtab Msg correctly populates the SendXec to address and amount fields
CashTab Unit Tests: <CreateTokenForm /> Validation works as expected for ALP and SLP max supply
CashTab Unit Tests: <Token /> available actions rendered We can list an SLP1 fungible token
CashTab Unit Tests: <Token /> available actions rendered We can list an SLP1 NFT
CashTab Unit Tests: <Token /> available actions rendered We can list an ALP fungible token
CashTab Unit Tests: <Token /> available actions rendered We can redeem XECX for XEC 1:1 using a workflow unique to XECX
CashTab Unit Tests: <Token /> available actions rendered We see expected alert in XECX redemption workflow for hot wallet balance
CashTab Unit Tests: <Token /> available actions rendered We DO NOT see expected alert in XECX redemption workflow for hot wallet balance if there is some error determining the hot wallet balance
CashTab Unit Tests: <Token /> available actions rendered We can redeem 1 Firma for $1 of XEC using a workflow unique to Firma
CashTab Unit Tests: <Token /> available actions rendered We show expected error if we are unable to get FIRMA bid price from API
CashTab Unit Tests: <Token /> available actions rendered We can list a MINT VAULT fungible token

Fix OrderBook qty-delta math and remaining locale input test expectations.

Normalize take qty before BigNumber delta/target display; update OrderBook,
TokenActions, App, and CreateTokenForm assertions for string/locale-formatted
input values. Full cashtab suite green locally (CI=true pnpm test).

bytesofman edited the test plan for this revision. (Show Details)
Fabien added a subscriber: Fabien.
Fabien added inline comments.
cashtab/src/components/Agora/OrderBook/__tests__/index.test.tsx
779 ↗(On Diff #60287)

this is equivalent ?

cashtab/src/components/Common/Inputs.tsx
21 ↗(On Diff #60287)

I trust you tested that live because it's not obvious this doesn't create confusion, my understanding is that it's changing the content as you type

cashtab/src/validation/index.ts
102 ↗(On Diff #60287)

This is now guaranteed

This revision is now accepted and ready to land.Jul 16 2026, 14:29
bytesofman added inline comments.
cashtab/src/components/Agora/OrderBook/__tests__/index.test.tsx
779 ↗(On Diff #60287)

I don't think so actually. If there are more than one, we get an array instead of the entry, and react test library won't pass it.

this is still a bit of a code smell; I prefer to expect the exact length instead of "there should be at least one". This helps to better track later changes in behavior.

But in this case, "toBeGreaterThanOrEqual(1)" is the exact test as before. Probably there should be no change. I'll take a look locally before landing.

cashtab/src/components/Common/Inputs.tsx
21 ↗(On Diff #60287)

Yes, from my testing it just works. A similar implementation is on the firma app. But there could still be some experience edge cases for special locales. Not guaranteed to be perfect here but on balance I think it's better than nothing, especially since typical txs involve amounts in the millions and it's quite easy to be 10x off without thousands separators.