Skip to content

Commit 7495599

Browse files
feat: update shared state fro bluetooth to accomodate changes in the ipc
1 parent cbf8a31 commit 7495599

6 files changed

+114
-141
lines changed

suite-common/bluetooth/src/bluetoothActions.ts

+3-7
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import { createAction } from '@reduxjs/toolkit';
22

3-
import {
4-
BluetoothDeviceCommon,
5-
BluetoothScanStatus,
6-
DeviceBluetoothStatus,
7-
} from './bluetoothReducer';
3+
import { BluetoothDeviceCommon, BluetoothScanStatus } from './bluetoothReducer';
84

95
export const BLUETOOTH_PREFIX = '@suite/bluetooth';
106

@@ -44,8 +40,8 @@ const removeKnownDeviceAction = createAction(
4440

4541
const connectDeviceEventAction = createAction(
4642
`${BLUETOOTH_PREFIX}/connect-device-event`,
47-
({ connectionStatus, id }: { id: string; connectionStatus: DeviceBluetoothStatus }) => ({
48-
payload: { id, connectionStatus },
43+
({ device }: { device: BluetoothDeviceCommon }) => ({
44+
payload: { device },
4945
}),
5046
);
5147

suite-common/bluetooth/src/bluetoothReducer.ts

+40-31
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,19 @@ import { bluetoothActions } from './bluetoothActions';
77

88
export type BluetoothScanStatus = 'idle' | 'running' | 'error';
99

10-
export type DeviceBluetoothStatus =
10+
export type DeviceBluetoothConnectionStatus =
11+
| { type: 'disconnected' }
1112
| { type: 'pairing'; pin?: string }
1213
| { type: 'paired' }
1314
| { type: 'connecting' }
1415
| { type: 'connected' }
1516
| {
16-
type: 'error';
17+
type: 'pairing-error'; // This device cannot be paired ever again (new macAddress, new device)
1718
error: string;
19+
}
20+
| {
21+
type: 'connection-error'; // Out-of-range, offline, in the faraday cage, ...
22+
error: string; // Timeout, connection aborted, ...
1823
};
1924

2025
// Do not export this outside of this suite-common package, Suite uses ist own type
@@ -24,19 +29,15 @@ export type BluetoothDeviceCommon = {
2429
name: string;
2530
data: number[]; // Todo: consider typed data-structure for this
2631
lastUpdatedTimestamp: number;
32+
connectionStatus: DeviceBluetoothConnectionStatus;
2733
};
2834

29-
export type DeviceBluetoothStatusType = DeviceBluetoothStatus['type'];
30-
31-
export type BluetoothDeviceState<T extends BluetoothDeviceCommon> = {
32-
device: T;
33-
status: DeviceBluetoothStatus | null;
34-
};
35+
export type DeviceBluetoothConnectionStatusType = DeviceBluetoothConnectionStatus['type'];
3536

3637
export type BluetoothState<T extends BluetoothDeviceCommon> = {
3738
adapterStatus: 'unknown' | 'enabled' | 'disabled';
3839
scanStatus: BluetoothScanStatus;
39-
nearbyDevices: BluetoothDeviceState<T>[];
40+
nearbyDevices: T[]; // Must be sorted, newest last
4041

4142
// This will be persisted, those are devices we believed that are paired
4243
// (because we already successfully paired them in the Suite) in the Operating System
@@ -47,7 +48,7 @@ export const prepareBluetoothReducerCreator = <T extends BluetoothDeviceCommon>(
4748
const initialState: BluetoothState<T> = {
4849
adapterStatus: 'unknown',
4950
scanStatus: 'idle',
50-
nearbyDevices: [] as BluetoothDeviceState<T>[],
51+
nearbyDevices: [] as T[],
5152
knownDevices: [] as T[],
5253
};
5354

@@ -64,25 +65,27 @@ export const prepareBluetoothReducerCreator = <T extends BluetoothDeviceCommon>(
6465
bluetoothActions.nearbyDevicesUpdateAction,
6566
(state, { payload: { nearbyDevices } }) => {
6667
state.nearbyDevices = nearbyDevices
67-
.sort((a, b) => b.lastUpdatedTimestamp - a.lastUpdatedTimestamp)
68-
.map(
69-
(device): Draft<BluetoothDeviceState<T>> => ({
70-
device: device as Draft<T>,
71-
status:
72-
state.nearbyDevices.find(it => it.device.id === device.id)
73-
?.status ?? null,
74-
}),
75-
);
68+
// Devices with 'pairing-error' status should NOT be displayed in the list, as it
69+
// won't be possible to connect to them ever again. User has to start pairing again,
70+
// which would produce a device with new id.
71+
.filter(
72+
nearbyDevice => nearbyDevice.connectionStatus?.type !== 'pairing-error',
73+
)
74+
.sort(
75+
(a, b) => b.lastUpdatedTimestamp - a.lastUpdatedTimestamp,
76+
) as Draft<T>[];
7677
},
7778
)
7879
.addCase(
7980
bluetoothActions.connectDeviceEventAction,
80-
(state, { payload: { id, connectionStatus } }) => {
81-
const device = state.nearbyDevices.find(it => it.device.id === id);
81+
(state, { payload: { device } }) => {
82+
state.nearbyDevices = state.nearbyDevices.map(it =>
83+
it.id === device.id ? device : it,
84+
) as Draft<T>[];
8285

83-
if (device !== undefined) {
84-
device.status = connectionStatus;
85-
}
86+
state.knownDevices = state.knownDevices.map(it =>
87+
it.id === device.id ? device : it,
88+
) as Draft<T>[];
8689
},
8790
)
8891
.addCase(
@@ -102,7 +105,7 @@ export const prepareBluetoothReducerCreator = <T extends BluetoothDeviceCommon>(
102105
.addCase(deviceActions.deviceDisconnect, (state, { payload: { bluetoothProps } }) => {
103106
if (bluetoothProps !== undefined) {
104107
state.nearbyDevices = state.nearbyDevices.filter(
105-
it => it.device.id !== bluetoothProps.id,
108+
it => it.id !== bluetoothProps.id,
106109
);
107110
}
108111
})
@@ -120,26 +123,32 @@ export const prepareBluetoothReducerCreator = <T extends BluetoothDeviceCommon>(
120123
return;
121124
}
122125

123-
const deviceState = state.nearbyDevices.find(
124-
it => it.device.id === bluetoothProps.id,
125-
);
126+
const device = state.nearbyDevices.find(it => it.id === bluetoothProps.id);
126127

127-
if (deviceState !== undefined) {
128+
if (device !== undefined) {
128129
// Once device is fully connected, we save it to the list of known devices
129130
// so next time user opens suite we can automatically connect to it.
130131
const foundKnownDevice = state.knownDevices.find(
131132
it => it.id === bluetoothProps.id,
132133
);
133134
if (foundKnownDevice === undefined) {
134-
state.knownDevices.push(deviceState.device);
135+
state.knownDevices.push(device);
135136
}
136137
}
137138
},
138139
)
139140
.addMatcher(
140141
action => action.type === extra.actionTypes.storageLoad,
141142
(state, action: AnyAction) => {
142-
state.knownDevices = action.payload.knownDevices?.bluetooth ?? [];
143+
const loadedKnownDevices = (action.payload.knownDevices?.bluetooth ??
144+
[]) as T[];
145+
146+
state.knownDevices = loadedKnownDevices.map(
147+
(it): T => ({
148+
...it,
149+
connectionStatus: { type: 'disconnected' },
150+
}),
151+
) as Draft<T>[];
143152
},
144153
),
145154
);

suite-common/bluetooth/src/bluetoothSelectors.ts

+10-12
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createWeakMapSelector } from '@suite-common/redux-utils';
22

3-
import { BluetoothDeviceCommon, BluetoothDeviceState, BluetoothState } from './bluetoothReducer';
3+
import { BluetoothDeviceCommon, BluetoothState } from './bluetoothReducer';
44

55
export type WithBluetoothState<T extends BluetoothDeviceCommon> = {
66
bluetooth: BluetoothState<T>;
@@ -25,21 +25,19 @@ export const prepareSelectAllDevices = <T extends BluetoothDeviceCommon>() =>
2525
createWeakMapSelector.withTypes<WithBluetoothState<T>>()(
2626
[state => state.bluetooth.nearbyDevices, state => state.bluetooth.knownDevices],
2727
(nearbyDevices, knownDevices) => {
28-
const map = new Map<string, BluetoothDeviceState<T>>();
28+
const map = new Map<string, T>();
2929

30-
nearbyDevices.forEach(nearbyDevice => {
31-
map.set(nearbyDevice.device.id, nearbyDevice);
32-
});
30+
knownDevices.forEach(knownDevice => map.set(knownDevice.id, knownDevice));
31+
32+
const nearbyDevicesCopy = [...nearbyDevices];
33+
nearbyDevicesCopy.reverse(); // We want to have the newest devices first in the UI
3334

34-
knownDevices.forEach(knownDevice => {
35-
if (!map.has(knownDevice.id)) {
36-
map.set(knownDevice.id, { device: knownDevice, status: null });
37-
}
35+
nearbyDevicesCopy.forEach(nearbyDevice => {
36+
map.delete(nearbyDevice.id); // Delete and re-add to change the order, replace would keep original order
37+
map.set(nearbyDevice.id, nearbyDevice);
3838
});
3939

40-
return Array.from(map.values()).sort(
41-
(a, b) => b.device.lastUpdatedTimestamp - a.device.lastUpdatedTimestamp,
42-
);
40+
return Array.from(map.values());
4341
},
4442
);
4543

suite-common/bluetooth/src/index.ts

+1-5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
export { BLUETOOTH_PREFIX, bluetoothActions } from './bluetoothActions';
22

33
export { prepareBluetoothReducerCreator } from './bluetoothReducer';
4-
export type {
5-
BluetoothDeviceState,
6-
BluetoothScanStatus,
7-
DeviceBluetoothStatusType,
8-
} from './bluetoothReducer';
4+
export type { BluetoothScanStatus, DeviceBluetoothConnectionStatusType } from './bluetoothReducer';
95

106
export {
117
prepareSelectAllDevices,

suite-common/bluetooth/tests/bluetoothReducer.test.ts

+28-71
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { configureMockStore, extraDependenciesMock } from '@suite-common/test-ut
55
import { deviceActions } from '@suite-common/wallet-core';
66
import { Device } from '@trezor/connect';
77

8-
import { BluetoothDeviceState, bluetoothActions, prepareBluetoothReducerCreator } from '../src';
8+
import { bluetoothActions, prepareBluetoothReducerCreator } from '../src';
99
import { BluetoothDeviceCommon, BluetoothState } from '../src/bluetoothReducer';
1010

1111
const bluetoothReducer =
@@ -14,38 +14,24 @@ const bluetoothReducer =
1414
const initialState: BluetoothState<BluetoothDeviceCommon> = {
1515
adapterStatus: 'unknown',
1616
scanStatus: 'idle',
17-
nearbyDevices: [] as BluetoothDeviceState<BluetoothDeviceCommon>[],
17+
nearbyDevices: [] as BluetoothDeviceCommon[],
1818
knownDevices: [] as BluetoothDeviceCommon[],
1919
};
2020

21-
const bluetoothStateDeviceA: BluetoothDeviceState<BluetoothDeviceCommon> = {
22-
device: {
23-
id: 'A',
24-
data: [],
25-
name: 'Trezor A',
26-
lastUpdatedTimestamp: 1,
27-
},
28-
status: { type: 'pairing' },
21+
const pairingDeviceA: BluetoothDeviceCommon = {
22+
id: 'A',
23+
data: [],
24+
name: 'Trezor A',
25+
lastUpdatedTimestamp: 1,
26+
connectionStatus: { type: 'pairing' },
2927
};
3028

31-
const bluetoothStateDeviceB: BluetoothDeviceState<BluetoothDeviceCommon> = {
32-
device: {
33-
id: 'B',
34-
data: [],
35-
name: 'Trezor B',
36-
lastUpdatedTimestamp: 2,
37-
},
38-
status: null,
39-
};
40-
41-
const bluetoothStateDeviceC: BluetoothDeviceState<BluetoothDeviceCommon> = {
42-
device: {
43-
id: 'C',
44-
data: [],
45-
name: 'Trezor C',
46-
lastUpdatedTimestamp: 3,
47-
},
48-
status: null,
29+
const disconnectedDeviceB: BluetoothDeviceCommon = {
30+
id: 'B',
31+
data: [],
32+
name: 'Trezor B',
33+
lastUpdatedTimestamp: 2,
34+
connectionStatus: { type: 'disconnected' },
4935
};
5036

5137
describe('bluetoothReducer', () => {
@@ -63,53 +49,27 @@ describe('bluetoothReducer', () => {
6349
expect(store.getState().bluetooth.adapterStatus).toEqual('disabled');
6450
});
6551

66-
it('sorts the devices based on the `lastUpdatedTimestamp` and keeps the status for already existing device', () => {
67-
const store = configureMockStore({
68-
extra: {},
69-
reducer: combineReducers({ bluetooth: bluetoothReducer }),
70-
preloadedState: {
71-
bluetooth: {
72-
...initialState,
73-
nearbyDevices: [bluetoothStateDeviceB, bluetoothStateDeviceA],
74-
},
75-
},
76-
});
77-
78-
const nearbyDevices: BluetoothDeviceCommon[] = [
79-
bluetoothStateDeviceA.device,
80-
bluetoothStateDeviceC.device,
81-
];
82-
83-
store.dispatch(bluetoothActions.nearbyDevicesUpdateAction({ nearbyDevices }));
84-
expect(store.getState().bluetooth.nearbyDevices).toEqual([
85-
bluetoothStateDeviceC,
86-
// No `B` device present, it was dropped
87-
{
88-
device: bluetoothStateDeviceA.device,
89-
status: { type: 'pairing' }, // Keeps the pairing status
90-
},
91-
]);
92-
});
93-
9452
it('changes the status of the given device during pairing process', () => {
9553
const store = configureMockStore({
9654
extra: {},
9755
reducer: combineReducers({ bluetooth: bluetoothReducer }),
9856
preloadedState: {
99-
bluetooth: { ...initialState, nearbyDevices: [bluetoothStateDeviceA] },
57+
bluetooth: { ...initialState, nearbyDevices: [pairingDeviceA] },
10058
},
10159
});
10260

10361
store.dispatch(
10462
bluetoothActions.connectDeviceEventAction({
105-
id: 'A',
106-
connectionStatus: { type: 'pairing', pin: '12345' },
63+
device: {
64+
...pairingDeviceA,
65+
connectionStatus: { type: 'pairing', pin: '12345' },
66+
},
10767
}),
10868
);
10969
expect(store.getState().bluetooth.nearbyDevices).toEqual([
11070
{
111-
device: bluetoothStateDeviceA.device,
112-
status: { type: 'pairing', pin: '12345' },
71+
...pairingDeviceA,
72+
connectionStatus: { type: 'pairing', pin: '12345' },
11373
},
11474
]);
11575
});
@@ -121,10 +81,7 @@ describe('bluetoothReducer', () => {
12181
preloadedState: { bluetooth: initialState },
12282
});
12383

124-
const knownDeviceToAdd: BluetoothDeviceCommon[] = [
125-
bluetoothStateDeviceA.device,
126-
bluetoothStateDeviceB.device,
127-
];
84+
const knownDeviceToAdd: BluetoothDeviceCommon[] = [pairingDeviceA, disconnectedDeviceB];
12885

12986
store.dispatch(
13087
bluetoothActions.knownDevicesUpdateAction({ knownDevices: knownDeviceToAdd }),
@@ -133,15 +90,15 @@ describe('bluetoothReducer', () => {
13390

13491
store.dispatch(bluetoothActions.removeKnownDeviceAction({ id: 'A' }));
13592

136-
expect(store.getState().bluetooth.knownDevices).toEqual([bluetoothStateDeviceB.device]);
93+
expect(store.getState().bluetooth.knownDevices).toEqual([disconnectedDeviceB]);
13794
});
13895

13996
it('removes device from nearbyDevices when the device is disconnected by TrezorConnect', () => {
14097
const store = configureMockStore({
14198
extra: {},
14299
reducer: combineReducers({ bluetooth: bluetoothReducer }),
143100
preloadedState: {
144-
bluetooth: { ...initialState, nearbyDevices: [bluetoothStateDeviceA] },
101+
bluetooth: { ...initialState, nearbyDevices: [pairingDeviceA] },
145102
},
146103
});
147104

@@ -154,9 +111,9 @@ describe('bluetoothReducer', () => {
154111
});
155112

156113
it('stores a device in `knownDevices` when device is connected by TrezorConnect', () => {
157-
const nearbyDevice: BluetoothDeviceState<BluetoothDeviceCommon> = {
158-
device: bluetoothStateDeviceA.device,
159-
status: { type: 'connected' },
114+
const nearbyDevice: BluetoothDeviceCommon = {
115+
...pairingDeviceA,
116+
connectionStatus: { type: 'connected' },
160117
};
161118

162119
const store = configureMockStore({
@@ -177,6 +134,6 @@ describe('bluetoothReducer', () => {
177134
settings: { defaultWalletLoading: 'passphrase' },
178135
}),
179136
);
180-
expect(store.getState().bluetooth.knownDevices).toEqual([nearbyDevice.device]);
137+
expect(store.getState().bluetooth.knownDevices).toEqual([nearbyDevice]);
181138
});
182139
});

0 commit comments

Comments
 (0)