Skip to content
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
c8af6f5
feat(browser): run calls end to end on the REST signaling surface
purpshell Sep 5, 2026
97d36ca
feat(react): the complete calls UI over CallsController
purpshell Sep 5, 2026
3302c2b
feat(sdk): voip token resource and the client-rules shape the runtime…
purpshell Sep 5, 2026
291e58f
chore(contracts): refresh to polymorfa@d2ddaef83 and record calls cov…
purpshell Sep 5, 2026
08b3900
feat(calls): socket transport, renegotiation, resumption, and pod-los…
purpshell Sep 6, 2026
6c0f320
fix(calls): review follow-ups — socket connect lifecycle, media acqui…
purpshell Sep 6, 2026
ba846b6
fix(calls): track the whole socket attempt so close() settles during …
purpshell Sep 6, 2026
2f12809
test(calls): type the pending socket ticket in the close-during-acqui…
purpshell Sep 6, 2026
66e971b
test(calls): make the fake timers observe cancellation
purpshell Sep 6, 2026
6734154
test(calls): re-check timer cancellation inside fire()
purpshell Sep 6, 2026
82c2ada
chore(contracts): refresh to polymorfa@b413f2c6a
purpshell Sep 6, 2026
f7d8e85
fix(calls): address CodeRabbit full review — outbound placement, medi…
purpshell Sep 6, 2026
6a0985e
test(calls): cover the mute applied when media finishes opening
purpshell Sep 6, 2026
ca479d5
fix(calls): second review round — timer binding, negotiation rollback…
purpshell Sep 6, 2026
8c76a5d
fix(calls): label the dial pad erase button for what it does
purpshell Sep 6, 2026
a7033d8
fix(calls): reset the pre-answer toggles per call, forward the placem…
purpshell Sep 6, 2026
b609ef4
fix(calls): surface socket errors, guard Enter re-dial, scope the liv…
purpshell Sep 6, 2026
f0eeb45
fix(calls): scope the terminal-offer mapping, clear timers on a lifec…
purpshell Sep 6, 2026
4928ce1
fix(calls): recover to the interrupted status, fix ticket session and…
purpshell Sep 6, 2026
2939513
fix(calls): put the device preference back when the switch fails
purpshell Sep 6, 2026
d34dbfc
fix(calls): drop a late video upgrade, gate the element's mute control
purpshell Sep 6, 2026
9783442
fix(calls): localize the default device option
purpshell Sep 6, 2026
51b4a02
fix(calls): guard concurrent device switches and post-await transitions
purpshell Sep 6, 2026
405ef71
fix(calls): serialize same-kind device switches in the media session
purpshell Sep 6, 2026
604e8a2
docs(calls): qualify the 410/503 mapping to the media offer
purpshell Sep 6, 2026
f1ca4f1
fix(calls): keep the call duration running through a reconnect
purpshell Sep 6, 2026
981c9c7
feat(calls): let pmfa-call upgrade an audio call to video
purpshell Sep 6, 2026
9f8d813
fix(calls): require the ticket session, pin the clock in the duration…
purpshell Sep 6, 2026
31c949e
fix(calls): carry the line through the socket, gate the upgrade, hold…
purpshell Sep 6, 2026
75eb23a
fix(calls): correct how a failed placement surfaces, and stop announc…
purpshell Sep 6, 2026
639ec30
fix(calls): guard switchDevice before the preference write, fix two w…
purpshell Sep 6, 2026
fc37d9e
fix(calls): absorb answer and reject rejections in pmfa-call
purpshell Sep 6, 2026
fb224b0
fix(calls): reject a late backend connect, serialize the video upgrade
purpshell Sep 6, 2026
c56636b
fix(calls): absorb a dial against a disposed controller
purpshell Sep 6, 2026
007bfc3
fix(calls): absorb a hang-up against a disposed controller
purpshell Sep 6, 2026
0ddbe75
fix(calls): omit enableVideo and restartIce when signaling cannot ren…
purpshell Sep 6, 2026
e8fe3b6
feat(calls): heartbeat the calls socket
purpshell Sep 6, 2026
1aa1746
fix(calls): widen the terminal guard, absorb a disposed-controller se…
purpshell Sep 6, 2026
d93571a
fix(calls): guard every synchronous controller call from the UI surfaces
purpshell Sep 6, 2026
3464183
fix(calls): release the capture on a trackless acquisition, guard ena…
purpshell Sep 6, 2026
c390b7e
fix(calls): guard #connection after the call ends, gate the camera up…
purpshell Sep 6, 2026
b6c90c3
fix(calls): ignore a duplicate ended for an ended call; reattach enab…
purpshell Sep 6, 2026
eaf0e2c
fix(calls): make the ringing line a parameterised message
purpshell Sep 6, 2026
271bda6
fix(browser): bound the calls socket handshake with openTimeoutMs
purpshell Sep 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,82 @@
# Changelog

## Unreleased

- `createSignalingCallsBackend` no longer invents a call id for outbound
calls. Nothing on the client-token surface dials a destination, so an
invented id reached no pod and its offer was refused as not-ready forever.
Outbound calling now goes through a `place` hook that hands the destination
to the application's server and takes back the platform's call id; the
removed `createCallId` option had no working use.
- `CallsSocket` heartbeats. A half-open connection still reports OPEN, so
candidates were dropped while the media factory kept REST polling disabled;
an unanswered `ping` now closes it and reconnects. `heartbeatMs` tunes the
period, 0 disables it.
- `CallsSocket` takes a `line`, carried onto the calls it delivers. It always
reported `linkedDevice`, so a Business Calling API session showed video
controls on an audio-only line.
- `CallsSignalingClient.socketTicket` no longer names a session. The client
token is bound to one, and the API answers 403 when a request names another.
- `CallsSocket` reports a signaling client that cannot mint tickets as an
`unsupported` error and stops, instead of backing off forever against a
failure that can never succeed; ordinary ticket failures still retry, now
reported as `ticket_failed`.
- `CallsSocket` gained `onError`, so a server `error` frame reaches the
application instead of being parsed and dropped while the socket keeps
reconnecting against a permanent failure.
- Timer defaults are bound to `globalThis`. `CallsController`, `CallsSocket`,
`WebRtcMediaFactory`, and the QuickLink controller called the unbound
`setTimeout`/`setInterval` with the instance as receiver, which browsers
reject with `TypeError: Illegal invocation`.
- Calls hardening: a recovered ICE flap leaves `reconnecting` instead of
waiting for a connection-state change that may never come; mute pressed
while the microphone was still being acquired now reaches the tracks;
pushed ICE candidates are held until the answer is applied rather than
discarded; setup failures release the candidate subscription; an audio-only
call no longer keeps an unnegotiated camera track (which had made the video
upgrade unretryable); a failed upgrade rolls the camera back; the calls
WebSocket survives a throwing constructor and validates every frame shape; a
camera button in `pmfa-call` upgrades an audio call over a video-capable
line, as the React dock's already did; a
failed re-offer rolls the peer out of `have-local-offer` so later upgrades
and ICE restarts are not blocked by the stale offer; the audio→video upgrade
acquires the camera chosen during the call rather than the one selected when
it opened; a failed device swap
releases the stream it acquired; and `pmfa-call` localizes the connected
heading rather than rendering a raw status identifier.

- Added `CallsSocket` (the calls WebSocket: pushed `call.*` lifecycle events,
ICE both ways, ticket-based auth, capped reconnect) as a drop-in lifecycle
source for `createSignalingCallsBackend` and a `candidateTransport` for
`WebRtcMediaFactory`; `CallsSignalingClient` gained `renegotiate`,
`socketTicket`, and `socketUrl`; `BrowserTransport` exposes `baseUrl`.
- `CallsController` gained `enableVideo()` (audio→video upgrade by re-offer),
a `reconnecting` status with ICE restart and a bounded resumption window,
and ends the call as `pod_lost` / `capacity` when the offer is refused
with 410 / 503. The React camera button upgrades audio calls to video;
`CallStage` and `pmfa-call` show "Reconnecting…".
- Added `MessagingClient.voip.socketTicket` and `voip.agentToken`; typed
`maxSetupsPerMinute` in the client rules; refreshed the contracts and the
coverage ledger to polymorfa@b413f2c6a (renegotiate, ws-ticket,
agent-token).

- Added `MessagingClient.voip.token` for minting the browser call token, typed
the client-rules response the runtime actually returns (including the calls
bindings `maxConcurrency` and `allowedNumber`), and typed client actions.
- Added `createSignalingCallsBackend`, `IncomingCallRelay`, and
`incomingCallFromWebhook` so browser calls run end to end on the REST
signaling surface; `CallsController` now carries the calling `line` and
`capabilities`, device lists and selections, live device switching, and
`connectedAt`.
- Replaced the placeholder `CallSurface` with the complete call UI —
`IncomingCallCard`, `CallStage`, `CallControls`, `DialPad`, a pop-out call
window, and `useCallDuration` — themed from the shared appearance variables
and localized through new `calls.*` messages; `pmfa-call` follows the same
controller contract.
- Refreshed the Messaging and Platform contracts and the coverage ledger for
the `/api/voip/*` signaling routes and the `call.ended` / `call.telemetry`
webhook events.

## 0.1.0-dev.0 - 2026-08-19

- Added handwritten Messaging clients for sessions, messages, and webhooks.
Expand Down
40 changes: 30 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ The development branch contains the TypeScript server SDK, a framework-neutral
browser runtime, shared UI contracts, Web Components, React bindings, thin
Next.js server helpers, and a production-gated developer assistant. It follows
the Messaging and Platform contracts recorded at source revision
`f156af2dda13e62b6b106a542fdedb39524bdb66`. Graph-compatible APIs are outside
`b413f2c6a701c86ec593241a70145ee89cc2bcd2`. Graph-compatible APIs are outside
this SDK's initial scope.

## Package architecture
Expand Down Expand Up @@ -84,6 +84,7 @@ The handwritten Messaging resources in this milestone are:
- `business`: manage the connected Business App profile, commerce catalog,
products, collections, orders, compliance, linked accounts, and eligibility
- `calls`: reject an identified incoming Linked Device call
- `voip`: mint the browser call token used by `@polymorfa/browser` signaling
- `campaigns`: list, create, retrieve, inspect analytics, launch, pause, resume,
stop, and requeue project campaigns through the Messaging control plane
- `messages`: send every contract-defined message kind through one typed send
Expand Down Expand Up @@ -338,15 +339,34 @@ that interoperate with the Custom Elements standard. `@polymorfa/react`
provides idiomatic hooks and components, including controlled and SDK-owned
controller lifecycles.

### Calls boundary

Call media and signaling follow the implementation in `voip-v2`: browser WebRTC
media, offer/answer exchange, trickle ICE candidate submission and polling, and
the existing `/api/voip/calls/{id}` signaling paths. The application supplies a
`CallsBackend` for place, answer, reject, hangup, and lifecycle events because
that control plane is not exposed by the current `voip-v2` REST signaling
surface. The SDK does not invent group rooms, participants, reactions, hand
raising, or waiting rooms.
### Calls

Call media and signaling follow the platform's browser signaling surface:
WebRTC media, offer/answer exchange, trickle ICE candidate submission and
polling, and idempotent teardown over the `/api/voip/calls/{id}` paths, all on
the client token. `createSignalingCallsBackend` is the `CallsBackend` for that
surface; outbound calls go through its `place` hook, because starting a call
is a server-key operation and the browser can only attach media to a call the
platform already owns. `IncomingCallRelay` is where the application hands the browser the
`call.received` webhook it received on its own server
(`incomingCallFromWebhook` maps the payload). The server mints the browser
token with `MessagingClient.voip.token` after granting the session's client
rules the `voip_place`, `voip_answer`, and `voip_signal` actions; those rules
also bind calls per token (`maxConcurrency`, `allowedNumber`).

Calls carry a `line`: `linkedDevice` (a paired WhatsApp device session, audio
and video) or `cloudApi` (the WhatsApp Business Calling API, audio only). Every
component gates on the snapshot's `capabilities`, never on the line name. The
controller also owns capture/playback device choice (`setPreferredDevices`,
`switchDevice`, `refreshDevices`) so a microphone or camera swap mid-call is a
track replacement, not a renegotiation. The SDK does not invent group rooms,
participants, reactions, hand raising, or waiting rooms.

`@polymorfa/react` ships the complete call UI: `CallSurface` (incoming card,
stage, control dock, and a pop-out window), plus `IncomingCallCard`,
`CallStage`, `CallControls`, and `DialPad` for composition. The design mirrors
the official WhatsApp desktop call windows in a monochrome Material-3 voice;
colors derive from the shared appearance variables.

## Next.js and dev mode

Expand Down
Loading