镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

fix(mobile): offer a new identity beside saved keys on unclaimed accounts - #9107

Merged
johnmatthewtennant merged 8 commits into
mainfrom
jtennant/xander/mobile-create-identity
Nov 5, 2026
Merged

johnmatthewtennant merged 8 commits into
mainfrom
jtennant/xander/mobile-create-identity

Conversation

@johnmatthewtennant

@johnmatthewtennant johnmatthewtennant commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This fixes a dead end in mobile Builderlab sign-in. A new account (unbound, with no backups) on a device that already had saved identities could only pick one of those identities. Any saved key forced the picker, and creating a key required the device to have none. If the service then definitively rejected binding the chosen key, the app showed "Let's try that again" with no way forward: the chosen key stayed in the account's slot, so every retry and every launch re-sent the same rejected bind.

For an unclaimed account, the picker now also offers Create a new identity:

  • Creation writes only this account's empty identity slot, so every saved and device key is kept.
  • Creation is offered only when every current device key was among the choices shown, so a new identity never silently passes over a key the user didn't see.
  • Bound accounts, accounts with backups, pairing and pending import never offer it.

When binding a chosen key is definitively rejected, the blocked screen offers Choose a different identity:

  • It appears only for a 400, 404, 409 or 422 from /verify in the current attempt, for the current account and pubkey, and never during pairing, whose transferred key is the only choice. Evidence from an earlier attempt is cleared before each bind. A challenge rejection, a 403, a 503 or any other ambiguous failure offers nothing.
  • Before releasing the slot, it rechecks the account: still unbound (/current and status), no backups, the same subject, and a slot that still holds that pubkey without a receipt.
  • It releases the slot only when another durable copy of the same key exists (the device inventory or another account slot), then verifies the slot is empty. No key is deleted: only the account's pointer to the rejected choice is cleared. A failed or refused release keeps the slot and the choice, so it can be retried in place or after a restart.
  • After release, the picker runs again and offers the saved keys plus creation.

The recovery code lives in hosted_flow_identity_recovery.dart, a part of hosted_flow.dart, to stay within the mobile file-size limit.

Related issue

None found.

Testing

Physical iPhone, a signed release build of 1fb633d55 installed over the existing app (keychain and saved identities kept): a saved identity was rejected, then Choose a different identity, the picker, Create a new identity and Join your community. Joining then completed. This head differs from 1fb633d55 only in the guards for leaving, pairing and a failed or refused release listed below, which this journey does not reach, and in no UI code. The status of that phone's original rejection was not captured, so it is not confirmed to be one of the rejections above. The 409 case is covered by tests.

Tests (red on main, green here):

  • Identity service and store: creation beside offered keys; refusal for unseen or partly offered device keys; release when another copy exists; refusal for a sole copy, a binding, a backup, a receipt, another account or an unusable recheck; failed delete and retry; a cancelled attempt.
  • Onboarding journeys: create beside saved keys; select, rejected bind, restart, choose a different identity, create and bind; ambiguous 503 and 403 failures; challenge rejection and stale evidence.
  • Release boundaries: an account switch at /current, at status, or between the recheck and the delete; cancelling between the recheck and the delete; leaving the picker before a late creation; an unreadable device inventory; leaving while a release is in flight; a failed delete retried in place and after a restart; a release refused because the inventory is unreadable, then retried in place; a rejected paired key. Each keeps every key, leaves the new account's slot untouched and leaves a cancelled flow idle, and a retry converges.
  • Mutating the creation guard, the generation check, the binding recheck, the store's receipt check, the account and cancellation checks before the delete, or the device-inventory error handling fails a test.

Screenshots

Physical iPhone at 1fb633d55 (frames from a screen recording; saved public keys redacted):

Rejected bind After Choose a different identity After Create a new identity
Phone: rejected bind offers Choose a different identity Phone: picker with saved identities and Create a new identity Phone: Join your community after creating a new identity

Widget-test renders of the same screens (the real HostedFlow against a mocked account service, with a fixture key; the square at the top left is the test renderer's placeholder for an asset):

Picker on an unclaimed account Rejected bind
Widget render: picker with Create a new identity Widget render: rejected bind with Choose a different identity

🤖 Generated with Claude Code

johnmatthewtennant and others added 8 commits November 5, 2026 10:44
…unts

A newly created Builderlab account with no binding and no backups
showed only the saved identities on this device, with no way to start
a new one. Any eligible saved key forced selection, and generation
required the device inventory to be empty.

Selection for an unclaimed account now also offers "Create a new
identity". Creation still writes only this account's empty slot, so
every saved and device key is kept, and it is authorized only when
every current device key was among the choices shown. Bound accounts,
accounts with backups, pairing and pending import never offer it.

Co-authored-by: Xander <ff30c859b96bd5233ffaf5d071200458f1274bd0dab03589214cf1055198491d@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: John Tennant <jtennant@squareup.com>
After binding a chosen saved key was definitively rejected, that key
stayed in the account's slot, so every launch skipped selection and
retried the same rejected binding.

A 400/404/409/422 on identity verify, recorded for the current attempt,
account and pubkey, now offers "Choose a different identity". Before
removing anything it rechecks that the account is still unbound with no
backups, that the slot has no backup receipt, and that another saved
slot or device record holds the same key. It then deletes only this
account's slot, verifies the deletion and returns to identity choice,
where a new identity can be created. Ambiguous failures, stale evidence,
a sole key copy, an existing binding or backup and another account keep
the slot. An already-empty slot counts as released, so retries after a
partial failure or restart converge.

Co-authored-by: Xander <ff30c859b96bd5233ffaf5d071200458f1274bd0dab03589214cf1055198491d@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: John Tennant <jtennant@squareup.com>
hosted_flow.dart is at the mobile file-size limit, so the rejected
binding recovery moves into a part of the same library, unchanged.

Co-authored-by: Xander <ff30c859b96bd5233ffaf5d071200458f1274bd0dab03589214cf1055198491d@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: John Tennant <jtennant@squareup.com>
Releasing a rejected saved choice must keep every key when the account
changes, the flow is cancelled or the device inventory cannot be read
partway through, and a failed delete must survive a restart. These
cases already hold; the tests pin them.

Co-authored-by: Xander <ff30c859b96bd5233ffaf5d071200458f1274bd0dab03589214cf1055198491d@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: John Tennant <jtennant@squareup.com>
Co-authored-by: Xander <ff30c859b96bd5233ffaf5d071200458f1274bd0dab03589214cf1055198491d@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: John Tennant <jtennant@squareup.com>
Co-authored-by: Xander <ff30c859b96bd5233ffaf5d071200458f1274bd0dab03589214cf1055198491d@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: John Tennant <jtennant@squareup.com>
Co-authored-by: Xander <ff30c859b96bd5233ffaf5d071200458f1274bd0dab03589214cf1055198491d@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: John Tennant <jtennant@squareup.com>
A release refused without an error (for example, when the keychain
inventory cannot confirm the copy kept elsewhere) left the rejection
tied to an older attempt, hiding Choose a different identity until
setup restarted. Record the rejection for the current attempt on any
outcome other than release.

Co-authored-by: Xander <ff30c859b96bd5233ffaf5d071200458f1274bd0dab03589214cf1055198491d@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: John Tennant <jtennant@squareup.com>
@johnmatthewtennant
johnmatthewtennant marked this pull request as ready for review November 5, 2026 19:13
@johnmatthewtennant
johnmatthewtennant requested a review from a team as a code owner November 5, 2026 19:13
@johnmatthewtennant
johnmatthewtennant merged commit c848b6d into main Nov 5, 2026
60 checks passed
@johnmatthewtennant
johnmatthewtennant deleted the jtennant/xander/mobile-create-identity branch November 5, 2026 19:24
bradseiler added a commit that referenced this pull request Nov 5, 2026
…-media

* origin/main:
  fix(mobile): offer a new identity beside saved keys on unclaimed accounts (#9107)
  fix(desktop): show upgrade guidance (BUZZ-517) (#9033)
  fix(mobile): skip the read-state snapshot when nothing needs publishing (#9093)
  fix(mobile): keep verified names on screen in channels with over 1,024 members (#9111)
  mobile: reuse channel message rows that did not change (#9115)
  fix(mobile): back off push bootstrap retries (#9092)
  fix(mobile): keep reply counts on screen while a reconnect catches up (#9100)
  fix(mobile): inflate gzip read-state snapshot responses (#9089)
  perf(relay): cache the community-active check on socket admission (#9109)
  fix(db): auto-migrate prebuild-guarded indexes on small databases (#8802)
  docs: mark desktop/ as the deprecated Buzz 0.X app (#8840)

Signed-off-by: Brad Seiler <seiler@squareup.com>
wpfleger96 added a commit that referenced this pull request Nov 5, 2026
…dary

* origin/main:
  fix(mobile): focus the search field when search opens (#9120)
  perf(mobile): validate each verified profile once, not on every publish (#9101)
  fix(mobile): offer a new identity beside saved keys on unclaimed accounts (#9107)
  fix(desktop): show upgrade guidance (BUZZ-517) (#9033)
  fix(mobile): skip the read-state snapshot when nothing needs publishing (#9093)
  fix(mobile): keep verified names on screen in channels with over 1,024 members (#9111)
  mobile: reuse channel message rows that did not change (#9115)
  fix(mobile): back off push bootstrap retries (#9092)
  fix(mobile): keep reply counts on screen while a reconnect catches up (#9100)
  fix(mobile): inflate gzip read-state snapshot responses (#9089)
  perf(relay): cache the community-active check on socket admission (#9109)
  fix(db): auto-migrate prebuild-guarded indexes on small databases (#8802)

Signed-off-by: Will Pfleger <wpfleger@block.xyz>
brow added a commit that referenced this pull request Nov 6, 2026
…(backport #9089, #9093, #9107 to 0.27) (#9175)

## Summary

Backports three fixes from `main` to `mobile-v0.27` for the Nov 10 App
Store release (0.27.1):

- [#9089 fix(mobile): inflate gzip read-state snapshot
responses](#9089). The Builderlab
relay gzips responses, so on 0.27 every read-state snapshot fetch fails
and read marks made on the phone never reach other devices.
- [#9093 fix(mobile): skip the read-state snapshot when nothing needs
publishing](#9093). Once the snapshot
works, this avoids an extra full snapshot download on each idle resume.
- [#9107 fix(mobile): offer a new identity beside saved keys on
unclaimed accounts](#9107). This fixes
a Builderlab sign-in dead end: a new account on a device that already
holds saved keys could only reuse one of those keys, and a rejected bind
left it stuck on "Let's try that again".

## Provenance

`git cherry-pick -x --signoff` of the squash merges `5e36d65c9e`,
`dce37f74d7` and `c848b6dcad`, in that order, onto `mobile-v0.27` at
`54d57a5430`. All three applied cleanly, and their patch IDs match the
originals. Nothing else from `main` is included.

## Validation

Independent stable-fitness reviews by two agents. Both said to pick all
three, with no blockers and no missing `main`-only prerequisites.
Results on this branch's tree
(`2e9b94cd9bf9363eb8fadd772764b0ecc4b8c091`):

- Mobile analysis and format: clean.
- Full mobile suite with CI's auth-domain and push-gateway defines:
4,427 passed, 7 skipped. The new hosted identity journeys ran, covering
rejection and restart, failed release, account switching and key
preservation.
- iOS simulator debug build with auth domains enabled: passed (Xcode
27.1).

Limitation: no live Builderlab sign-in or device read-mark sync was run
on this stable build.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Tom Brow <tomb@squareup.com>
Signed-off-by: John Tennant <jtennant@squareup.com>
Co-authored-by: Tom Brow <tomb@squareup.com>
Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz>
Co-authored-by: John Matthew Tennant <jtennant@squareup.com>
Co-authored-by: Xander <ff30c859b96bd5233ffaf5d071200458f1274bd0dab03589214cf1055198491d@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

This branch was previously deployed

1 inactive deployment
codex-review — 3eb23f4c Deployed Nov 5, 2026 by johnmatthewtennant via Run Codex Security Review #12941
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