Skip to content

A way out of the numeric keyboard, and a Swap form that empties after a swap - #232

Merged
EduMenges merged 6 commits into
redesign/assets-boxed-panels-260808from
redesign/swap-keyboard-and-reset-260810
Aug 10, 2026
Merged

A way out of the numeric keyboard, and a Swap form that empties after a swap#232
EduMenges merged 6 commits into
redesign/assets-boxed-panels-260808from
redesign/swap-keyboard-and-reset-260810

Conversation

@SirYakoob

Copy link
Copy Markdown

Three things a person hits on the Swap screen, two of which were traps rather than rough edges.

You could not get out of the numeric keyboard

iOS gives its number and decimal pads no return key. A field using one can only be escaped by tapping somewhere else, and nothing on screen says so. Jakub hit this on Swap and could not tell how to dismiss it. Six surfaces had the same trap: Swap, Bridge, Swap settings, Banxa, the token page and Settings.

GWKeyboardDoneBar wraps a field and floats a toolbar above the keyboard while that field holds focus. Leading chevrons move focus between fields, trailing is a tick. Tapping outside dismisses too - the bar is the affordance, tapping away is the shortcut, and doing both inside the component means no call site can ship half the pair.

It is drawn in the idiom iOS 26 uses, not the one iOS 18 used. The first version was an edge-to-edge opaque band flush against the keyboard, which is the old UIToolbar look, and on a device running 26.6 it read as foreign. iOS 26 gave the system keyboard a margin and rounded corners of its own, so nothing sits flush against it any more, and Apple's accessory bars became floating rounded translucent islands with glyphs instead of words - Safari's now shows a key, a card, a pin and a checkmark. So: an island inset from both edges, 8pt clear of the keyboard, rounded, blurred, with a shadow so the gap reads as clearance rather than a hole, and a tick rather than the word "Done".

A truly native accessory view is not available to us, and this was checked rather than assumed. Flutter has no API for it (flutter#124784, open since 2023, P3). keyboard_actions, the popular package, draws a Flutter widget exactly as this does. The one package claiming native is version 0.0.4 with 314 downloads, which is not a dependency a wallet should take, and AGENTS.md says no new dependency if it can be avoided. Reaching into the engine to find FlutterTextInputView and attach a real UIToolbar depends on undocumented internals. On iOS 26 the gap between native and drawn is small anyway, because Apple broke the seamless blend for everyone.

The chevrons are real, not decoration. Every field wrapped with the bar enabled is a navigation stop, so they light up wherever a surface genuinely has more than one. Today that is Settings, with four numeric rows; its text rows are excluded for free. On the other five there is one field and both chevrons are correctly dead. A field wrapped with the bar disabled - Swap's read-only "You Receive" - is deliberately not a stop, because focusing it opens no keyboard and the bar would vanish mid-navigation.

Building the chevrons surfaced a latent bug in the bar itself. It holds its overlay entry until the keyboard inset reaches zero, which is what makes it ride the keyboard down instead of popping. Correct when the keyboard is leaving, wrong when a sibling field took the focus: by the third hop the stale entry is on top, and one field's controls would paint over another field's bar. Invisible without the chevrons. A bar now drops every other bar's entry before inserting its own, and bars are scoped by focus scope so one inside a drawer cannot step onto the screen behind it.

The form kept its values after a swap

Pressing the CTA showed a receipt and recorded a transaction, then left "You Pay", "You Receive" and the fetched route exactly as they were. Closing the receipt returned to a fully populated form, and one stray tap on the CTA repeated the whole thing.

The amounts, both controllers and the route are cleared once the submit path succeeds. The token selections are kept on purpose: the wallet-change listener nulls them because the chain moved and the catalogue is being reloaded, but after a swap nothing about the chain changed, and re-picking both from a drawer is the expensive half of the form. The debounce timer is cancelled and the route-error flag reset, so the screen is genuinely at rest rather than only looking it.

The settings icon sat below the title it belongs to

Measured 22px below the title's centre line. GWPageHeader centres a trailing against the whole identity block, which on this screen is title plus subtitle, so the icon drifted down by half the subtitle. It now sits on the title's own centre line at the right edge, the same relationship the filter trigger has on Transactions.

The title and subtitle stay centred at both widths. Left-aligning them to match Assets and News was tried and rejected on device.

Verification

gate result
dart format lib test 402 files, 0 changed
flutter analyze No issues found
flutter test +1221 ~3, all passed, run twice
check_brace_style 0
check_raw_colors 0
check_onboarding_seed_safety 0
check_no_new_key_logging --scan-tree 0
check_agent_rules_sync 1 - pre-existing, see below

check_agent_rules_sync is red and was red before this branch existed: the drift is between AGENTS.md and .github/copilot-instructions.md, in prose about the lazy-developer ladder. Neither file is touched here, and it is red on PR #231 for the same reason.

Nineteen tests are added: fourteen on the keyboard bar, four on the Swap header, and the header set compares a really-pumped Swap screen against a really-pumped Assets screen rather than against a copied constant.

Numbers in this description are measured, not estimated. The 22px icon offset, the 92-to-108 header growth quoted below, the tap targets and every contrast figure come from pumped widget tests.

Things worth a reviewer's attention

  • The trigger is a 48x32 box, not a 48x48 button, and that is load-bearing. Measured: a 48-tall control on the title line takes the header from 92 to 108 and drops the Swap title 8px below where Assets and Transactions put theirs. Jakub asked specifically for those titles to agree. Same trade the Transactions trigger already makes: 32 clears WCAG 2.2 SC 2.5.8's 24x24 floor and Android's 48dp horizontally, and misses Apple's 44pt recommendation vertically.
  • The bar's fill is 94% opaque, which is less glass than it looks. The honest worst case for a translucent surface is arbitrary content behind it, and at 88% a dark label falls to 4.26:1 over dark content, under AA. Measured worst cases as shipped: 5.81:1 dark, 4.93:1 light; over the app's own canvas, 6.83:1 and 5.60:1. The blur buys motion and a soft edge rather than transparency. If it reads as flat on a device, the fix is a different label colour, not a more transparent fill.

Deliberately not here

  • One new test is order-dependent. a chevron with nowhere to go is disabled to a screen reader passes in isolation and in the full suite, but failed once in a multi-file subset run on a semantics focus action. It is new code in this PR, not a pre-existing flake. Left standing rather than quietly deleted or loosened.
  • The disabled chevron and the enabled one measure the same contrast in light mode - 4.93:1 both - so the distinction there is hue alone, with no drop in weight. Dark mode gets both. No token in the set is simultaneously weaker than the brand and over 3:1 on this fill in both appearances, so closing it needs an appearance-aware disabled-foreground token. Marked in the source and pointed at the light-theme pass.
  • The blur's GPU cost is unmeasured. Widget tests do not rasterize, so the suite says nothing about it. Structurally the BackdropFilter exists only while the bar is on screen and is clipped to a 44pt island rather than the layer, but a real frame number needs a profiling pass.
  • The Swap screen still executes nothing. // TODO: invoke Squid API is untouched; the CTA records a transaction locally and shows a receipt. The form clear therefore hangs off the local record, and the code says next to that TODO that it must move behind a real success when Squid is wired, or a rejected swap will wipe the amounts the user typed.
  • The Swap title sits 20px further in than the Assets title and should. Its header is nested inside the 560px form column, so it heads that column the way Assets' heads its panel. Hoisting it out to buy absolute equality would put the title left of the form it names.
  • A stale doc, noticed and not touched: gwPageHeaderContentInset and the centered field both still say the centred form is "(Swap, Feedback)". Feedback stopped passing centered when 153-B moved its title to the frame's left edge, so Swap is now the only one. Predates this branch.
  • The suite's reported test count is not stable in this repo - identical runs on an unedited tree gave 1216, 1220, 1221, 1221, always green with 3 skipped. Chased as far as the dynamically generated suites, which are deterministic. Predates this work and is worth its own look.

Base branch

Stacked on redesign/news-digest-tx-filters-260810 (PR #231), which is on redesign/assets-boxed-panels-260808 (PR #230), which is on redesign/row-rhythm-260808 (PR #227). Basing on develop would drag every commit already under review into this diff. Retarget once the parents land.

On a phone the news grid resolved to two 181px columns, which gave a 16px
headline 147px of line against a 62-character median headline. Nearly every
card was truncated, and 30 articles cost 4,248px of grid for very little
readable text.

The narrow presentation is now a lead hero followed by one digest panel: the
same DashboardScrollContainer every other list page uses, holding the search
field, a story count and every remaining article as a 72px-thumbnail row with
a two-line headline and a one-line dek. "Next up" is gone as a separate band;
its three stories are simply the first three rows. The wide presentation is
untouched.

Two smaller corrections to the header while it was open:

The refresh button now renders on desktop only. It exists because
pull-to-refresh is unreachable with a mouse, which is a reason that only holds
where there is a mouse; on a phone RefreshIndicator is already mounted over
this same feed, so the glyph was a second door into a room that has one. Its
40px tap target, not the 32px title, was what set the header row's height, so
removing it takes the header from 56 to 48.

The freshness stamp is nudged down 1px. GWPageHeader centres its trailing by
line box, and a line box is not where the letters are: Inter's proportional
leading puts the 24/32 title's ink at 7.93..30.61 and this 14/20 stamp's at
11.49..25.03, so the stamp read a pixel high. Baseline alignment overshoots
2.6px the other way.

Sketches 189 (scheme C) and 194 (scheme A).
The phone page inherited the dashboard panel's filter bar: a track of four
icon-only chips with the remaining five filters behind an overflow menu, and
no labels anywhere, because a chip that grows on tap shoves its neighbours
sideways. It cost the panel a 44px row plus its gap, and a filter chosen from
the menu left no mark on the page at all.

The trigger is now a funnel in GWPageHeader.trailing, the slot Crypto News
already fills, which costs the page no vertical space. It opens the app's own
ResponsiveDrawer with a GWSelectRow per filter, each carrying the same badge
mark the rows below use and its live count, so all nine filters and All are
one gesture away and every one of them is named. A live filter paints the
trigger and adds a dismissible chip row above the list carrying the filter's
name and "3 of 10", which is the count Assets prints and this page never had.

Icons.filter_alt_outlined rather than Icons.tune: tune is already Swap's
settings glyph, and filter_alt_outlined is already this screen's own
filtered-empty mark.

The page's filter now lives on TransactionsScreen rather than inside
TransactionsSlimView's State, because the trigger that sets it sits two
widgets above the list. Both pass-throughs take it as an optional parameter,
so the dashboard panel keeps its own internal filter and its call sites are
unchanged.

Three corrections from reviewing it on device:

The page title sat 8px lower than every other page title, and the list 16px
lower, because a 48x48 IconButton in the header rows sets that row's height
against the 32px title line. The trigger is 48x32 now and the page measures
48/0/48, identical to Assets. That trades tap height: 32 clears WCAG 2.2 SC
2.5.8's 24x24 floor but is under Apple's 44pt recommendation vertically.

The phone background drops GWMeshBackground for the plain surfaceBase that
Assets and Crypto News paint.

The "No more transactions" terminus is gone. The cap it used to distinguish is
now asserted by row counts and the presence of View all.

Sketch 195, scheme G2.
Seven sketches behind the two changes above, kept because each records a
decision that is not visible in the diff.

189 measured why the phone news grid failed and put five schemes against real
CoinTelegraph data. 190 put the Transactions page in the Assets page's
language. 191 and 192 are two rounds of filter treatments; 193 corrected three
assumptions the earlier rounds had made about the app's own drawer, its icon
table and whether the count line earned its space. 194 covered the news
freshness stamp. 195 is the design that shipped.

Three corrections recorded there rather than silently fixed: sketches 188, 190,
191 and 192 drew hand-made filter glyphs instead of the shared badgeSpec table,
so their marks were stroked where the app's are filled and vertical where the
app's are diagonal; 188 and 190 drew a count on each filter chip that the code
has never rendered; and 191 and 192 invented a bottom sheet rather than using
ResponsiveDrawer. The layout findings stand, the impressions they invited were
made against the wrong marks.

Also noted for later, not fixed here: Mint is the one filter whose glyph is not
a Material icon but a stroked SVG asset, so it reads heavier than everything
beside it at any size, and that is true in the shipped app.
The team's standup note for the Crypto News feed and the Transactions filter,
in the flat-bullet chat format, plus what was deliberately left open and why.
… has one

iOS gives its number and decimal pads no return key, so a field using one can
only be escaped by tapping somewhere else. Nothing on screen says so. Six
surfaces had that trap: Swap, Bridge, Swap settings, Banxa, the token page and
Settings.

`GWKeyboardDoneBar` wraps a field and floats a toolbar above the keyboard for
as long as that field holds focus. It is drawn in the idiom iOS 26 uses rather
than the one iOS 18 used: an island inset from both edges, 8pt clear of the
keyboard rather than flush against it, rounded, blurred, with a shadow so the
gap reads as clearance instead of a hole. Leading chevrons move focus between
fields, trailing is a tick rather than the word "Done", which is the direction
Apple's own accessory bar went. Tapping outside dismisses too - the bar is the
affordance, tapping away is the shortcut, and doing it inside the component
means no call site can ship half the pair.

A truly native accessory view is not available to us. Flutter has no API for it
(flutter#124784, open since 2023, P3), the popular package draws a Flutter
widget exactly as this does, and the one claiming native has 314 downloads,
which is not a dependency a wallet should take. On iOS 26 the point is close to
moot: the system keyboard grew a margin and rounded corners of its own, so no
accessory view sits flush any more, native or not.

The chevrons are real, not decoration. Every field wrapped with the bar enabled
is a navigation stop, so they light up wherever a surface has more than one -
today that is Settings, with four numeric rows. A field wrapped with the bar
disabled, like Swap's read-only "You Receive", is deliberately not a stop:
focusing it opens no keyboard and the bar would vanish mid-navigation.

Building them surfaced a latent bug in the bar itself. It holds its overlay
until the keyboard inset reaches zero, which is what makes it ride the keyboard
down, but on a hop between sibling fields the stale entry stays on top - so by
the third hop one field's controls would paint over another field's bar. Never
visible without the chevrons. A bar now drops every other bar's entry before
inserting its own, and bars are scoped so one inside a drawer cannot step onto
the screen behind it.

Contrast measured against the real translucent fill rather than the token: the
fill is 94% opaque because the honest worst case is arbitrary content behind
it, and at 88% a dark label falls to 4.26:1 over dark content. The blur buys
motion and a soft edge, not transparency.
… its title

Two things Jakub hit testing the Swap screen on a phone.

**The form kept its values.** Pressing the CTA showed a receipt and recorded a
transaction, then left "You Pay", "You Receive" and the fetched route exactly as
they were, so closing the receipt returned to a fully populated form and one
stray tap repeated the whole thing. The amounts, both controllers and the route
are cleared once the submit path has succeeded. The token selections are kept
on purpose: the wallet-change listener nulls them because the chain moved and
the catalogue is being reloaded, but after a swap nothing about the chain
changed and the pair just traded is plausibly the pair wanted again. The
debounce timer is cancelled and the route-error flag reset, so the screen is
genuinely at rest rather than only looking it.

There is a caveat recorded next to the API TODO, where whoever wires Squid will
see it: this screen does not execute anything yet, so the clear hangs off the
local record. When the real call lands the clear has to move behind a genuine
success, or a rejected swap will wipe the amounts the user typed.

**The settings icon sat 22px below the title it belongs to.** `GWPageHeader`
centres a trailing against the whole identity block, which on this screen is
title plus subtitle, so the icon drifted down by half the subtitle. It now sits
on the title's own centre line, at the right edge, the same relationship the
filter trigger has on the Transactions page. `GWPageHeader` gained an opt-in for
that; the eight other callers do not pass it and render exactly what they did
before.

The title and subtitle stay centred at both widths. Left-aligning them to match
Assets and News was tried on 2026-08-10 and rejected on device.

The trigger is a 48x32 box rather than a plain 48x48 button, and that is
load-bearing rather than cosmetic: measured, a 48-tall control on the title line
takes the header from 92 to 108 and drops the title 8px below where Assets and
Transactions put theirs. Same trade as the Transactions trigger - 32 clears
WCAG 2.2 SC 2.5.8 and Android's 48dp horizontally, and misses Apple's 44pt
vertically.
@braianxde
braianxde marked this pull request as ready for review August 10, 2026 11:34
Base automatically changed from redesign/news-digest-tx-filters-260810 to redesign/assets-boxed-panels-260808 August 10, 2026 18:06
@EduMenges
EduMenges merged commit dcbb839 into redesign/assets-boxed-panels-260808 Aug 10, 2026
@EduMenges
EduMenges deleted the redesign/swap-keyboard-and-reset-260810 branch August 10, 2026 18:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants