Add WhatsApp phone-number pairing (no QR race) + clear expiry UX#78
Merged
Conversation
Closes #77. Pairing WhatsApp by QR has a ~2-3 minute window that closes silently; three real attempts failed because the scan landed after the window expired, with the phone stuck on "waiting for the other device" and nothing on the desktop explaining why. Phone-number pairing removes the race entirely. whatsmeow's PairPhone returns an 8-character code the user types into WhatsApp (Linked devices -> Link with phone number instead) — no camera, no expiring image, and the code can be surfaced as plain text (so an agent can drive a re-pair over the HTTP API without the user racing a QR). - whatsapplive.Bridge.PairPhone(phone): connects the unpaired client and requests a linking code; pairing completes over the same connection through the existing PairSuccess -> Connected events (same success state as QR), so persistence and status handling are unchanged. - App.PairWhatsAppPhone + POST /api/whatsapp/pair-code {phone} -> {code, status}. - Web UI: "Can't scan? Link with a phone number instead" under the QR, with a number field and the code rendered dash-split like WhatsApp shows it; hidden once a session is paired. - Expiry UX (#77 item 1): a timed-out QR now shows "The pairing window expired… click Connect for a fresh code, or link with a phone number" instead of leaking a raw "timeout" error string. Deliberately no `openmessage pair --whatsapp-code` CLI subcommand: it would open a second bridge in the CLI's separate data dir and fight the running app for the linked-device slot (the two-data-dir footgun in the runbook). The HTTP endpoint against the live app is the correct agent-driveable path. Tests: 3 new Go API tests (success, bad-request/method, no-bridge), 3 new e2e tests (code flow renders dash-split code, affordance hidden when paired, expired-window helper text with no raw error). go build/vet/test green, full e2e 91/91. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e connected
Two fixes found by driving the new endpoint against the live app:
1. PairPhone was sending clientDisplayName "OpenMessage (macOS)". WhatsApp
validates this against a `Browser (OS)` allowlist server-side and
returns "400 bad-request" for a product name, so every code request
failed. Use "Chrome (macOS)" (matches PairClientChrome). The Go/e2e
tests stub PairPhone, so they can't catch a server-side rejection —
this only surfaces against the real WhatsApp servers.
2. handleConnected marked the bridge connected=true when *events.Connected
fired on the unpaired pairing transport, flipping the UI to "Connected"
and hiding the code the user still had to enter. Skip it while
Store.ID == nil so the pairing affordance stays until PairSuccess.
Verified live: POST /api/whatsapp/pair-code now returns a code with
status {pairing:true, connected:false, paired:false}.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WhatsApp pairing/connection failures were invisible: the client was constructed with waLog.Noop, so a failed pair or a stream teardown left no trace and the bridge just returned to idle. Wire whatsmeow's internal logger to the bridge zerolog (component=whatsmeow) so pairing handshake stages, stream-end frames, and connect errors are diagnosable. This surfaced the phone-pairing completion path end to end (companion_hello -> primary_hello -> companion_finish) when debugging a live re-pair. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Driving the pair-code endpoint against the live app revealed why every phone-number code failed on this account: the server accepts the code (companion_finish -> ok) and then sends passkey_prologue_request — a WebAuthn challenge for passkey-protected accounts. Our whatsmeow pin (2026-03-27) logged it as an unhandled notification and the pairing idled out silently; the phone showed "Couldn't link device". - Bump whatsmeow 2026-03-27 -> 2026-06-30 (b572e5b "pair: add support for passkeys", plus "client: ignore connect success before pairing is completed" and "pair-code: fix platform ID type"). - Handle events.PairPasskeyRequest/PairPasskeyError in the bridge: we cannot produce a companion-side WebAuthn assertion (the passkey lives on the user's phone/keychain), so fail fast with "this WhatsApp account is protected by a passkey ... scan the QR code instead" instead of idling. QR linking has no passkey step. - Adapt downloadMediaWithPath to the new upstream signature (mmsType + allowNoHash replace fileLength + fileEncSHA256B64); hash verification now enforced whenever a plaintext hash is stored. - Runbook: new "WhatsApp linking" section — passkey/code incompat, code and QR expiry windows, the "Couldn't link device. Try again later." throttle + zombie-device diagnosis, live log capture recipe, and the go.work local-override gotcha that ate the first bump attempt. go build/vet green; full go test green; deployed live and verified the passkey error now surfaces instead of a silent idle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #77.
Problem
WhatsApp QR pairing has a ~2–3 minute window that closes silently. In three consecutive real attempts the scan landed after the window had expired: the phone hung on "waiting for the other device" and the desktop showed nothing —
qr_eventjust flipped totimeoutand the QR card vanished.Fix
Phone-number pairing removes the race. whatsmeow's
PairPhonereturns an 8-character code the user types into WhatsApp → Linked devices → Link with phone number instead. No camera, no expiring image, and the code is plain text — so it can be surfaced anywhere, including driven by an agent over the HTTP API for the recurring re-pair (see the runbook's WhatsApp-logout note).whatsapplive.Bridge.PairPhone(phone)connects the unpaired client and requests the code; pairing completes over the same connection via the existingPairSuccess → Connectedevents — identical success/persistence path to QR.App.PairWhatsAppPhone+POST /api/whatsapp/pair-code {phone}→{code, status}.timeoutstring as an error.Scope note
No
openmessage pair --whatsapp-codeCLI subcommand: it would open a second bridge in the CLI's separate data dir and fight the running app for the linked-device slot (the two-data-dir footgun indocs/agent-runbook.md). The HTTP endpoint against the live app is the correct agent-driveable path.Testing
E2EP-AIR1dash-split; affordance hidden while paired; expired-window helper text with no error banner.go build/go vet/go testgreen; full Playwright suite 91/91.🤖 Generated with Claude Code