feat(connect): add DEVICE.THP_PAIRING_STATUS_CHANGED event

This commit is contained in:
Szymon Lesisz
2026-02-04 16:05:52 +01:00
committed by Szymon Lesisz
parent 4c0d26f872
commit b2fd9e2d9e
7 changed files with 78 additions and 1 deletions

View File

@@ -560,6 +560,19 @@ const onThpCredentialsChangedHandler =
);
};
const onThpPhaseChangedHandler =
(device: Device, context: CoreContext) =>
(payload: DeviceEvents['device-thp_pairing_status_changed']) => {
const { sendCoreMessage } = context;
sendCoreMessage(
createDeviceMessage(DEVICE.THP_PAIRING_STATUS_CHANGED, {
device: device.toMessageObject(),
...payload,
}),
);
};
const registerDeviceEvents =
(context: CoreContext, method?: AbstractMethod<any>) => (device: Device) => {
device.removeAllListeners();
@@ -585,6 +598,7 @@ const registerDeviceEvents =
});
device.on(DEVICE.THP_PAIRING, onThpPairingHandler(device, context));
device.on(DEVICE.THP_CREDENTIALS_CHANGED, onThpCredentialsChangedHandler(device, context));
device.on(DEVICE.THP_PAIRING_STATUS_CHANGED, onThpPhaseChangedHandler(device, context));
};
/**

View File

@@ -37,6 +37,7 @@ import {
DeviceButtonRequestPayload,
DeviceThpCredentialsChangedPayload,
DeviceThpPairingPayload,
DeviceThpPairingStatus,
DeviceVersionChanged,
UI,
UiResponsePassphrase,
@@ -112,6 +113,7 @@ export interface DeviceEvents {
callback: (response: Result<UiResponseThpPairingTag['payload']>) => void;
};
[DEVICE.THP_CREDENTIALS_CHANGED]: DeviceThpCredentialsChangedPayload;
[DEVICE.THP_PAIRING_STATUS_CHANGED]: DeviceThpPairingStatus;
}
interface DeviceLifecycleEvents {

View File

@@ -39,17 +39,37 @@ export const getThpChannel = async (device: Device, withInteraction?: boolean) =
await thpHandshake(device, isPinLocked);
}
}
if (thpState.phase === 'pairing' && withInteraction) {
const startPairing = thpState.phase === 'pairing' && withInteraction;
if (startPairing) {
device.emit(DEVICE.THP_PAIRING_STATUS_CHANGED, { status: 'started' });
// start pairing with UI interaction
await thpPairing(device);
}
if (thpState.phase !== 'paired') {
device.emit(DEVICE.THP_PAIRING_STATUS_CHANGED, {
status: 'failed',
message: 'THP Locked',
});
return 'thp-locked';
} else if (startPairing) {
device.emit(DEVICE.THP_PAIRING_STATUS_CHANGED, { status: 'finished' });
}
} catch (error) {
thpState.resetState();
if (error.code === 'Device_ThpPairingTagInvalid') {
// ignore. phase already changed to 'invalid-tag'
} else if (error.code === 'Failure_ActionCancelled') {
device.emit(DEVICE.THP_PAIRING_STATUS_CHANGED, { status: 'canceled' });
} else {
device.emit(DEVICE.THP_PAIRING_STATUS_CHANGED, {
status: 'failed',
message: error.message,
});
}
throw error;
}
};

View File

@@ -208,6 +208,13 @@ const waitForPairingTag = async (device: Device) => {
// catch pairing tag mismatch
// DataError since 2.10.0 https://github.com/trezor/trezor-firmware/commit/b0c3be9b1d95946471ebdab27918a3f652cf11e9
if (e.code === 'Failure_FirmwareError' || e.code === 'Failure_DataError') {
if ('tag' in pairingResponse) {
device.emit(DEVICE.THP_PAIRING_STATUS_CHANGED, {
status: 'invalid-tag',
tag: pairingResponse.tag,
});
}
// 'Unexpected Code Entry Tag'
throw ERRORS.TypedError('Device_ThpPairingTagInvalid', e.message);
}

View File

@@ -103,6 +103,7 @@ export const abortThpWorkflow = async (device: Device) => {
await thpState.pairingTagPromise.abort();
await device.getCurrentSession().cancelCall();
thpState.resetState();
device.emit(DEVICE.THP_PAIRING_STATUS_CHANGED, { status: 'canceled' });
} else if (thpState.cancelablePromise) {
thpState.sync('send', 'Cancel');
await device.getCurrentSession().send('Cancel', {});

View File

@@ -39,6 +39,7 @@ export const DEVICE = {
PASSPHRASE_ON_DEVICE: 'passphrase_on_device',
WORD: 'word',
THP_PAIRING: 'thp_pairing', // ask UI for pairing tag
THP_PAIRING_STATUS_CHANGED: 'device-thp_pairing_status_changed',
} as const;
export interface DeviceButtonRequestPayload extends Omit<PROTO.ButtonRequest, 'code'> {
@@ -63,6 +64,26 @@ export type DeviceThpCredentialsChangedPayload = {
credentials: ThpCredentials;
};
export type DeviceThpPairingStatus =
| {
status: 'started' | 'canceled' | 'finished';
}
| {
status: 'failed';
message: string;
}
| {
status: 'invalid-tag';
tag: string;
};
export interface DeviceThpPairingStatusChanged {
type: typeof DEVICE.THP_PAIRING_STATUS_CHANGED;
payload: DeviceThpPairingStatus & {
device: Device;
};
}
export type DeviceThpPairingPayload = {
availableMethods: ThpPairingMethod[];
selectedMethod: ThpPairingMethod; // expected pairing method
@@ -94,6 +115,7 @@ export type DeviceEvent =
}
| DeviceButtonRequest
| DeviceThpCredentialsChanged
| DeviceThpPairingStatusChanged
| DeviceVersionChanged
| DeviceTrezorPushNotification;

View File

@@ -44,6 +44,17 @@ export const events = (api: TrezorConnect) => {
if (event.type === 'device-trezor_push_notification') {
return;
}
if (event.type === 'device-thp_pairing_status_changed') {
const { payload } = event;
if (payload.status === 'invalid-tag') {
payload.tag.toLowerCase();
}
if (payload.status === 'failed') {
payload.message.toLowerCase();
}
return;
}
const { payload } = event;
payload.path.toLowerCase();
if (payload.type === 'acquired') {