Skip to content

Commit

Permalink
feat: add Bluetooth common reducers and actions
Browse files Browse the repository at this point in the history
  • Loading branch information
peter-sanderson committed Feb 17, 2025
1 parent ed8e7e8 commit 1348459
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 0 deletions.
17 changes: 17 additions & 0 deletions suite-common/bluetooth/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "@suite-common/bluetooth",
"version": "1.0.0",
"private": true,
"license": "See LICENSE.md in repo root",
"sideEffects": false,
"main": "src/index",
"scripts": {
"depcheck": "yarn g:depcheck",
"type-check": "yarn g:tsc --build"
},
"dependencies": {
"@reduxjs/toolkit": "1.9.5",
"@suite-common/redux-utils": "workspace:*",
"@suite-common/wallet-core": "workspace:*"
}
}
45 changes: 45 additions & 0 deletions suite-common/bluetooth/src/bluetoothActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { createAction } from '@reduxjs/toolkit';

import {
BluetoothDevice,
BluetoothScanStatus,
DeviceBluetoothStatus,
} from './bluetoothReducerCreator';

export const BLUETOOTH_PREFIX = '@suite/bluetooth';

export const bluetoothAdapterEventAction = createAction(
`${BLUETOOTH_PREFIX}/adapter-event`,
({ isPowered }: { isPowered: boolean }) => ({ payload: { isPowered } }),
);

type BluetoothDeviceListUpdatePayload = {
devices: BluetoothDevice[];
knownDevices: BluetoothDevice[];
};

export const bluetoothDeviceListUpdate = createAction(
`${BLUETOOTH_PREFIX}/device-list-update`,
({ devices, knownDevices }: BluetoothDeviceListUpdatePayload) => ({
payload: { devices, knownDevices },
}),
);

export const bluetoothConnectDeviceEventAction = createAction(
`${BLUETOOTH_PREFIX}/device-connection-status`,
({ connectionStatus, id }: { id: string; connectionStatus: DeviceBluetoothStatus }) => ({
payload: { id, connectionStatus },
}),
);

export const bluetoothScanStatusAction = createAction(
`${BLUETOOTH_PREFIX}/scan-status`,
({ status }: { status: BluetoothScanStatus }) => ({ payload: { status } }),
);

export const allBluetoothActions = {
bluetoothAdapterEventAction,
bluetoothDeviceListUpdate,
bluetoothConnectDeviceEventAction,
bluetoothScanStatusAction,
};
133 changes: 133 additions & 0 deletions suite-common/bluetooth/src/bluetoothReducerCreator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { AnyAction, Draft } from '@reduxjs/toolkit';

import { createReducerWithExtraDeps } from '@suite-common/redux-utils';
import { deviceActions } from '@suite-common/wallet-core';

import {
bluetoothAdapterEventAction,
bluetoothConnectDeviceEventAction,
bluetoothDeviceListUpdate,
bluetoothScanStatusAction,
} from './bluetoothActions';

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

export type BluetoothDevice = {
id: string;
name: string;
data: number[];
lastUpdate: number;
status: DeviceBluetoothStatus | null;
};

export type DeviceBluetoothStatus =
| { type: 'pairing'; pin?: string }
| { type: 'paired' }
| { type: 'connecting' }
| { type: 'connected' }
| {
type: 'error';
error: string;
};

export type DeviceBluetoothStatusType = DeviceBluetoothStatus['type'];

type BluetoothState<T extends BluetoothDevice> = {
isAdapterEnabled: boolean;
scanStatus: BluetoothScanStatus;

// 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[];

// This list of devices that is union of saved-devices and device that we get from scan
devices: T[];
};

export const bluetoothReducerCreator = <T extends BluetoothDevice>() => {
const initialState: BluetoothState<T> = {
isAdapterEnabled: true, // To prevent the UI from flickering when the page is loaded
scanStatus: 'running', // To prevent the UI from flickering when the page is loaded
knownDevices: [] as T[],
devices: [] as T[],
};

return createReducerWithExtraDeps<BluetoothState<T>>(initialState, (builder, extra) =>
builder
.addCase(bluetoothAdapterEventAction, (state, { payload: { isPowered } }) => {
state.isAdapterEnabled = isPowered;
if (!isPowered) {
state.devices = [];
state.scanStatus = 'done';
}
})
.addCase(bluetoothDeviceListUpdate, (state, { payload: { devices, knownDevices } }) => {
const newList = new Map<string, T>();
knownDevices.forEach(device => {
newList.set(device.id, device as T);
});

state.knownDevices = knownDevices as Draft<T>[];

devices.forEach(device => {
newList.set(device.id, device as T);
});

state.devices = Array.from(newList.values()).sort(
(a, b) => a.lastUpdate - b.lastUpdate,
) as Draft<T>[];
})
.addCase(
bluetoothConnectDeviceEventAction,
(state, { payload: { id, connectionStatus } }) => {
const device = state.devices.find(it => it.id === id);

if (device !== undefined) {
device.status = connectionStatus;
}
},
)
.addCase(bluetoothScanStatusAction, (state, { payload: { status } }) => {
state.scanStatus = status;
})
.addCase(deviceActions.deviceDisconnect, (state, { payload: { bluetoothProps } }) => {
if (bluetoothProps) {
state.devices = state.devices.filter(it => it.id !== bluetoothProps.id);
}
})
.addCase(
deviceActions.connectDevice,
(
state,
{
payload: {
device: { bluetoothProps },
},
},
) => {
if (bluetoothProps && bluetoothProps.id in state.devices) {
const deviceState = state.devices.find(it => it.id === bluetoothProps.id);

if (deviceState !== undefined) {
deviceState.status = null;

// Once device is fully connected, we save it to the list of paired devices
// so next time user opens suite
const foundPairedDevice = state.knownDevices.find(
it => it.id === bluetoothProps.id,
);
if (foundPairedDevice === undefined) {
state.knownDevices.push(deviceState);
}
}
}
},
)
.addMatcher(
action => action.type === extra.actionTypes.storageLoad,
(state, action: AnyAction) => {
state.knownDevices = action.payload.knownDevices?.bluetooth ?? [];
},
),
);
};
9 changes: 9 additions & 0 deletions suite-common/bluetooth/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": { "outDir": "libDev" },
"references": [
{ "path": "../redux-utils" },
{ "path": "../suite-types" },
{ "path": "../wallet-core" }
]
}
10 changes: 10 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10265,6 +10265,16 @@ __metadata:
languageName: unknown
linkType: soft

"@suite-common/bluetooth@workspace:suite-common/bluetooth":
version: 0.0.0-use.local
resolution: "@suite-common/bluetooth@workspace:suite-common/bluetooth"
dependencies:
"@reduxjs/toolkit": "npm:1.9.5"
"@suite-common/redux-utils": "workspace:*"
"@suite-common/wallet-core": "workspace:*"
languageName: unknown
linkType: soft

"@suite-common/connect-init@workspace:*, @suite-common/connect-init@workspace:suite-common/connect-init":
version: 0.0.0-use.local
resolution: "@suite-common/connect-init@workspace:suite-common/connect-init"
Expand Down

0 comments on commit 1348459

Please sign in to comment.