@@ -4,7 +4,7 @@ import type {
4
4
ControllerStateChangeEvent ,
5
5
} from '@metamask/base-controller' ;
6
6
import { BaseController } from '@metamask/base-controller' ;
7
- import { createDeferredPromise } from '@metamask/utils' ;
7
+ import { createDeferredPromise , hasProperty } from '@metamask/utils' ;
8
8
9
9
const controllerName = 'DeviceController' ;
10
10
@@ -48,17 +48,22 @@ export type DeviceControllerMessenger = RestrictedControllerMessenger<
48
48
> ;
49
49
50
50
export enum DeviceType {
51
- HID ,
52
- Bluetooth ,
51
+ HID = 'HID' ,
53
52
}
54
53
55
- export type Device = {
54
+ export type DeviceMetadata = {
56
55
type : DeviceType ;
56
+ id : string ;
57
+ name : string ;
58
+ } ;
59
+
60
+ export type Device = DeviceMetadata & {
61
+ connected : boolean ;
57
62
} ;
58
63
59
64
export type DeviceControllerState = {
60
65
devices : Record < string , Device > ;
61
- pairing ? : { snapId : string } ;
66
+ pairing : { snapId : string } | null ;
62
67
} ;
63
68
64
69
export type DeviceControllerArgs = {
@@ -75,7 +80,7 @@ export class DeviceController extends BaseController<
75
80
> {
76
81
#pairing?: {
77
82
promise : Promise < unknown > ;
78
- resolve : ( result : unknown ) => void ;
83
+ resolve : ( result : string ) => void ;
79
84
reject : ( error : unknown ) => void ;
80
85
} ;
81
86
@@ -87,7 +92,7 @@ export class DeviceController extends BaseController<
87
92
pairing : { persist : false , anonymous : false } ,
88
93
} ,
89
94
name : controllerName ,
90
- state : { ...state , devices : { } } ,
95
+ state : { ...state , devices : { } , pairing : null } ,
91
96
} ) ;
92
97
93
98
this . messagingSystem . registerActionHandler (
@@ -102,22 +107,58 @@ export class DeviceController extends BaseController<
102
107
}
103
108
104
109
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 ) ;
106
115
107
- console . log ( 'Paired device' , device ) ;
108
- // TODO: Persist device
109
116
// TODO: Grant permission to use device
117
+
118
+ return null ;
110
119
}
111
120
112
121
async #hasPermission( snapId : string , device : Device ) {
113
122
// TODO: Verify Snap has permission to use device.
114
123
return true ;
115
124
}
116
125
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
+
117
145
// Get actually connected devices
118
- async #getDevices( ) {
146
+ async #getDevices( ) : Promise < Record < string , DeviceMetadata > > {
147
+ const type = DeviceType . HID ;
119
148
// 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
+ ) ;
121
162
}
122
163
123
164
#isPairing( ) {
@@ -130,24 +171,29 @@ export class DeviceController extends BaseController<
130
171
throw new Error ( 'A pairing is already underway.' ) ;
131
172
}
132
173
133
- const { promise, resolve, reject } = createDeferredPromise < unknown > ( ) ;
174
+ const { promise, resolve, reject } = createDeferredPromise < string > ( ) ;
134
175
135
176
this . #pairing = { promise, resolve, reject } ;
177
+
178
+ // TODO: Consider polling this call while pairing is ongoing?
179
+ await this . #syncDevices( ) ;
180
+
136
181
this . update ( ( draftState ) => {
137
182
draftState . pairing = { snapId } ;
138
183
} ) ;
139
184
140
185
return promise ;
141
186
}
142
187
143
- resolvePairing ( device : unknown ) {
188
+ resolvePairing ( deviceId : string ) {
144
189
if ( ! this . #isPairing( ) ) {
145
190
return ;
146
191
}
147
192
148
- this . #pairing?. resolve ( device ) ;
193
+ this . #pairing?. resolve ( deviceId ) ;
194
+ this . #pairing = undefined ;
149
195
this . update ( ( draftState ) => {
150
- delete draftState . pairing ;
196
+ draftState . pairing = null ;
151
197
} ) ;
152
198
}
153
199
@@ -157,8 +203,9 @@ export class DeviceController extends BaseController<
157
203
}
158
204
159
205
this . #pairing?. reject ( new Error ( 'Pairing rejected' ) ) ;
206
+ this . #pairing = undefined ;
160
207
this . update ( ( draftState ) => {
161
- delete draftState . pairing ;
208
+ draftState . pairing = null ;
162
209
} ) ;
163
210
}
164
211
}
0 commit comments