-
-
Notifications
You must be signed in to change notification settings - Fork 278
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: move reducers into seperate common package, create shared state…
… for mobile
- Loading branch information
1 parent
db9e51a
commit d5be258
Showing
11 changed files
with
555 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
const baseConfig = require('../../jest.config.base'); | ||
|
||
module.exports = { | ||
...baseConfig, | ||
roots: ['<rootDir>/src', '<rootDir>/tests', '<rootDir>/../test-utils/__mocks__'], | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
{ | ||
"name": "@suite-common/bluetooth", | ||
"version": "1.0.0", | ||
"private": true, | ||
"license": "See LICENSE.md in repo root", | ||
"sideEffects": false, | ||
"main": "src/index", | ||
"scripts": { | ||
"test:unit": "yarn g:jest -c ./jest.config.js", | ||
"depcheck": "yarn g:depcheck", | ||
"type-check": "yarn g:tsc --build" | ||
}, | ||
"dependencies": { | ||
"@reduxjs/toolkit": "1.9.5", | ||
"@suite-common/redux-utils": "workspace:*", | ||
"@trezor/connect": "workspace:*" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { AsyncThunkAction } from '@reduxjs/toolkit'; | ||
|
||
declare module 'redux' { | ||
export interface Dispatch<A extends Action = AnyAction> { | ||
<TThunk extends AsyncThunkAction<any, any, any>>(thunk: TThunk): ReturnType<TThunk>; | ||
|
||
<ReturnType = any, State = any, ExtraThunkArg = any>( | ||
thunkAction: ThunkAction<ReturnType, State, ExtraThunkArg, A>, | ||
): ReturnType; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import { createAction } from '@reduxjs/toolkit'; | ||
|
||
import { | ||
BluetoothDeviceCommon, | ||
BluetoothScanStatus, | ||
DeviceBluetoothStatus, | ||
} from './bluetoothReducer'; | ||
|
||
export const BLUETOOTH_PREFIX = '@suite/bluetooth'; | ||
|
||
const adapterEventAction = createAction( | ||
`${BLUETOOTH_PREFIX}/adapter-event`, | ||
({ isPowered }: { isPowered: boolean }) => ({ payload: { isPowered } }), | ||
); | ||
|
||
type BluetoothNearbyDevicesUpdateActionPayload = { | ||
nearbyDevices: BluetoothDeviceCommon[]; | ||
}; | ||
|
||
const nearbyDevicesUpdateAction = createAction( | ||
`${BLUETOOTH_PREFIX}/nearby-devices-update`, | ||
({ nearbyDevices }: BluetoothNearbyDevicesUpdateActionPayload) => ({ | ||
payload: { nearbyDevices }, | ||
}), | ||
); | ||
|
||
type BluetoothKnownDevicesUpdateActionPayload = { | ||
knownDevices: BluetoothDeviceCommon[]; | ||
}; | ||
|
||
const knownDevicesUpdateAction = createAction( | ||
`${BLUETOOTH_PREFIX}/known-devices-update`, | ||
({ knownDevices }: BluetoothKnownDevicesUpdateActionPayload) => ({ | ||
payload: { knownDevices }, | ||
}), | ||
); | ||
|
||
const removeKnownDeviceAction = createAction( | ||
`${BLUETOOTH_PREFIX}/remove-known-device`, | ||
({ id }: { id: string }) => ({ | ||
payload: { id }, | ||
}), | ||
); | ||
|
||
const connectDeviceEventAction = createAction( | ||
`${BLUETOOTH_PREFIX}/connect-device-event`, | ||
({ connectionStatus, id }: { id: string; connectionStatus: DeviceBluetoothStatus }) => ({ | ||
payload: { id, connectionStatus }, | ||
}), | ||
); | ||
|
||
const scanStatusAction = createAction( | ||
`${BLUETOOTH_PREFIX}/scan-status`, | ||
({ status }: { status: BluetoothScanStatus }) => ({ payload: { status } }), | ||
); | ||
|
||
export const bluetoothActions = { | ||
adapterEventAction, | ||
nearbyDevicesUpdateAction, | ||
connectDeviceEventAction, | ||
scanStatusAction, | ||
knownDevicesUpdateAction, | ||
removeKnownDeviceAction, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
import { AnyAction, Draft } from '@reduxjs/toolkit'; | ||
|
||
import { createReducerWithExtraDeps } from '@suite-common/redux-utils'; | ||
import { deviceActions } from '@suite-common/wallet-core'; | ||
|
||
import { bluetoothActions } from './bluetoothActions'; | ||
|
||
export type BluetoothScanStatus = 'idle' | 'running' | 'error'; | ||
|
||
export type DeviceBluetoothStatus = | ||
| { type: 'pairing'; pin?: string } | ||
| { type: 'paired' } | ||
| { type: 'connecting' } | ||
| { type: 'connected' } | ||
| { | ||
type: 'error'; | ||
error: string; | ||
}; | ||
|
||
// Do not export this outside of this suite-common package, Suite uses ist own type | ||
// from the '@trezor/transport-bluetooth' and mobile (native) have its own type as well. | ||
export type BluetoothDeviceCommon = { | ||
id: string; | ||
name: string; | ||
data: number[]; // Todo: consider typed data-structure for this | ||
lastUpdatedTimestamp: number; | ||
}; | ||
|
||
export type DeviceBluetoothStatusType = DeviceBluetoothStatus['type']; | ||
|
||
export type BluetoothDeviceState<T extends BluetoothDeviceCommon> = { | ||
device: T; | ||
status: DeviceBluetoothStatus | null; | ||
}; | ||
|
||
export type BluetoothState<T extends BluetoothDeviceCommon> = { | ||
adapterStatus: 'unknown' | 'enabled' | 'disabled'; | ||
scanStatus: BluetoothScanStatus; | ||
nearbyDevices: BluetoothDeviceState<T>[]; | ||
|
||
// 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[]; | ||
}; | ||
|
||
export const prepareBluetoothReducerCreator = <T extends BluetoothDeviceCommon>() => { | ||
const initialState: BluetoothState<T> = { | ||
adapterStatus: 'unknown', | ||
scanStatus: 'idle', | ||
nearbyDevices: [] as BluetoothDeviceState<T>[], | ||
knownDevices: [] as T[], | ||
}; | ||
|
||
return createReducerWithExtraDeps<BluetoothState<T>>(initialState, (builder, extra) => | ||
builder | ||
.addCase(bluetoothActions.adapterEventAction, (state, { payload: { isPowered } }) => { | ||
state.adapterStatus = isPowered ? 'enabled' : 'disabled'; | ||
if (!isPowered) { | ||
state.nearbyDevices = []; | ||
state.scanStatus = 'idle'; | ||
} | ||
}) | ||
.addCase( | ||
bluetoothActions.nearbyDevicesUpdateAction, | ||
(state, { payload: { nearbyDevices } }) => { | ||
state.nearbyDevices = nearbyDevices | ||
.sort((a, b) => b.lastUpdatedTimestamp - a.lastUpdatedTimestamp) | ||
.map( | ||
(device): Draft<BluetoothDeviceState<T>> => ({ | ||
device: device as Draft<T>, | ||
status: | ||
state.nearbyDevices.find(it => it.device.id === device.id) | ||
?.status ?? null, | ||
}), | ||
); | ||
}, | ||
) | ||
.addCase( | ||
bluetoothActions.connectDeviceEventAction, | ||
(state, { payload: { id, connectionStatus } }) => { | ||
const device = state.nearbyDevices.find(it => it.device.id === id); | ||
|
||
if (device !== undefined) { | ||
device.status = connectionStatus; | ||
} | ||
}, | ||
) | ||
.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) { | ||
state.nearbyDevices = state.nearbyDevices.filter( | ||
it => it.device.id !== bluetoothProps.id, | ||
); | ||
} | ||
}) | ||
.addCase( | ||
deviceActions.connectDevice, | ||
( | ||
state, | ||
{ | ||
payload: { | ||
device: { bluetoothProps }, | ||
}, | ||
}, | ||
) => { | ||
if (bluetoothProps === undefined) { | ||
return; | ||
} | ||
|
||
const deviceState = state.nearbyDevices.find( | ||
it => it.device.id === bluetoothProps.id, | ||
); | ||
|
||
if (deviceState !== undefined) { | ||
// Once 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(deviceState.device); | ||
} | ||
} | ||
}, | ||
) | ||
.addMatcher( | ||
action => action.type === extra.actionTypes.storageLoad, | ||
(state, action: AnyAction) => { | ||
state.knownDevices = action.payload.knownDevices?.bluetooth ?? []; | ||
}, | ||
), | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { createWeakMapSelector } from '@suite-common/redux-utils'; | ||
|
||
import { BluetoothDeviceCommon, BluetoothDeviceState, BluetoothState } from './bluetoothReducer'; | ||
|
||
export type WithBluetoothState<T extends BluetoothDeviceCommon> = { | ||
bluetooth: BluetoothState<T>; | ||
}; | ||
|
||
export const selectAdapterStatus = <T extends BluetoothDeviceCommon>( | ||
state: WithBluetoothState<T>, | ||
) => state.bluetooth.adapterStatus; | ||
|
||
export const selectKnownDevices = <T extends BluetoothDeviceCommon>(state: WithBluetoothState<T>) => | ||
state.bluetooth.knownDevices; | ||
|
||
/** | ||
* We need to have generic `createWeakMapSelector.withTypes` so we need to wrap it into Higher Order Function, | ||
* but we need to make sure it is called only **once** to not break the memoization. So it is named | ||
* `prepareSelectAllDevices` and shall be called **outside** of the component to create a selector with | ||
* concrete type: | ||
* | ||
* For example: `const selectAllDevices = prepareSelectAllDevices<ConcreteBluetoothDevice>();` | ||
*/ | ||
export const prepareSelectAllDevices = <T extends BluetoothDeviceCommon>() => | ||
createWeakMapSelector.withTypes<WithBluetoothState<T>>()( | ||
[state => state.bluetooth.nearbyDevices, state => state.bluetooth.knownDevices], | ||
(nearbyDevices, knownDevices) => { | ||
const map = new Map<string, BluetoothDeviceState<T>>(); | ||
|
||
nearbyDevices.forEach(nearbyDevice => { | ||
map.set(nearbyDevice.device.id, nearbyDevice); | ||
}); | ||
|
||
knownDevices.forEach(knownDevice => { | ||
if (!map.has(knownDevice.id)) { | ||
map.set(knownDevice.id, { device: knownDevice, status: null }); | ||
} | ||
}); | ||
|
||
return Array.from(map.values()).sort( | ||
(a, b) => b.device.lastUpdatedTimestamp - a.device.lastUpdatedTimestamp, | ||
); | ||
}, | ||
); | ||
|
||
export const selectScanStatus = <T extends BluetoothDeviceCommon>(state: WithBluetoothState<T>) => | ||
state.bluetooth.scanStatus; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
export { BLUETOOTH_PREFIX, bluetoothActions } from './bluetoothActions'; | ||
|
||
export { prepareBluetoothReducerCreator } from './bluetoothReducer'; | ||
export type { | ||
BluetoothDeviceState, | ||
BluetoothScanStatus, | ||
DeviceBluetoothStatusType, | ||
} from './bluetoothReducer'; | ||
|
||
export { | ||
prepareSelectAllDevices, | ||
selectKnownDevices, | ||
selectAdapterStatus, | ||
selectScanStatus, | ||
} from './bluetoothSelectors'; |
Oops, something went wrong.