fix(vpn): the reset must never be conditional on the state being sane
'Forget all connections' was shown only when registered || connected || profiles.length > 0 — evidence that something had worked. A half-finished sign-in leaves none of those true. The core reaches NeedsLogin, no profile is persisted, nothing connects — and the only control that could recover the app was hidden, because it was reading the very state that had broken. The screen said 'Waiting for sign-in' with no way to choose another server, enter another key, or start over. Deleting the app from the home screen was the only exit, on a real phone, at one in the morning. The comment above that gate already described the same mistake in a different case, which is the tell: every condition here is a bet that the app knows its own state, and this button exists for the times it does not. So there is no condition now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+19
-14
@@ -1325,12 +1325,20 @@ export const VpnScreen = ({ embedded, onShowCredits, wordmark, wordmarkLight }:
|
||||
|
||||
What belongs here is the whole-app reset: forget every connection at once.
|
||||
|
||||
Gated on anything indicating a node exists, NOT on `registered` alone. That flag is written
|
||||
by the app and stays false on the Tailscale path, so a fully connected node offered no
|
||||
reset at all — invisible precisely when it applied.
|
||||
NOT GATED AT ALL, and that is the point.
|
||||
|
||||
It was shown only when `registered || connected || profiles.length > 0` — evidence that
|
||||
something had worked. But a reset is what you reach for when nothing has worked, and the
|
||||
state it was reading is exactly the state that breaks: a half-finished sign-in leaves no
|
||||
profile, no registration and no connection, so all three were false and the only way out
|
||||
of the app was to delete it from the home screen.
|
||||
|
||||
That happened, on a real phone, at one in the morning. The previous version of this gate
|
||||
had the same shape and the comment above it said the same thing about a different case,
|
||||
which is the tell: any condition here is a bet that the app knows its own state, and this
|
||||
button exists for when it does not.
|
||||
*/}
|
||||
{vm.registered || vm.connected || vm.profiles.length > 0 ? (
|
||||
<Pressable
|
||||
<Pressable
|
||||
// Close Settings on the way out, so signing out lands on the first-run chooser rather
|
||||
// than on a settings screen describing a node that no longer exists.
|
||||
onPress={async () => {
|
||||
@@ -1349,15 +1357,12 @@ export const VpnScreen = ({ embedded, onShowCredits, wordmark, wordmarkLight }:
|
||||
<Text className="text-base font-bold" style={{ color: palette.danger }}>
|
||||
{vm.busy ? 'Forgetting…' : 'Forget all connections'}
|
||||
</Text>
|
||||
</Pressable>
|
||||
) : null}
|
||||
{vm.registered || vm.connected || vm.profiles.length > 0 ? (
|
||||
<Text style={{ color: palette.text, fontSize: 12 }}>
|
||||
Removes this device from every tailnet it has joined and returns OffScale to setup. Each
|
||||
one disappears from its admin panel; signing in again registers a new device. To leave
|
||||
just one, sign out of it from its row on the main screen.
|
||||
</Text>
|
||||
) : null}
|
||||
</Pressable>
|
||||
<Text style={{ color: palette.text, fontSize: 12 }}>
|
||||
Removes this device from every tailnet it has joined and returns OffScale to setup. Each
|
||||
one disappears from its admin panel; signing in again registers a new device. To leave
|
||||
just one, sign out of it from its row on the main screen.
|
||||
</Text>
|
||||
</>
|
||||
}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user