Page MenuHomePhabricator

[Cashtab] Add mobile custom amount keypad for locale input
ClosedPublic

Authored by bytesofman on Fri, Jul 17, 19:16.

Details

Summary

On mobile and Capacitor native, replace the OS decimal keyboard
for locale-aware amount fields with a Cashtab-styled numeric keypad
(digits, locale decimal separator, backspace; no math operators). This
keeps locale formatters applied consistently.

For now we implement only on the send screen. Each callsite has some UX considerations. The point of using a custom keypad is so the user can do all their actions in the same view without having to dismiss they keypad e.g. to push "Send." Additional benefit of standardizing expected decimal and thousands separators to the users actual locale.

We disable the "Advanced" features on the Send screen in mobile (we hide the input). Mobile users have these features if they copy paste or scan a bip21 QR code. but imo mobile users are not typing in csvs or OP_RETURN_RAW so it's better UX to hide them.

Will extend across Send, Receive, Token, Airdrop, Agora Slider, and related inputs in later diffs. Each callsite has unique UX considerations.

Test Plan
  • cd cashtab && npm test -- --testPathPatterns='CashtabAmountKeypad|cashtabAmountKeypadInput' --watchAll=false
  • On Android/iOS (or mobile UA): focus a Send/Receive amount field; custom keypad appears and OS keyboard does not; de-DE shows "," on the decimal key
  • Desktop: amount fields still use the normal keyboard (no keypad)

image.png (1×570 px, 194 KB)

Diff Detail

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

Event Timeline

Fix keypad clipped to 1-6: portal a fixed bottom dock above the footer so 7-9, decimal, and backspace are always visible.

Drop accidental capacitor.settings.gradle path rewrite from worktree node_modules symlink; keep portal keypad fix only.

Failed tests logs:

====== CashTab Unit Tests: <Wallets /> Deleting the active wallet while other wallets exist activates the next wallet in the list and does not show onboarding ======
Error: Unable to find role="button" and name `/Delete Transaction Fixtures/i`

Ignored nodes: comments, script, style
<body>
  <div>
    <div
      class="sc-dTsoBL eCPsjP"
    >
      <section
        aria-atomic="false"
        aria-label="Notifications"
        aria-live="polite"
        aria-relevant="additions text"
        class="Toastify"
      />
      <div
        class="sc-btewqU fVHtV"
      >
        <div
          class="sc-imapFV fsJynT"
        >
          <div
            class="sc-SFOxd eOPmaJ"
          >
            <div
              class="sc-hPeUyl gLxdtR"
            />
          </div>
        </div>
      </div>
    </div>
  </div>
</body>
    at waitForWrapper (/work/node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/dist/wait-for.js:163:27)
    at /work/node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/dist/query-helpers.js:86:33
    at Object.findByRole (/work/cashtab/src/components/Wallets/__tests__/index.test.js:724:26)

Each failure log is accessible here:
CashTab Unit Tests: <Wallets /> Deleting the active wallet while other wallets exist activates the next wallet in the list and does not show onboarding

Failed tests logs:

====== CashTab Unit Tests: <Wallets /> Deleting the active wallet while other wallets exist activates the next wallet in the list and does not show onboarding ======
Error: Unable to find role="button" and name `/Delete Transaction Fixtures/i`

Ignored nodes: comments, script, style
<body>
  <div>
    <div
      class="sc-dTsoBL eCPsjP"
    >
      <section
        aria-atomic="false"
        aria-label="Notifications"
        aria-live="polite"
        aria-relevant="additions text"
        class="Toastify"
      />
      <div
        class="sc-btewqU fVHtV"
      >
        <div
          class="sc-imapFV fsJynT"
        >
          <div
            class="sc-SFOxd eOPmaJ"
          >
            <div
              class="sc-hPeUyl gLxdtR"
            />
          </div>
        </div>
      </div>
    </div>
  </div>
</body>
    at waitForWrapper (/work/node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/dist/wait-for.js:163:27)
    at /work/node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/dist/query-helpers.js:86:33
    at Object.findByRole (/work/cashtab/src/components/Wallets/__tests__/index.test.js:724:26)

Each failure log is accessible here:
CashTab Unit Tests: <Wallets /> Deleting the active wallet while other wallets exist activates the next wallet in the list and does not show onboarding

Native Send: hide Advanced + Sending preview; raise fixed Send above keypad dock via CSS offset so amount entry never covers Send.

Dock keypad above Send (CTA offset); restore Sending preview on native; remove Send to/Amount labels.

Failed tests logs:

====== CashTab Unit Tests: <Wallets /> Deleting the active wallet while other wallets exist activates the next wallet in the list and does not show onboarding ======
Error: Unable to find role="button" and name `/Delete Transaction Fixtures/i`

Ignored nodes: comments, script, style
<body>
  <div>
    <div
      class="sc-dTsoBL eCPsjP"
    >
      <section
        aria-atomic="false"
        aria-label="Notifications"
        aria-live="polite"
        aria-relevant="additions text"
        class="Toastify"
      />
      <div
        class="sc-btewqU fVHtV"
      >
        <div
          class="sc-imapFV fsJynT"
        >
          <div
            class="sc-SFOxd eOPmaJ"
          >
            <div
              class="sc-hPeUyl gLxdtR"
            />
          </div>
        </div>
      </div>
    </div>
  </div>
</body>
    at waitForWrapper (/work/node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/dist/wait-for.js:163:27)
    at /work/node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/dist/query-helpers.js:86:33
    at Object.findByRole (/work/cashtab/src/components/Wallets/__tests__/index.test.js:724:26)

Each failure log is accessible here:
CashTab Unit Tests: <Wallets /> Deleting the active wallet while other wallets exist activates the next wallet in the list and does not show onboarding

Scope custom keypad to Send only (enableAmountKeypad opt-in); Agora/mint/burn keep OS keyboard for now.

This revision is now accepted and ready to land.Mon, Jul 20, 12:18
Fabien requested changes to this revision.Mon, Jul 20, 12:34
Fabien added a subscriber: Fabien.
Fabien added inline comments.
cashtab/src/components/Common/__tests__/CashtabAmountKeypad.test.tsx
71 ↗(On Diff #60382)

this test makes no sense

121 ↗(On Diff #60382)

check the value in a loop here

126 ↗(On Diff #60382)

???

134 ↗(On Diff #60382)

this test is not necessary imo

170 ↗(On Diff #60382)

this doesn't test anything

cashtab/src/components/Common/cashtabAmountKeypadInput.ts
93 ↗(On Diff #60382)

you can avoid this function, it brings no value

cashtab/src/components/Send/SendXec.tsx
3276 ↗(On Diff #60382)

Why did you remove the labels ?

cashtab/src/components/Send/styled.ts
104 ↗(On Diff #60382)

why ? 100 should be enough

This revision now requires changes to proceed.Mon, Jul 20, 12:34

Address Fabien feedback:

  • Drop no-value getKeypadDecimalLabel; use getDecimalSeparator directly
  • Restore Send Address / Amount / Send to labels
  • Revert Send CTA z-index to 100
  • Trim weak keypad unit tests; assert digit handler values in the loop; clarify portal assertion

Addressed inline feedback:

  • this test makes no sense — removed the "no math operators" test (component never had them).
  • check the value in a loop here — digit handler assertions now run inside the 0–9 loop via toHaveBeenLastCalledWith.
  • ??? (portal parent chain) — replaced with an explicit document.body.contains / !container.contains check.
  • this test is not necessary imo — removed the CSS offset unit test.
  • this doesn't test anything — removed the press-timeout smoke test.
  • you can avoid this function — dropped getKeypadDecimalLabel; keypad uses getDecimalSeparator directly.
  • Why did you remove the labels? — restored Address / Amount / Send to labels.
  • why? 100 should be enough — reverted Send CTA z-index to 100.

Failed tests logs:

====== CashTab Unit Tests: <SendXec /> rendered with params in URL bip21 param - parses op_return_raw for Proof of Writing (POWR) auth/login ======
Error: expect(element).toHaveTextContent()

Expected element to have text content:
  9,513.12 XEC
Received:
  9,513.12
    at toHaveTextContent (/work/cashtab/src/components/Send/__tests__/SendByUrlParams.test.js:605:15)
    at runNextTicks (node:internal/process/task_queues:65:5)
    at listOnTimeout (node:internal/timers:549:9)
    at processTimers (node:internal/timers:523:7)
====== CashTab Unit Tests: <SendXec /> rendered with params in URL bip21 param - parses op_return_raw for eCash Chat Auth ======
Error: expect(element).toHaveTextContent()

Expected element to have text content:
  9,513.12 XEC
Received:
  9,513.12
    at toHaveTextContent (/work/cashtab/src/components/Send/__tests__/SendByUrlParams.test.js:605:15)
    at runNextTicks (node:internal/process/task_queues:65:5)
    at listOnTimeout (node:internal/timers:549:9)
    at processTimers (node:internal/timers:523:7)
====== CashTab Unit Tests: <SendXec /> rendered with params in URL bip21 param - parses op_return_raw for XECX staking reward ======
Error: expect(element).toHaveTextContent()

Expected element to have text content:
  9,513.12 XEC
Received:
  9,513.12
    at toHaveTextContent (/work/cashtab/src/components/Send/__tests__/SendByUrlParams.test.js:605:15)
====== CashTab Unit Tests: <SendXec /> rendered with params in URL bip21 param - parses op_return_raw for Solana Address (SOL0) ======
Error: expect(element).toHaveTextContent()

Expected element to have text content:
  9,513.12 XEC
Received:
  9,513.12
    at toHaveTextContent (/work/cashtab/src/components/Send/__tests__/SendByUrlParams.test.js:605:15)
    at runNextTicks (node:internal/process/task_queues:65:5)
    at listOnTimeout (node:internal/timers:549:9)
    at processTimers (node:internal/timers:523:7)
====== CashTab Unit Tests: <SendXec /> rendered with params in URL bip21 param - parses op_return_raw for DICE bet ======
Error: expect(element).toHaveTextContent()

Expected element to have text content:
  9,513.12 XEC
Received:
  9,513.12
    at toHaveTextContent (/work/cashtab/src/components/Send/__tests__/SendByUrlParams.test.js:605:15)
    at runNextTicks (node:internal/process/task_queues:65:5)
    at listOnTimeout (node:internal/timers:549:9)
    at processTimers (node:internal/timers:523:7)
====== CashTab Unit Tests: <SendXec /> rendered with params in URL bip21 param - parses op_return_raw for ROLL payout ======
Error: expect(element).toHaveTextContent()

Expected element to have text content:
  9,513.12 XEC
Received:
  9,513.12
    at toHaveTextContent (/work/cashtab/src/components/Send/__tests__/SendByUrlParams.test.js:605:15)
    at runNextTicks (node:internal/process/task_queues:65:5)
    at listOnTimeout (node:internal/timers:549:9)
    at processTimers (node:internal/timers:523:7)

Each failure log is accessible here:
CashTab Unit Tests: <SendXec /> rendered with params in URL bip21 param - parses op_return_raw for Proof of Writing (POWR) auth/login
CashTab Unit Tests: <SendXec /> rendered with params in URL bip21 param - parses op_return_raw for eCash Chat Auth
CashTab Unit Tests: <SendXec /> rendered with params in URL bip21 param - parses op_return_raw for XECX staking reward
CashTab Unit Tests: <SendXec /> rendered with params in URL bip21 param - parses op_return_raw for Solana Address (SOL0)
CashTab Unit Tests: <SendXec /> rendered with params in URL bip21 param - parses op_return_raw for DICE bet
CashTab Unit Tests: <SendXec /> rendered with params in URL bip21 param - parses op_return_raw for ROLL payout

Fabien requested changes to this revision.Mon, Jul 20, 13:15
Fabien added inline comments.
cashtab/android/capacitor.settings.gradle
3 ↗(On Diff #60412)

this is useless

cashtab/src/components/Common/__tests__/CashtabAmountKeypad.test.tsx
91 ↗(On Diff #60412)

Either I still don't understand the point or you're testing JS specs which makes no sense.

Address Fabien feedback + CI:

  • Remove portal unit test (React/createPortal behavior)
  • Drop accidental capacitor.settings.gradle worktree path rewrite
  • Fix SendByUrlParams balance assert: 9,513.12 (no XEC suffix; tickerInTitle)
  • Apply prettier to manifest.json so arc lint is clean

Addressed latest feedback + CI:

  • Removed the portal unit test (was only asserting React/createPortal behavior).
  • Dropped the accidental capacitor.settings.gradle worktree path rewrite.
  • Fixed SendByUrlParams balance assert to 9,513.12 (no XEC suffix after tickerInTitle).
  • Applied prettier to manifest.json so arc lint is clean (no --nolint).

Rebase onto latest master.

Resolve cashtab version bump against master 5.19.1 → 5.20.0; SendByUrlParams balance assert already on master.

This revision is now accepted and ready to land.Tue, Jul 21, 12:48