Skip to content

Commit 9994073

Browse files
yyq1025claude
andcommitted
pair: guard the already-paired re-scan (no-op same Mac, confirm replace)
The /pair modal is unguarded and opens over the drawer when already paired, but its copy + action were identical to a first pair — so a re-scan silently overwrote the single daemon slot with no "replace" wording, and re-scanning the SAME Mac needlessly tore down a healthy transport to rebuild an identical record. Branch on the current pairing (V0 is single-daemon): - same daemon (pubkey match) → "Already paired" + Close; skip pair() so a healthy connection isn't rebuilt for nothing - different daemon → "Replace <current>?" with a red destructive confirm that spells out the disconnect - unpaired → unchanged first-pair flow primaryAction gains an optional tint (red for Replace); pair()'s transport swap was already clean, so this is UX-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 8172c1f commit 9994073

1 file changed

Lines changed: 37 additions & 5 deletions

File tree

packages/app/src/app/pair.tsx

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ import { useDaemonClient } from "@/lib/daemon-client-context";
2323
* 1. **Universal Link** — Camera app scans
2424
* `https://sidecode.app/pair?o=<base64url>` and cold-launches us
2525
* straight here. `Stack.Protected` shows `/onboarding` underneath
26-
* for an unpaired user, or the drawer screen for a paired user
27-
* adding another Mac (V0.5+).
26+
* for an unpaired user, or the drawer underneath for an
27+
* already-paired user (see the paired sub-cases below — V0 holds one
28+
* daemon, so a fresh offer either no-ops or REPLACES the current one).
2829
* 2. **In-app scanner** (`/onboarding` "Scan QR code") — VisionKit
2930
* modal returns the QR string; the onboarding screen extracts the
3031
* payload and `router.push("/pair?o=...")`s here.
@@ -65,7 +66,7 @@ import { useDaemonClient } from "@/lib/daemon-client-context";
6566
*/
6667
export default function PairModal() {
6768
const { o } = useLocalSearchParams<{ o?: string }>();
68-
const { pair } = useDaemonClient();
69+
const { pair, paired } = useDaemonClient();
6970

7071
const decoded = useMemo(() => decode(o), [o]);
7172

@@ -113,7 +114,14 @@ export default function PairModal() {
113114
// is shared across states, so we only vary the title / body / buttons.
114115
let title: string;
115116
let body: string;
116-
let primaryAction: { label: string; onPress: () => void; disabled: boolean };
117+
let primaryAction: {
118+
label: string;
119+
onPress: () => void;
120+
disabled: boolean;
121+
// Override the default orange CTA tint — red for the destructive
122+
// "Replace" confirm. Defaults to "#EE5722" in the render below.
123+
tint?: string;
124+
};
117125
let secondary: { label: string; onPress: () => void } | null = null;
118126

119127
if (decoded.status === "missing") {
@@ -125,6 +133,30 @@ export default function PairModal() {
125133
body =
126134
"This QR isn't a valid sidecode pair code, or it's expired. Get a fresh one from your Mac.";
127135
primaryAction = { label: "Close", onPress: handleCancel, disabled: false };
136+
} else if (
137+
paired?.daemonIdentityPublicKey === decoded.offer.daemonIdentityPublicKey
138+
) {
139+
// Re-scanning the Mac we're ALREADY paired with. Don't re-run pair() —
140+
// that would tear down a healthy transport just to rebuild an identical
141+
// record (and flash a connecting state). Dead-end with a single Close.
142+
title = "Already paired";
143+
body = `You're already paired with ${paired.serviceName}.`;
144+
primaryAction = { label: "Close", onPress: handleCancel, disabled: false };
145+
} else if (paired !== null) {
146+
// A DIFFERENT daemon than the one we hold. V0 is single-daemon, so
147+
// confirming here REPLACES the current pairing — make the disconnect
148+
// explicit (current name in the title, incoming name in the serviceName
149+
// line below) and style the confirm destructively (red).
150+
title = `Replace ${paired.serviceName}?`;
151+
body =
152+
"sidecode pairs with one Mac at a time. Pairing here disconnects the current Mac.";
153+
primaryAction = {
154+
label: busy ? "Pairing…" : "Replace",
155+
onPress: handleConfirm,
156+
disabled: busy,
157+
tint: "#FF3B30",
158+
};
159+
secondary = { label: "Cancel", onPress: handleCancel };
128160
} else {
129161
title = "Pair with this Mac?";
130162
// No countdown: the daemon's pair-window-open gate (not the QR) is
@@ -189,7 +221,7 @@ export default function PairModal() {
189221
modifiers={[
190222
buttonStyle("glassProminent"),
191223
controlSize("extraLarge"),
192-
tint("#EE5722"),
224+
tint(primaryAction.tint ?? "#EE5722"),
193225
disabled(primaryAction.disabled === true),
194226
]}
195227
>

0 commit comments

Comments
 (0)