# @vojo/widget-whatsapp Vojo WhatsApp bridge management widget — mounts inside `/bots/whatsapp` in the Vojo client. This is **not** a WhatsApp client. It's a control panel for the mautrix-whatsapp bridge that talks to the bridge's **provisioning HTTP API** (bridgev2 `/_matrix/provision/v3/*`, exposed by Caddy at `https://vojo.chat/_provision/whatsapp`). It links the account (QR scan or an 8-character pairing code typed into the WhatsApp app), shows the linked account, lists WhatsApp contacts, resolves +phone numbers, and creates DM portals on demand. Auth: the widget requests MSC1960 OpenID credentials from the host (`get_openid` over the widget API; granted by `BotWidgetDriver.askOpenID` when config.json opts the bot into the `vojo.openid` capability) and sends them to the bridge as `Authorization: Bearer openid:`. The OpenID token only proves identity — it is not a Matrix access token. There is no bot text-command transport and no reply parsing: the legacy `!wa`-command dialect (`bridge-protocol/`) was deleted when the bridge API became reachable. The bot control DM still exists (BotShell needs a room and the «Show chat» fallback), the widget just doesn't read or write it — it requests **zero** MSC2762 capabilities. ## WhatsApp-specific contract notes Extracted from mautrix-whatsapp v0.2604.0 (`pkg/connector/login.go`, `startchat.go`) + mautrix-go v0.27.0 (`bridgev2/matrix/provisioning.go`): - Login flows: `qr` and `phone`. The phone flow's submit answers with a `display_and_wait` step of type **`code`** — an `XXXX-XXXX` pairing code the user types into the WhatsApp app (NOT an SMS OTP entered into the widget like Telegram's flow). - The login window is ~160 s: whatsmeow issues ~6 QR tokens (60 s + 5×20 s) and closes the socket when they run out; the pairing code lives in the same window. - There are **no** `.incorrect` retry steps: every rejected input is a `FI.MAU.WHATSAPP.*` RespError that deletes the login process server-side. The phone form transparently starts a fresh process on resubmit. - Login-liveness semantics differ by bridge generation. mautrix ≤ v0.28.0: the login process dies with the poll's request context (and there is **no** `/v3/login/cancel` route — cancelling works by aborting the long-poll). mautrix ≥ v0.28.1 (`provisioninglogin.go`): the process lives server-side with a 30-minute TTL, dropped polls reattach, and the cancel route exists. The widget's wait loop is written for both: it retries transport-shaped poll failures with backoff (Android freezes the backgrounded WebView while the user enters the pairing code in WhatsApp), wakes on return to foreground, and disambiguates a late 404 via whoami (the login may have COMPLETED while frozen). - Identifiers are phone numbers only (`tel:+…`); no usernames. Contact ids are bare phone digits (or `lid-`/`bot-` prefixed for hidden-number/bot peers). The About card and modal carry the **Meta-ToS risk disclosure** (`warning.*` keys, amber warn styling, triangle icon) — WhatsApp's terms of service forbid third-party clients and Meta may ban accounts for it. This copy is a deliberate product/legal requirement: keep it intact when restyling. ## Layout ``` src/ ├── bootstrap.ts Parse URL params the host appends (matches BotWidgetEmbed.ts) ├── widget-api.ts Inline matrix-widget-api postMessage transport: handshake, │ theme, MSC1960 get_openid, io.vojo.bot-widget verbs ├── provisioning.ts Typed client for the bridgev2 provisioning API + identifier │ helpers (wire contract documented in the file header) ├── errors.ts Bridge/WhatsApp errcode → localized copy mapping ├── login.tsx Login flow over the v3 step machine + forms (phone form, │ QR panel and pairing-code panel with long-poll) ├── contacts.tsx Contacts list, search-as-filter, phone probe, create DM ├── App.tsx Shell: boot/disconnected/connected phases, account view, │ About modal with the Meta-ToS warning callout ├── ui.tsx Icons, initials avatar, command cards, notices ├── main.tsx Entry: init bootstrap, render App or diagnostic └── styles.css Telegram-widget stylesheet verbatim + WhatsApp-only additions (warn card, ToS callout, pairing-code plate) ``` ## Local development **Don't touch the committed `config.json`.** Create `config.local.json` at the project root once — gitignored, never deployed. The host's Vite dev server overlays it on top of `/config.json` responses (see `serveLocalConfigOverlay` in `vite.config.js`); prod builds ignore the overlay entirely. Note the overlay merges bot entries **shallowly** — your local `experience` object replaces the base one wholesale, so it must carry `provisioningUrl` and `capabilities` too: ```bash # one-time: install widget deps cd apps/widget-whatsapp && npm install # one-time: create config.local.json (gitignored) at the project root cat > /home/ubuntu/projects/vojo/cinny/config.local.json <<'JSON' { "bots": [ { "id": "whatsapp", "experience": { "type": "matrix-widget", "url": "http://localhost:8083/", "provisioningUrl": "https://vojo.chat/_provision/whatsapp", "capabilities": ["vojo.openid"] } } ] } JSON ``` Run both servers: ```bash # terminal 1 — widget on :8083 with HMR cd apps/widget-whatsapp && npm run dev # terminal 2 — host SPA on :8080 cd /home/ubuntu/projects/vojo/cinny && npm start ``` Open `http://localhost:8080/bots/whatsapp`. Iframe loads cross-origin from the widget dev server, HMR works, no proxy. The provisioning calls go straight to the prod bridge API (CORS `*` + per-request bearer auth), acting on whatever account you're signed in with — same trust model as the dev client talking to the prod homeserver. `http://localhost:*` URLs are accepted by the host's URL validator only in dev builds (`import.meta.env.DEV` branch in `src/app/features/bots/catalog.ts`); production builds drop the branch via Vite's dead-code elimination, AND production-only enforces an origin allowlist (`PROD_WIDGET_ORIGINS`) so prod can never embed `localhost` even if config.json is poisoned. ## Build ```bash npm run build ``` Outputs to `apps/widget-whatsapp/dist/`. Deploy by rsyncing `dist/*` into `~/vojo/widgets/whatsapp/` on the production host (Caddy serves this via the `widgets.vojo.chat` block) — the VSCode task `Deploy widgets` does all three widgets. ## Server-side requirements 1. The widget static files at `widgets.vojo.chat/whatsapp/` (Caddy `handle_path /whatsapp/*` block). 2. The bridge provisioning API exposed at the URL configured in config.json `experience.provisioningUrl`. Caddy block inside the `vojo.chat` site, next to the telegram one: ``` handle /_provision/whatsapp/* { uri replace /_provision/whatsapp /_matrix/provision reverse_proxy whatsapp-bridge:29318 # host:port from appservice.address } ``` Path-scoped on purpose: the same bridge listener serves the appservice transaction endpoints (`/_matrix/app/*`), which must stay internal. 3. `provisioning.shared_secret` in the bridge config must NOT be `disable` (a ≥16-char secret enables the API; the widget never sees the secret — it authenticates with per-user OpenID tokens). ## Updating the production /config.json ```json "experience": { "type": "matrix-widget", "url": "https://widgets.vojo.chat/whatsapp/index.html", "provisioningUrl": "https://vojo.chat/_provision/whatsapp", "capabilities": ["vojo.openid"] } ``` ## Capability contract The widget requests **no** MSC2762 capabilities — the handshake replies with only `org.matrix.msc4039.download_file` (avatar thumbnails). The privileged surfaces are: - MSC1960 `get_openid` — granted by `BotWidgetDriver.askOpenID` iff config.json declares `"capabilities": ["vojo.openid"]` for this bot; - `io.vojo.bot-widget` side-channel verbs `open-external-url` and `open-matrix-to` (origin-pinned and validated host-side in `BotWidgetEmbed.onWidgetMessage`).