mirror of
https://github.com/trezor/trezor-suite.git
synced 2026-03-23 07:37:10 +01:00
217 lines
9.6 KiB
TypeScript
217 lines
9.6 KiB
TypeScript
import { AnyAction, Draft } from '@reduxjs/toolkit';
|
|
|
|
import { createReducerWithExtraDeps } from '@suite-common/redux-utils';
|
|
import {
|
|
DeviceConnectActionPayload,
|
|
deviceActions,
|
|
} from '@suite-common/wallet-core/src/device/deviceActions';
|
|
import { BluetoothDeviceId, TrezorPushNotificationType } from '@trezor/connect';
|
|
|
|
import { bluetoothActions } from './bluetoothActions';
|
|
import { deserializeBluetoothDeviceSerialization } from './deserializeBluetoothDeviceSerialization';
|
|
import { filterOutOldDuplicates } from './filterOutOldDuplicates';
|
|
import {
|
|
BluetoothAdapterStatus,
|
|
BluetoothAutoConnectPolicy,
|
|
BluetoothDeviceCommon,
|
|
BluetoothScanStatus,
|
|
} from './types';
|
|
|
|
export type BluetoothState<T extends BluetoothDeviceCommon> = {
|
|
adapterStatus: BluetoothAdapterStatus;
|
|
scanStatus: BluetoothScanStatus;
|
|
nearbyDevices: null | T[]; // Must be sorted, the newest last. Null = we haven't received first update yet
|
|
|
|
// This will be persisted. Those are devices we believed that are paired
|
|
// (because we already successfully paired them in the Suite) in the Operating System
|
|
knownDevices: T[];
|
|
ignoredDeviceIds: string[];
|
|
autoConnectPolicy: Record<BluetoothDeviceId, BluetoothAutoConnectPolicy | undefined>;
|
|
isDeviceOsUnpairingRequired: boolean;
|
|
};
|
|
|
|
export const prepareInitialState = <T extends BluetoothDeviceCommon>(): BluetoothState<T> => ({
|
|
adapterStatus: 'unknown',
|
|
scanStatus: 'idle',
|
|
nearbyDevices: null,
|
|
knownDevices: [] as T[],
|
|
ignoredDeviceIds: [],
|
|
autoConnectPolicy: {},
|
|
isDeviceOsUnpairingRequired: false,
|
|
});
|
|
|
|
export const prepareBluetoothReducerCreator = <T extends BluetoothDeviceCommon>() =>
|
|
createReducerWithExtraDeps<BluetoothState<T>>(prepareInitialState<T>(), (builder, extra) =>
|
|
builder
|
|
.addCase(bluetoothActions.adapterEventAction, (state, { payload: { status } }) => {
|
|
state.adapterStatus = status;
|
|
if (status !== 'enabled') {
|
|
state.nearbyDevices = [];
|
|
state.scanStatus = 'idle';
|
|
}
|
|
})
|
|
.addCase(
|
|
bluetoothActions.nearbyDevicesUpdateAction,
|
|
(state, { payload: { nearbyDevices } }) => {
|
|
state.nearbyDevices = filterOutOldDuplicates(
|
|
nearbyDevices
|
|
// Devices with 'pairing-error' status should NOT be displayed in the list, as it
|
|
// won't be possible to connect to them ever again. User has to start pairing again,
|
|
// which would produce a device with new id.
|
|
.filter(
|
|
nearbyDevice =>
|
|
nearbyDevice.connectionStatus?.type !== 'pairing-error' &&
|
|
!state.ignoredDeviceIds.includes(nearbyDevice.id),
|
|
) as Draft<T>[],
|
|
) as Draft<T>[];
|
|
},
|
|
)
|
|
.addCase(
|
|
bluetoothActions.updateDeviceConnectionStatus,
|
|
(state, { payload: { deviceId, connectionStatus } }) => {
|
|
if (state.nearbyDevices !== null) {
|
|
state.nearbyDevices = state.nearbyDevices.map(it =>
|
|
it.id === deviceId ? { ...it, connectionStatus } : it,
|
|
) as Draft<T>[];
|
|
}
|
|
|
|
state.knownDevices = state.knownDevices.map(it =>
|
|
it.id === deviceId ? { ...it, connectionStatus } : it,
|
|
) as Draft<T>[];
|
|
},
|
|
)
|
|
.addCase(bluetoothActions.deviceUpdateAction, (state, { payload: { device } }) => {
|
|
if (state.nearbyDevices !== null) {
|
|
state.nearbyDevices = state.nearbyDevices.map(it =>
|
|
it.id === device.id ? device : it,
|
|
) as Draft<T>[];
|
|
}
|
|
|
|
// pairing error can be received from NAPI connectDevice
|
|
// in that case the rust server doesnt know about it
|
|
if (device.connectionStatus.type === 'pairing-error') {
|
|
state.ignoredDeviceIds.push(device.id);
|
|
}
|
|
|
|
state.knownDevices = state.knownDevices.map(it =>
|
|
it.id === device.id ? { deviceId: it.deviceId, ...device } : it,
|
|
) as Draft<T>[];
|
|
})
|
|
.addCase(
|
|
bluetoothActions.knownDevicesUpdateAction,
|
|
(state, { payload: { knownDevices } }) => {
|
|
state.knownDevices = knownDevices as Draft<T>[];
|
|
},
|
|
)
|
|
.addCase(bluetoothActions.removeKnownDeviceAction, (state, { payload: { id } }) => {
|
|
state.knownDevices = state.knownDevices.filter(
|
|
knownDevice => knownDevice.id !== id,
|
|
);
|
|
})
|
|
.addCase(bluetoothActions.scanStatusAction, (state, { payload: { status } }) => {
|
|
state.scanStatus = status;
|
|
})
|
|
.addCase(deviceActions.deviceDisconnect, (state, { payload: { bluetoothProps } }) => {
|
|
if (bluetoothProps !== undefined) {
|
|
if (state.nearbyDevices !== null) {
|
|
state.nearbyDevices = state.nearbyDevices.filter(
|
|
it => it.id !== bluetoothProps.id,
|
|
);
|
|
}
|
|
}
|
|
})
|
|
.addCase(bluetoothActions.enableAutoConnect, (state, { payload }) => {
|
|
if (payload) {
|
|
delete state.autoConnectPolicy[payload.deviceId];
|
|
} else {
|
|
state.autoConnectPolicy = {};
|
|
}
|
|
})
|
|
.addCase(bluetoothActions.setIsDeviceOsUnpairingRequired, (state, { payload }) => {
|
|
state.isDeviceOsUnpairingRequired = payload;
|
|
})
|
|
|
|
.addMatcher(
|
|
action =>
|
|
action.type === deviceActions.connectDevice.type ||
|
|
action.type === deviceActions.connectUnacquiredDevice.type,
|
|
(
|
|
state,
|
|
{
|
|
payload: {
|
|
device: { bluetoothProps, id: deviceId },
|
|
},
|
|
}: { payload: DeviceConnectActionPayload },
|
|
) => {
|
|
if (bluetoothProps === undefined) {
|
|
return;
|
|
}
|
|
|
|
const device = state.nearbyDevices?.find(it => it.id === bluetoothProps.id);
|
|
|
|
if (device !== undefined) {
|
|
// Once the device is fully connected, we save it to the list of known devices,
|
|
// so next time user opens suite, we can automatically connect to it.
|
|
const foundKnownDevice = state.knownDevices.find(
|
|
it => it.id === bluetoothProps.id,
|
|
);
|
|
if (foundKnownDevice === undefined) {
|
|
state.knownDevices.push({ ...device, deviceId });
|
|
} else {
|
|
delete state.autoConnectPolicy[bluetoothProps.id];
|
|
// update deviceId in case it was missing
|
|
if (deviceId && foundKnownDevice.deviceId !== deviceId) {
|
|
foundKnownDevice.deviceId = deviceId;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
)
|
|
.addMatcher(
|
|
action => action.type === deviceActions.deviceDisconnect.type,
|
|
(state, { payload }: ReturnType<typeof deviceActions.deviceDisconnect>) => {
|
|
const id = payload.bluetoothProps?.id;
|
|
const affectedDevice = id && state.knownDevices.find(it => it.id === id);
|
|
if (
|
|
affectedDevice &&
|
|
state.autoConnectPolicy[id]?.type !== 'autoconnect-disabled'
|
|
) {
|
|
state.autoConnectPolicy[id] = {
|
|
type: 'recently-disconnected',
|
|
timestamp: Date.now(),
|
|
};
|
|
}
|
|
},
|
|
)
|
|
.addMatcher(
|
|
action => action.type === deviceActions.devicePushNotification.type,
|
|
(
|
|
state,
|
|
{
|
|
payload: { device, type },
|
|
}: ReturnType<typeof deviceActions.devicePushNotification>,
|
|
) => {
|
|
if (type === TrezorPushNotificationType.DISCONNECT) {
|
|
const id = device.bluetoothProps?.id;
|
|
const affectedDevice = id && state.knownDevices.find(it => it.id === id);
|
|
if (affectedDevice) {
|
|
state.autoConnectPolicy[id] = {
|
|
type: 'autoconnect-disabled',
|
|
};
|
|
}
|
|
}
|
|
},
|
|
)
|
|
.addMatcher(
|
|
action => action.type === extra.actionTypes.storageLoad,
|
|
(state, action: AnyAction) => {
|
|
const loadedKnownDevices = (action.payload?.bluetooth?.knownDevices ??
|
|
[]) as T[];
|
|
|
|
state.knownDevices = loadedKnownDevices.map(
|
|
deserializeBluetoothDeviceSerialization,
|
|
) as Draft<T>[];
|
|
},
|
|
),
|
|
);
|