setConnectionListener
Registers a callback that fires whenever the connection state changes. The callback receives two independent axes: the popup axis (is the bridge window alive?) and the device axis (is a hardware wallet attached?). Use it to drive connection UI (spinner, connected badge, "please reconnect your device" prompt).
Signature
dcent.setConnectionListener(
listener: (state: TransportState, detail: ConnectionStateDetail) => void
): void
Parameters
| Field | Type | Description |
|---|---|---|
state | 'connected' | 'disconnected' | Popup axis only. 'connected' means the bridge popup window is open, not that a device is attached. v1 passed 'dcent-connected' / 'dcent-disconnected' here — compare against 'connected' / 'disconnected' (dcent.state.CONNECTED is still 'dcent-connected' and does not match). |
detail | ConnectionStateDetail | Both axes at once. v2 new. Older code written as function (state) {…} is still called — JavaScript ignores the extra argument — but must compare state against the new values. |
ConnectionStateDetail
| Field | Type | Description |
|---|---|---|
popup | 'connected' | 'disconnected' | Same value as the first argument — the bridge popup window. |
device | 'connected' | 'disconnected' | 'awaiting-connect-approval' | 'unknown' | The hardware wallet. 'unknown' means not observable (for example before the popup opens, after it closes, or in a newly opened popup until the bridge reports the device) — it is deliberately not reported as 'disconnected', which would be a false claim that the device was removed. 'awaiting-connect-approval' means the link is open but the user has not approved the connection on the device screen yet. v2 new |
deviceInfo | DeviceBriefInfo | undefined | Filled in only while device === 'connected'. 🔴 Like deviceReason below, the key is always present on detail — do not branch on 'deviceInfo' in detail. |
deviceReason | DeviceDisconnectReason | undefined | Filled in only while device === 'disconnected' — exactly symmetric with deviceInfo. Tells you why the connection did not hold. 🔴 The key is always present on detail (do not branch on 'deviceReason' in detail), and it can still be undefined while disconnected — an older bridge, or a reason this connector version does not recognise. Always handle a default. v2 new |
DeviceDisconnectReason
Why the device axis went to 'disconnected'. The connect- prefix separates these from signing rejection — a rejected signature is request-scoped and surfaces on that request's response (the promise resolves with header.status === 'failure' and user_cancel), never on this axis.
| Value | Meaning |
|---|---|
'connect-approval-rejected' | The user rejected the connection on the device screen. USB only — see the next row for BLE. |
'connect-approval-cancelled' | Cancelled from the popup, a BLE session was handed over, or the bridge could not observe the cause. 🔴 On BLE a user rejection also arrives here, not as 'connect-approval-rejected' — the bridge cannot tell a rejection apart from a dropped link on that transport. |
'device-removed' | Cable unplugged, GATT link dropped, or the transport was released explicitly. A physical disconnect is reported here even while waiting for approval. |
'reconnect-timeout' | The automatic reconnect budget ran out. |
'transport-failed' | The link or initialization failed — for example a fatal transport error while waiting for approval (the user never approved, rejected, or cancelled), or an attached device that does not answer the device-info request. Physical removal is 'device-removed', not this. |
A reason does not settle your in-flight request. Receiving deviceReason tells you the connection failed, but the connector does not settle a pending signTransaction / getAccountInfo promise because of it. While the first connection is still being set up (device picker / on-device approval), the request is bounded by a fixed 60 s handshake timeout, after which the connector closes the popup. Once the connection is up, unless the bridge answers the request, the promise waits up to the timeout set by setTimeOutMs (180000 ms / 3 minutes by default), counted from when the request is sent — the connector sends one request at a time, so a request queued behind another first waits for that one. Either way it then resolves with header.status === 'failure' and time_out (or pop-up_closed if the popup is closed first) — it does not reject. If you want the rejection UI to close immediately, cancel your own pending work from this callback — do not assume the promise settles at the same moment.
DeviceBriefInfo
Display-only fields. Every field is optional — treat a missing value as "not reported". The field names match getDeviceInfo's response, so an app using both APIs does not have to learn two vocabularies for the same value.
| Field | Type | Same as getDeviceInfo | Description |
|---|---|---|---|
deviceId | string? | deviceId | Device identifier — the only field that distinguishes two units of the same model. See the note below. |
label | string? | label | User-set device label. When the device has no label set, the bridge sends the deviceId here instead (so label can equal deviceId); getDeviceInfo does not substitute the deviceId. |
version | string? | version | Firmware version string. |
deviceModel | string? | deviceModel | Model name. Absent on v1 (Biometric) devices — it is only reported for models the wallet can identify, so treat a missing value as "older device", not as an error. |
connectType | 'usb' | 'ble' ? | connectType | How the device is attached. |
coinCount | number? | (count of coin_list) | Number of coins installed. The list is not sent — an installed-coin set is semi-identifying, and this signal goes out unsolicited. Call getDeviceInfo for the list. |
deviceId identifies the device — know the trade-off. This signal is pushed automatically without the app asking, so an app that merely subscribes can recognise a returning device. It is carried anyway because it is the only field that separates two units of the same model — label is not a reliable substitute (it defaults to the model name on some devices, and users may set the same label on two units). ksm_version and the device state are still not carried — they are not needed to identify a device. Call getDeviceInfo if you need those.
Example
dcent.setConnectionListener((state, detail) => {
// popup axis — 'connected' / 'disconnected' (v1 used 'dcent-connected' / 'dcent-disconnected')
if (state === 'connected') showPopupOpen()
else showPopupClosed()
// device axis — new
if (detail.device === 'connected') {
showDevice(detail.deviceInfo?.label, detail.deviceInfo?.connectType)
} else if (detail.device === 'awaiting-connect-approval') {
showApprovalWaiting() // approve the connection on the device screen
} else if (detail.device === 'disconnected') {
promptReconnectDevice(detail.deviceReason) // may be undefined — handle a default
} else {
hideDeviceBadge() // 'unknown' — nothing observed yet
}
})
Do not let the final else swallow the new value. Without an explicit 'awaiting-connect-approval' branch, the approval wait renders as 'unknown' ("nothing observed"), which is exactly the state this value exists to distinguish.
The listener fires for either axis. It is called when either axis changes, so state (the first argument) may repeat the same value across consecutive calls. If your handler does work that must not repeat (analytics, a toast, a reconnect attempt), compare against your own previous value instead of assuming every call is a change of state.
The listener is cached and re-registered automatically after the transport is reset (e.g. after popupWindowClose + reconnect). Registering the exact same function reference twice is a no-op — it stays subscribed once. Registering a different closure on every render keeps them all, and there is no unregister call — register once, outside the render path.
Notes
-
The two axes are independent. Unplugging USB while the popup stays open changes only
detail.device;statestays'connected'. -
Closing the popup sets
popup: 'disconnected'anddevice: 'unknown'in a single call, and clearsdeviceReason. Reopening the popup does the same. -
Initial values before anything happens:
popup: 'disconnected',device: 'unknown'. -
detailanddetail.deviceInfoare frozen — mutating them does not affect the connector, and your changes will not persist. -
A
'disconnected'→'disconnected'transition does fire the listener when onlydeviceReasonchanged — the reason is a discriminator, not a display field. 🔴 The converse also holds: the same reason delivered twice in a row is de-duplicated and does not fire. A repeated rejection with the same reason fires again only if some other state is applied in between — for example'awaiting-connect-approval','connected', or a popup close/reopen (which resets the device axis to'unknown'). Do not count rejections by counting callbacks. -
Older bridge popups that do not emit the device signal simply leave
detail.deviceat'unknown'; the popup axis keeps working unchanged. The same applies to'awaiting-connect-approval'anddeviceReason— a bridge that does not send them simply never produces those values.