A second account on the same phone gets its own device (android#22)

Sign out normally, then create a second account, and the app refused it
with "Your session expired. Sign out and back in to keep going." That is
advice nobody can act on: you have already signed out, and the screen
saying it is the one you sign up from.

The cause is one line doing the right thing in the wrong case.
`authenticate` reuses the device identity stored in the vault, and the
comment above it says exactly why: keeping the device is what stops
signing out and back in costing every contact a fresh trust prompt.
Plain sign-out keeps the device on purpose; only the erase door destroys
it.

That reasoning holds for the same account and breaks for a different
one. The second sign-up reused a device that already belonged to the
first account and tried to register it against the new one. The relay
refused, correctly, and the refusal reached the person as a sentence
about a session they did not have.

The stored credential already carries `userId` and `relayBaseUrl`, so
the check is cheap: same relay and same user means the device is ours to
reuse, anything else is somebody else's account on a shared phone and
gets its own device. Which is what "a device belongs to an account"
already meant everywhere else.

Reproduced and fixed on hardware, not argued: `SignUpFlowTest` against
relay1 failed this way on every run before, and both of its tests pass
after. That suite is the regression guard, and !122 is what made it able
to run at all.

JVM suites unchanged: 1294 tests, no failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-09-11 10:09:43 -04:00
co-authored by Claude Opus 5
parent 77ca69b327
commit 4412c0a0fa
@@ -515,11 +515,33 @@ class MessengerViewModel(
}
val vault = vault ?: error("the vault is not open")
// The stored identity FIRST, and this line is why signing out and back
// in used to cost every contact a prompt.
// in used to cost every contact a prompt: keeping the device means
// keeping every session built on it.
//
// Only when it is the SAME account, though. Signing out plainly keeps
// the device on purpose, so a second sign-up on the same phone used to
// reuse a device that already belonged to somebody else and try to
// register it against the new account. The relay refused, correctly,
// and the refusal surfaced to the person as "Your session expired.
// Sign out and back in to keep going" on the sign-up screen, which is
// advice nobody can act on: they had already signed out, and that IS
// the screen you sign up from. See android#22.
//
// Same relay and same user id means the device is ours to reuse.
// Anything else is a different account on a shared phone, and it gets
// its own device, which is what "a device belongs to an account" means.
val previous = runCatching { vault.loadCredential() }.getOrNull()
val sameAccount =
previous == null ||
(previous.userId == account.userId && previous.relayBaseUrl == config.baseUrl)
val storedIdentity = if (sameAccount) vault.loadIdentity() else null
if (!sameAccount) identity = null
val device =
identity
?: vault.loadIdentity()?.also { identity = it }
?: storedIdentity?.also { identity = it }
?: DeviceIdentity.create().also {
vault.saveIdentity(it)
identity = it