Skip to content

feat(login): name account switcher rows by username, and load balances in two calls - #1626

Merged
bmc08gt merged 5 commits into
code/cashfrom
feat/account-switcher-username
Sep 30, 2026
Merged

bmc08gt merged 5 commits into
code/cashfrom
feat/account-switcher-username

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

Each row in the account switcher is now titled with the account's @username, else its profile display name, else the mnemonic name it showed before. iOS is making the same change in the same order.

Names for accounts that aren't signed in

Only the signed-in account has a session, so each row fetches its own profile signed as that account, the same self-signed pattern the list already uses for balances. GetProfile is keyed by user id, so the fetch calls Login first to map the owner key to one. The contract describes Login as a lookup for recovering an account.

Answers are cached per owner key in AccountProfileCache, a local DataStore that UserManager's state also writes on sign-in, refresh, and profile edits. The cached name shows immediately and offline, and the fetch replaces it. A NotFound profile clears the entry; other failures keep it. Names stay out of the Block Store record, which has a 4KB budget.

Balances

Balances were slow for accounts holding many tokens. fetchTokenAccounts resolves metadata one mint at a time, which the main screen hides behind its cache. The switcher has no cache, so an account with 43 tokens made 43 sequential calls of 100-200ms each.

TokenController.fetchTokenBalances makes one account lookup and one batched metadata lookup per account, as iOS does. A mint missing from the batch fails the fetch, so a short total is never shown. Key derivation for the stored accounts also runs in parallel now, instead of blocking the list one account at a time.

While a balance is in flight, its slot shows the shimmer skeleton from core-ui. It has no semantics; TalkBack announces the balance when it lands.

Each row now carries the ic_checked/ic_unchecked circle the region, token and group-invite pickers use, in place of a tick on the current account only. It matches iOS's CheckView.

Open question

Login is assumed to have no server-side side effects, such as updating a last-login time. That rests on the contract comment, not on a check with the backend.

Each row in the account switcher now shows the account's @username, else
its profile display name, else the mnemonic name it showed before. iOS
uses the same order.

Only the signed-in account has a session, so the other rows read from a
new AccountProfileCache: a local DataStore keyed by owner public key,
written from UserManager's state whenever a profile is restored, fetched,
or edited. Reading it needs no network and no signing as the other
account. The server's getProfile takes a user id and a signed request,
so there was no unsigned lookup by owner key to use instead.

Names stay out of the Block Store entry, which has a 4KB budget sized
for fixed-width records. An account fills its cache entry the next time
it signs in on this device; until then its row keeps the mnemonic name.
The switcher named only accounts that had signed in on this device, since
the cache was filled from the signed-in session. Each row now fetches its
own profile, signed as that account, the way the list already fetches
balances: Login maps the owner key to a user id, then GetProfile by that
id. The cached names still show first, and offline.

A NotFound profile counts as "no names", so a name the account has since
dropped is cleared from the cache. Other failures leave the row's cached
title in place.
An account holding 43 tokens made 43 sequential metadata calls of
100-200ms each before its balance showed. fetchTokenAccounts resolves
metadata one mint at a time, which is cheap behind the main screen's
cache and a round trip per token here, where there is no cache.

TokenController.fetchTokenBalances makes one account lookup and one
batched metadata lookup, the shape iOS's account list uses. A mint the
batch does not return fails the fetch instead of shortening the total.

The list also showed nothing until every stored account's key was
derived, one PBKDF2 run and SLIP-10 chain after another. Those now run
side by side on the Default dispatcher.
@bmc08gt bmc08gt self-assigned this Sep 30, 2026
@github-actions github-actions Bot added type: feature New functionality area: auth Login, session, access keys, identity area: network gRPC, connectivity, API, exchange rates area: onboarding labels Sep 30, 2026
The rows used a bare tick on the current account only. They now use the
ic_checked/ic_unchecked pair the region, token and group-invite pickers
use, which matches iOS's CheckView.
@bmc08gt
bmc08gt force-pushed the feat/account-switcher-username branch from 292120d to af4fffa Compare September 30, 2026 15:04
@bmc08gt
bmc08gt merged commit 8802fa9 into code/cash Sep 30, 2026
3 checks passed
bmc08gt added a commit that referenced this pull request Sep 30, 2026
…count (#1630)

* fix(login): stop logging in as other accounts to name switcher rows

The account switcher named each row by calling the Login RPC signed as
that row's account, then GetProfile with the user id it returned
(AccountProfileFetcher, from #1626). The app must not sign in as an
account the user has not switched to, and iOS dropped the same call.

Android stores no user id for accounts other than the signed-in one, so
there is no GetProfile call left that avoids Login. Rows now take their
username and display name only from AccountProfileCache, which is
written while each account is signed in. An account that has never
signed in on this device keeps its mnemonic name. The fallback order is
unchanged: @username, then display name, then mnemonic name.

The balance fetch per row is unchanged. It signs a read as the row's
owner key but does not call Login or start a session.

* feat(login): fetch switcher profiles by a user id cached at sign-in

Rows for other accounts could only show the name cached while that
account was signed in, because the user id GetProfile needs was never
stored. AccountProfileCache now keeps each account's user id alongside
its names, taken from UserManager's state while the account is signed
in. NoId, the value clear() leaves at sign-out, is skipped.

A row with a cached user id fetches its profile with GetProfile by that
id, signed with the row's owner key the way the balance fetch signs. A
row without one makes no profile call. AccountProfileFetcher now takes
the user id and depends only on ProfileRepository and the cache, so it
has no path to the Login RPC.

Accounts that last signed in before this change have no cached user id
until their next sign-in on this device, and keep their cached or
mnemonic name until then.

* fix(user-profile): stub setDisplayName with its String? result

#1625 changed ProfileController.setDisplayName to return the server-assigned
username as Result<String?>. NameEntryViewModelTest still stubbed Result<Unit>,
so the module's unit tests no longer compiled.

* fix(login): resolve a missing user id with Login as the last fallback

A switcher row with a cached user id still calls GetProfile by that id. A row
without one now calls Login signed with its own owner key, caches the user id it
returns, then calls GetProfile. The cached id means each account goes through
Login at most once on this device.

This matches code-payments/code-ios-app#918, which calls login(owner:) only when
a row has no stored user id and its database has none either. Android has no
database step, so Login follows the cache directly.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: auth Login, session, access keys, identity area: network gRPC, connectivity, API, exchange rates area: onboarding type: feature New functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant