Repository navigation
feat(login): name account switcher rows by username, and load balances in two calls - #1626
Merged
Merged
Conversation
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.
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
force-pushed
the
feat/account-switcher-username
branch
from
September 30, 2026 15:04
292120d to
af4fffa
Compare
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
GetProfileis keyed by user id, so the fetch callsLoginfirst to map the owner key to one. The contract describesLoginas a lookup for recovering an account.Answers are cached per owner key in
AccountProfileCache, a local DataStore thatUserManager's state also writes on sign-in, refresh, and profile edits. The cached name shows immediately and offline, and the fetch replaces it. ANotFoundprofile 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.
fetchTokenAccountsresolves 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.fetchTokenBalancesmakes 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
shimmerskeleton fromcore-ui. It has no semantics; TalkBack announces the balance when it lands.Each row now carries the
ic_checked/ic_uncheckedcircle the region, token and group-invite pickers use, in place of a tick on the current account only. It matches iOS'sCheckView.Open question
Loginis 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.