Skip to main content

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​

FieldTypeDescription
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).
detailConnectionStateDetailBoth 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​

FieldTypeDescription
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
deviceInfoDeviceBriefInfo | undefinedFilled in only while device === 'connected'. 🔴 Like deviceReason below, the key is always present on detail — do not branch on 'deviceInfo' in detail.
deviceReasonDeviceDisconnectReason | undefinedFilled 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.

ValueMeaning
'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.
warning

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.

FieldTypeSame as getDeviceInfoDescription
deviceIdstring?deviceIdDevice identifier — the only field that distinguishes two units of the same model. See the note below.
labelstring?labelUser-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.
versionstring?versionFirmware version string.
deviceModelstring?deviceModelModel 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' ?connectTypeHow the device is attached.
coinCountnumber?(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.
warning

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
}
})
warning

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.

warning

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.

info

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; state stays 'connected'.

  • Closing the popup sets popup: 'disconnected' and device: 'unknown' in a single call, and clears deviceReason. Reopening the popup does the same.

  • Initial values before anything happens: popup: 'disconnected', device: 'unknown'.

  • detail and detail.deviceInfo are frozen — mutating them does not affect the connector, and your changes will not persist.

  • A 'disconnected' → 'disconnected' transition does fire the listener when only deviceReason changed — 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.device at 'unknown'; the popup axis keeps working unchanged. The same applies to 'awaiting-connect-approval' and deviceReason — a bridge that does not send them simply never produces those values.

  • setSignProgressListener

  • setTransport

  • popupWindowClose

  • Error Codes