Skip to content

feat(auth): give Android a Keychain equivalent for the account list - #1502

Merged
bmc08gt merged 1 commit into
code/cashfrom
feat/blockstore-account-list
Sep 19, 2026
Merged

bmc08gt merged 1 commit into
code/cashfrom
feat/blockstore-account-list

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Sep 18, 2026 •

Copy link
Copy Markdown
Collaborator

iOS keeps its account list in the Keychain, which is why deleting the app and reinstalling it still leaves the user a list to pick from. Android had no counterpart. Google Password Manager, which this replaces, could restore a single account but never enumerate them, and it sat behind a beta flag that was never used.

Block Store is the counterpart

Play services keeps the blob in its own directory rather than the app sandbox, so it outlives the app the way a Keychain item does. Three ways the equivalence is not exact, each of which the design leans on:

  • It depends on Play services, and on Backup services being on. Writes are best effort as a result: losing the store costs an account its place in the list, not its funds — the access key is still the backstop. A mutation never rewrites a list it could not first read, so a failed read can drop an addition but cannot lose an existing entry.
  • pm clear wipes it. Play services logs Removed Blockstore data for <pkg> upon clearing data. This outlives a reinstall, not a data clear.
  • Cloud backup is conditional. It is requested only when Play services reports end-to-end encryption is available, which needs a screen lock, and the flag is recomputed on every write — leaving it unset deletes previously backed-up cloud data on the next sync.

What gets stored

The whole list lives in one of Block Store's 16 entries, packed by AccountRecordCodec as [version:1][count:2][record × count], each record 16 bytes of entropy plus three timestamps. That is 2003 bytes at the 50-account cap, roughly half the 4KB entry limit, which leaves room for a format change. Only the seed is stored; the owner key and the display name derive from it. AccountRecord mirrors iOS's AccountDescription, soft delete included.

Select an Account

Reachable the two ways iOS offers it: Log In routes there when the store holds accounts, and Advanced → Switch Accounts opens it from inside the app. Switch Accounts stays a StaffMenuItem, as it was behind the old flag, so it keeps both the beta/staff gate and the badge.

Balances are fetched per account with its own owner key as both the account owner and the requesting owner — a self-signed request, which is what makes them resolvable while logged out. A row the backend does not know says "Not Found"; a row whose balance could not be fetched says so separately. Collapsing the two would tell an offline user that every wallet they own is gone.

The in-app switcher hides the "Enter a Different Access Key" footer. From inside the app there is no onboarding flow to fall through to, and a logged-in user reaching for a new access key is logging out, which the menu already offers.

Credential Manager

Removed, along with FeatureFlag.CredentialManager. The existing credentials DataStore seeds Block Store on first launch, written as a condition rather than a one-shot upgrade hook so it also recovers an entry that decodes as empty. That DataStore is now excluded from Google backup.

Maestro flows

Three flows cover the list, the in-app switcher, and the case this whole change exists for. blockstore_survives_reinstall.yaml needs a real adb uninstall + install between flows, since Maestro has no uninstall command; its header gives the sequence. Switch Accounts is gated, so subflows/unlock_beta_features.yaml unlocks it the way a human does — seven taps on the version footer. The betaFlags launch argument only flips individual FeatureFlag keys and does not set that override.

Design and plan: docs/superpowers/specs/2026-09-18-blockstore-account-list-design.md and docs/superpowers/plans/2026-09-18-blockstore-account-list.md.

iOS lists every account the user has signed into and lets them switch between
them, with the Keychain holding that list across an uninstall. Android had no
counterpart: Google Password Manager could restore one account but never
enumerate them, and it sat behind a beta flag that was never used.

Google Block Store keeps its blob in Play services' own directory rather than
the app sandbox, so it survives uninstall when Backup services is on. The whole
list lives in one of its 16 entries, packed by AccountRecordCodec as
[version:1][count:2][record x count], each record 16 bytes of entropy plus three
timestamps. That is 2003 bytes at the 50-account cap, about half the 4KB entry
limit. Only the seed is stored; the owner key and the display name derive from
it.

Writes are best effort. Play services being unavailable costs an account its
place in the list, not its funds, and the access key is still the backstop. A
mutation never rewrites a list it could not first read, so a failed read can
drop an addition but cannot lose an entry.

Select an Account is reachable the two ways iOS offers it: Log In routes there
when the store holds accounts, and Advanced > Switch Accounts opens it from
inside the app. Switch Accounts stays a StaffMenuItem, as it was behind the old
flag, so it keeps the beta gate and the badge.

Balances are fetched per account with its own owner key as both the account
owner and the requesting owner, a self-signed request, which is what makes them
resolvable while logged out. A row the backend does not know says "Not Found";
a row whose balance could not be fetched says so separately, because collapsing
the two would tell an offline user that every wallet they own is gone.

Google Credential Manager and its feature flag are removed. The existing
credentials DataStore seeds Block Store on first launch, as a condition rather
than a one-shot hook so it also recovers an entry that decodes as empty.
@bmc08gt bmc08gt self-assigned this Sep 18, 2026
@github-actions github-actions Bot added type: feature New functionality area: auth Login, session, access keys, identity area: ui Compose UI, theme, components, resources area: build-system Gradle, convention plugins, build-logic area: onboarding and removed type: feature New functionality labels Sep 18, 2026
@bmc08gt bmc08gt changed the title feat(auth): keep an account list that survives uninstall feat(auth): give Android a Keychain equivalent for the account list Sep 18, 2026
@bmc08gt
bmc08gt merged commit 257aa69 into code/cash Sep 19, 2026
4 checks passed
@bmc08gt
bmc08gt deleted the feat/blockstore-account-list branch September 23, 2026 16:45
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: build-system Gradle, convention plugins, build-logic area: onboarding area: ui Compose UI, theme, components, resources

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant