Skip to content

Commit 69fc8a7

Browse files
More progress on pairing flow
1 parent e9b95d3 commit 69fc8a7

File tree

1 file changed

+64
-17
lines changed

1 file changed

+64
-17
lines changed

packages/snaps-controllers/src/devices/DeviceController.ts

+64-17
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type {
44
ControllerStateChangeEvent,
55
} from '@metamask/base-controller';
66
import { BaseController } from '@metamask/base-controller';
7-
import { createDeferredPromise } from '@metamask/utils';
7+
import { createDeferredPromise, hasProperty } from '@metamask/utils';
88

99
const controllerName = 'DeviceController';
1010

@@ -48,17 +48,22 @@ export type DeviceControllerMessenger = RestrictedControllerMessenger<
4848
>;
4949

5050
export enum DeviceType {
51-
HID,
52-
Bluetooth,
51+
HID = 'HID',
5352
}
5453

55-
export type Device = {
54+
export type DeviceMetadata = {
5655
type: DeviceType;
56+
id: string;
57+
name: string;
58+
};
59+
60+
export type Device = DeviceMetadata & {
61+
connected: boolean;
5762
};
5863

5964
export type DeviceControllerState = {
6065
devices: Record<string, Device>;
61-
pairing?: { snapId: string };
66+
pairing: { snapId: string } | null;
6267
};
6368

6469
export type DeviceControllerArgs = {
@@ -75,7 +80,7 @@ export class DeviceController extends BaseController<
7580
> {
7681
#pairing?: {
7782
promise: Promise<unknown>;
78-
resolve: (result: unknown) => void;
83+
resolve: (result: string) => void;
7984
reject: (error: unknown) => void;
8085
};
8186

@@ -87,7 +92,7 @@ export class DeviceController extends BaseController<
8792
pairing: { persist: false, anonymous: false },
8893
},
8994
name: controllerName,
90-
state: { ...state, devices: {} },
95+
state: { ...state, devices: {}, pairing: null },
9196
});
9297

9398
this.messagingSystem.registerActionHandler(
@@ -102,22 +107,58 @@ export class DeviceController extends BaseController<
102107
}
103108

104109
async requestDevices(snapId: string) {
105-
const device = await this.#requestPairing({ snapId });
110+
const deviceId = await this.#requestPairing({ snapId });
111+
112+
await this.#syncDevices();
113+
114+
console.log('Granting access to', deviceId);
106115

107-
console.log('Paired device', device);
108-
// TODO: Persist device
109116
// TODO: Grant permission to use device
117+
118+
return null;
110119
}
111120

112121
async #hasPermission(snapId: string, device: Device) {
113122
// TODO: Verify Snap has permission to use device.
114123
return true;
115124
}
116125

126+
async #syncDevices() {
127+
const connectedDevices = await this.#getDevices();
128+
129+
this.update((draftState) => {
130+
for (const device of Object.values(draftState.devices)) {
131+
draftState.devices[device.id].connected = hasProperty(
132+
connectedDevices,
133+
device.id,
134+
);
135+
}
136+
for (const device of Object.values(connectedDevices)) {
137+
if (!hasProperty(draftState.devices, device.id)) {
138+
// @ts-expect-error Not sure why this is failing, continuing.
139+
draftState.devices[device.id] = { ...device, connected: true };
140+
}
141+
}
142+
});
143+
}
144+
117145
// Get actually connected devices
118-
async #getDevices() {
146+
async #getDevices(): Promise<Record<string, DeviceMetadata>> {
147+
const type = DeviceType.HID;
119148
// TODO: Merge multiple device implementations
120-
return (navigator as any).hid.getDevices();
149+
const devices: any[] = await (navigator as any).hid.getDevices();
150+
return devices.reduce<Record<string, DeviceMetadata>>(
151+
(accumulator, device) => {
152+
const { vendorId, productId, productName } = device;
153+
154+
const id = `${type}-${vendorId}-${productId}`;
155+
156+
accumulator[id] = { type, id, name: productName };
157+
158+
return accumulator;
159+
},
160+
{},
161+
);
121162
}
122163

123164
#isPairing() {
@@ -130,24 +171,29 @@ export class DeviceController extends BaseController<
130171
throw new Error('A pairing is already underway.');
131172
}
132173

133-
const { promise, resolve, reject } = createDeferredPromise<unknown>();
174+
const { promise, resolve, reject } = createDeferredPromise<string>();
134175

135176
this.#pairing = { promise, resolve, reject };
177+
178+
// TODO: Consider polling this call while pairing is ongoing?
179+
await this.#syncDevices();
180+
136181
this.update((draftState) => {
137182
draftState.pairing = { snapId };
138183
});
139184

140185
return promise;
141186
}
142187

143-
resolvePairing(device: unknown) {
188+
resolvePairing(deviceId: string) {
144189
if (!this.#isPairing()) {
145190
return;
146191
}
147192

148-
this.#pairing?.resolve(device);
193+
this.#pairing?.resolve(deviceId);
194+
this.#pairing = undefined;
149195
this.update((draftState) => {
150-
delete draftState.pairing;
196+
draftState.pairing = null;
151197
});
152198
}
153199

@@ -157,8 +203,9 @@ export class DeviceController extends BaseController<
157203
}
158204

159205
this.#pairing?.reject(new Error('Pairing rejected'));
206+
this.#pairing = undefined;
160207
this.update((draftState) => {
161-
delete draftState.pairing;
208+
draftState.pairing = null;
162209
});
163210
}
164211
}

0 commit comments

Comments
 (0)