onekey-electron-demo 1.1.26-alpha.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,22 @@
1
+
2
+ # Q & A
3
+ ## The mac running message is damaged and cannot be opened. You should move it to the trash.
4
+ ### Intel Mac
5
+ 1. Open settings
6
+ 2. Security & Privacy
7
+ 3. Security
8
+ 4. Allow apps downloaded from: App Store and identified developers
9
+ 5. Open the app again
10
+
11
+ ### Apple Silicon Mac
12
+ 1. Open terminal
13
+ 2. Run the following command
14
+ ```bash
15
+ sudo /usr/bin/xattr -c /Applications/YourAppName.app
16
+ ```
17
+
18
+ ### If the above command does not work, try the following command
19
+ ```bash
20
+ sudo xattr -r -d com.apple.quarantine /Applications/YourAppName.app
21
+ ```
22
+
@@ -0,0 +1,123 @@
1
+ /* eslint-disable no-template-curly-in-string */
2
+ // eslint-disable-next-line import/no-import-module-exports, @typescript-eslint/no-var-requires
3
+ const { version } = require('./package.json');
4
+
5
+ module.exports = {
6
+ extraMetadata: {
7
+ main: 'dist/index.js',
8
+ version,
9
+ },
10
+ appId: 'so.onekey.example.hardware-desktop',
11
+ productName: 'HardwareExample',
12
+ copyright: 'Copyright © OeKey 2024',
13
+ asar: true,
14
+ // Unpack native modules so they can be loaded at runtime
15
+ asarUnpack: [
16
+ 'node_modules/@stoprocent/noble/**',
17
+ 'node_modules/@stoprocent/bluetooth-hci-socket/**',
18
+ ],
19
+ buildVersion: version,
20
+ directories: {
21
+ output: 'out',
22
+ },
23
+ files: [
24
+ 'web-build',
25
+ 'public',
26
+ 'bin',
27
+ '!public/bin/**/*',
28
+ 'dist/**/*.js',
29
+ '!dist/__**',
30
+ 'package.json',
31
+ '!scripts/**',
32
+ ],
33
+ extraResources: [
34
+ {
35
+ from: 'public/icons/512x512.png',
36
+ to: 'icons/512x512.png',
37
+ },
38
+ ],
39
+ dmg: {
40
+ sign: false,
41
+ contents: [
42
+ {
43
+ x: 410,
44
+ y: 175,
45
+ type: 'link',
46
+ path: '/Applications',
47
+ },
48
+ {
49
+ x: 130,
50
+ y: 175,
51
+ type: 'file',
52
+ },
53
+ ],
54
+ background: 'public/icons/background.png',
55
+ },
56
+ nsis: {
57
+ oneClick: false,
58
+ installerSidebar: 'public/icons/installerSidebar.bmp',
59
+ },
60
+ mac: {
61
+ // skip code signing
62
+ identity: null,
63
+ extraResources: [
64
+ {
65
+ from: 'public/bin/bridge/mac-${arch}',
66
+ to: 'bin/bridge',
67
+ },
68
+ ],
69
+ icon: 'public/icons/512x512.png',
70
+ artifactName: 'Hardware-Example-mac-${arch}.${ext}',
71
+ hardenedRuntime: true,
72
+ gatekeeperAssess: false,
73
+ darkModeSupport: false,
74
+ category: 'productivity',
75
+ target: [
76
+ { target: 'dmg', arch: ['x64', 'arm64'] },
77
+ // { target: 'zip', arch: ['x64', 'arm64'] },
78
+ ],
79
+ entitlements: 'entitlements.mac.plist',
80
+ extendInfo: {
81
+ NSCameraUsageDescription: 'Please allow OneKey to use your camera',
82
+ },
83
+ },
84
+ win: {
85
+ extraResources: [
86
+ {
87
+ from: 'public/bin/bridge/win-${arch}',
88
+ to: 'bin/bridge',
89
+ },
90
+ ],
91
+ icon: 'public/icons/512x512.png',
92
+ artifactName: 'Hardware-Example-win-${arch}.${ext}',
93
+ verifyUpdateCodeSignature: false,
94
+ target: [
95
+ {
96
+ target: 'nsis',
97
+ arch: ['x64'],
98
+ },
99
+ ],
100
+ },
101
+ linux: {
102
+ extraResources: [
103
+ {
104
+ from: 'public/bin/bridge/linux-${arch}',
105
+ to: 'bin/bridge',
106
+ },
107
+ ],
108
+ icon: 'public/icons/512x512.png',
109
+ artifactName: 'Hardware-Example-linux-${arch}.${ext}',
110
+ executableName: 'onekey-hardware-example',
111
+ category: 'Utility',
112
+ target: ['AppImage'],
113
+ },
114
+ publish: [
115
+ {
116
+ provider: 'github',
117
+ owner: 'OneKeyHQ',
118
+ repo: 'hardware-js-sdk',
119
+ private: false,
120
+ vPrefixedTagName: true,
121
+ },
122
+ ],
123
+ };
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>com.apple.security.cs.allow-jit</key>
6
+ <true/>
7
+ <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
8
+ <true/>
9
+ </dict>
10
+ </plist>
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "onekey-electron-demo",
3
+ "productName": "OneKey Electron Demo",
4
+ "executableName": "onekey-electron-demo",
5
+ "version": "1.1.26-alpha.30",
6
+ "author": "OneKey",
7
+ "description": "End-to-end encrypted workspaces for teams",
8
+ "main": "dist/index.js",
9
+ "license": "GPL-3.0-only",
10
+ "scripts": {
11
+ "copy:inject": "node scripts/copy-injected.js",
12
+ "clean:build": "rimraf out",
13
+ "dev-electron-web": "cross-env NODE_ENV=development yarn copy:inject && yarn build:main && cd ../expo-example && yarn dev:electron-web",
14
+ "build-electron-web": "yarn copy:inject && yarn build:main && cd ../expo-example && yarn build:electron-web",
15
+ "dev": "npx concurrently \"yarn dev:electron\" \"cross-env TRANSFORM_REGENERATOR_DISABLED=true BROWSER=none yarn dev-electron-web\"",
16
+ "dev:electron": "electron --inspect=5858 dist/index.js",
17
+ "build:main": "webpack --config webpack.config.ts",
18
+ "rebuild:deps": "electron-builder install-app-deps",
19
+ "make:mac": "yarn rebuild:deps && yarn clean:build && yarn build-electron-web && electron-builder build --mac --config electron-builder.config.js --publish always",
20
+ "make:win": "yarn rebuild:deps && yarn clean:build && yarn build-electron-web && electron-builder build --win --config electron-builder.config.js --publish always",
21
+ "lint": "eslint --ext .tsx --ext .ts ./",
22
+ "ts:check": "yarn tsc --noEmit"
23
+ },
24
+ "dependencies": {
25
+ "@onekeyfe/hd-transport-electron": "1.1.26-alpha.30",
26
+ "@stoprocent/noble": "2.3.16",
27
+ "debug": "4.3.4",
28
+ "electron-is-dev": "^3.0.1",
29
+ "electron-log": "^5.1.5",
30
+ "electron-updater": "^6.2.1",
31
+ "fs-extra": "^11.2.0",
32
+ "node-fetch": "^2.6.7"
33
+ },
34
+ "devDependencies": {
35
+ "@types/webpack": "^5.28.5",
36
+ "@types/webpack-node-externals": "^3.0.4",
37
+ "clean-webpack-plugin": "^4.0.0",
38
+ "cross-env": "^7.0.3",
39
+ "electron": "^40.1.0",
40
+ "electron-builder": "^24.9.1",
41
+ "webpack": "^5.90.2",
42
+ "webpack-node-externals": "^3.0.0"
43
+ },
44
+ "resolutions": {
45
+ "**/node-gyp": "^10.0.1",
46
+ "tmp": "0.2.4"
47
+ },
48
+ "gitHead": "88ec305cb60347e09cf9e0c8e921d414f7fe3656"
49
+ }
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,38 @@
1
+ /* eslint-disable @typescript-eslint/no-var-requires */
2
+ const fs = require('fs-extra');
3
+ const path = require('path');
4
+
5
+ const rootDir = path.join(__dirname, '../../../../');
6
+
7
+ const sourceDir = path.join(rootDir, 'node_modules/@onekeyfe/hd-web-sdk/build/');
8
+ const targetDir = path.join(rootDir, 'packages/connect-examples/electron-example/public/js-sdk/');
9
+
10
+ async function copyFiles() {
11
+ try {
12
+ await fs.remove(targetDir);
13
+ await fs.ensureDir(targetDir);
14
+ console.log(`Target directory ${targetDir} is ensured`);
15
+
16
+ await fs.copy(sourceDir, targetDir, {
17
+ recursive: true,
18
+ overwrite: true,
19
+ filter: (src, dest) => {
20
+ // Don't copy onekey-js-sdk
21
+ if (src.endsWith('onekey-js-sdk.min.js') || src.endsWith('onekey-js-sdk.js')) {
22
+ return false;
23
+ }
24
+
25
+ // Don't copy source maps
26
+ if (src.endsWith('.map')) {
27
+ return false;
28
+ }
29
+ return true;
30
+ },
31
+ });
32
+ console.log(`Files copied from ${sourceDir} to ${targetDir}`);
33
+ } catch (error) {
34
+ console.error('Error copying files:', error);
35
+ }
36
+ }
37
+
38
+ copyFiles();
package/src/config.ts ADDED
@@ -0,0 +1,12 @@
1
+ export const ipcMessageKeys = {
2
+ // Updater
3
+ UPDATE_AVAILABLE: 'update/available',
4
+ UPDATE_DOWNLOADED: 'update/downloaded',
5
+ UPDATE_RESTART: 'update/restartApp',
6
+
7
+ APP_RELOAD_BRIDGE_PROCESS: 'app/reloadBridgeProcess',
8
+
9
+ INJECT_ONEKEY_DESKTOP_GLOBALS: 'inject/onekeyDesktop',
10
+
11
+ APP_RESTART: 'app/restart',
12
+ };
package/src/index.ts ADDED
@@ -0,0 +1,400 @@
1
+ import { BrowserWindow, app, ipcMain, screen, session, shell } from 'electron';
2
+ import path from 'path';
3
+ import isDevelopment from 'electron-is-dev';
4
+ import { format as formatUrl } from 'url';
5
+ import log from 'electron-log';
6
+ import { autoUpdater } from 'electron-updater';
7
+ import { exec } from 'child_process';
8
+ import { initNobleBleSupport } from '@onekeyfe/hd-transport-electron';
9
+
10
+ import initProcess, { restartBridge } from './process';
11
+ import { ipcMessageKeys } from './config';
12
+
13
+ // Set log level
14
+ log.transports.file.level = 'info';
15
+ log.transports.console.level = 'info';
16
+ autoUpdater.logger = log;
17
+
18
+ const isMac = process.platform === 'darwin';
19
+ const isWin = process.platform === 'win32';
20
+
21
+ const APP_NAME = 'OneKey Electron Demo';
22
+ app.name = APP_NAME;
23
+ let mainWindow: BrowserWindow | null;
24
+
25
+ let isAppReady = false;
26
+
27
+ (global as any).resourcesPath = isDevelopment
28
+ ? path.join(__dirname, '../public')
29
+ : path.join(process.resourcesPath, 'app');
30
+ const staticPath = isDevelopment
31
+ ? path.join(__dirname, '../public')
32
+ : path.join((global as any).resourcesPath, 'public');
33
+
34
+ const sdkConnectSrc = isDevelopment
35
+ ? `file://${path.join(staticPath, 'js-sdk/')}`
36
+ : path.join('public', 'js-sdk/');
37
+
38
+ function initChildProcess() {
39
+ return initProcess({ isDevelopment });
40
+ }
41
+
42
+ function showMainWindow() {
43
+ if (!mainWindow) {
44
+ return;
45
+ }
46
+ mainWindow.show();
47
+ mainWindow.focus();
48
+ }
49
+
50
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
51
+ function quitOrMinimizeApp(event?: Event) {
52
+ // On OS X it is common for applications and their menu bar
53
+ // to stay active until the user quits explicitly with Cmd + Q
54
+ if (isMac) {
55
+ // **** renderer app will reload after minimize, and keytar not working.
56
+ event?.preventDefault();
57
+ if (!mainWindow?.isDestroyed()) {
58
+ mainWindow?.hide();
59
+ }
60
+ // ****
61
+ // app.quit();
62
+ } else {
63
+ app.quit();
64
+ }
65
+ }
66
+
67
+ function createMainWindow() {
68
+ const display = screen.getPrimaryDisplay();
69
+ const dimensions = display.workAreaSize;
70
+ const ratio = 16 / 9;
71
+
72
+ const browserWindow = new BrowserWindow({
73
+ title: APP_NAME,
74
+ titleBarStyle: isWin ? 'default' : 'hidden',
75
+ trafficLightPosition: { x: 20, y: 12 },
76
+ autoHideMenuBar: true,
77
+ frame: true,
78
+ resizable: true,
79
+ width: Math.min(1920, dimensions.width),
80
+ height: Math.min(1920 / ratio, dimensions.height),
81
+ webPreferences: {
82
+ spellcheck: false,
83
+ webviewTag: true,
84
+ webSecurity: !isDevelopment,
85
+ // @ts-expect-error
86
+ nativeWindowOpen: true,
87
+ allowRunningInsecureContent: isDevelopment,
88
+ // webview injected js needs isolation=false, because property can not be exposeInMainWorld() when isolation enabled.
89
+ contextIsolation: false,
90
+ preload: path.join(__dirname, 'preload.js'),
91
+ sandbox: false,
92
+ },
93
+ });
94
+
95
+ if (isDevelopment) {
96
+ browserWindow.webContents.openDevTools();
97
+ }
98
+
99
+ browserWindow.webContents.on('did-finish-load', () => {
100
+ console.log('browserWindow >>>> did-finish-load');
101
+ browserWindow.webContents.send(ipcMessageKeys.INJECT_ONEKEY_DESKTOP_GLOBALS, {
102
+ resourcesPath: (global as any).resourcesPath,
103
+ staticPath: `file://${staticPath}`,
104
+ sdkConnectSrc,
105
+ });
106
+ });
107
+
108
+ const src = isDevelopment
109
+ ? 'http://localhost:19006/'
110
+ : formatUrl({
111
+ pathname: 'index.html',
112
+ protocol: 'file',
113
+ slashes: true,
114
+ });
115
+
116
+ browserWindow.loadURL(src);
117
+
118
+ browserWindow.on('closed', () => {
119
+ mainWindow = null;
120
+ isAppReady = false;
121
+ console.log('set isAppReady on browserWindow closed', isAppReady);
122
+ });
123
+
124
+ browserWindow.webContents.on('devtools-opened', () => {
125
+ browserWindow.focus();
126
+ setImmediate(() => {
127
+ browserWindow.focus();
128
+ });
129
+ });
130
+
131
+ // dom-ready is fired after ipcMain:app/ready
132
+ browserWindow.webContents.on('dom-ready', () => {
133
+ isAppReady = true;
134
+ console.log('set isAppReady on browserWindow dom-ready', isAppReady);
135
+ });
136
+
137
+ const filter = {
138
+ urls: ['http://127.0.0.1:21320/*', 'http://localhost:21320/*'],
139
+ };
140
+
141
+ session.defaultSession.webRequest.onBeforeSendHeaders(filter, (details, callback) => {
142
+ const { url } = details;
143
+ if (url.startsWith('http://127.0.0.1:21320/') || url.startsWith('http://localhost:21320/')) {
144
+ // resolve onekey bridge CORS error
145
+ details.requestHeaders.Origin = 'https://jssdk.onekey.so';
146
+ }
147
+
148
+ callback({ cancel: false, requestHeaders: details.requestHeaders });
149
+ });
150
+
151
+ // 记录已授权的设备
152
+ let grantedDeviceThroughPermHandler = null;
153
+
154
+ browserWindow.webContents.session.setPermissionCheckHandler(
155
+ (webContents, permission, requestingOrigin, details) => {
156
+ log.debug('WebUSB: 权限检查被调用:', {
157
+ permission,
158
+ requestingOrigin,
159
+ details: JSON.stringify(details, null, 2),
160
+ });
161
+
162
+ // 允许所有 USB 权限请求
163
+ if (permission === 'usb') {
164
+ return true;
165
+ }
166
+ return false;
167
+ }
168
+ );
169
+
170
+ browserWindow.webContents.session.setDevicePermissionHandler(details => {
171
+ log.debug('WebUSB: 设备权限请求被调用:', {
172
+ deviceType: details.deviceType,
173
+ origin: details.origin,
174
+ device: JSON.stringify(details, null, 2),
175
+ });
176
+
177
+ // 允许所有 USB 设备请求
178
+ if (details.deviceType === 'usb') {
179
+ log.debug('WebUSB: 记录已授权的设备');
180
+ grantedDeviceThroughPermHandler = details.device;
181
+ return true;
182
+ }
183
+ return false;
184
+ });
185
+
186
+ browserWindow.webContents.session.setUSBProtectedClassesHandler(details =>
187
+ details.protectedClasses.filter(
188
+ usbClass =>
189
+ // Exclude classes except for audio classes
190
+ usbClass.indexOf('audio') === -1
191
+ )
192
+ );
193
+
194
+ // 添加设备选择处理程序
195
+ browserWindow.webContents.session.on('select-usb-device', (event, details, callback) => {
196
+ log.debug('WebUSB: select-usb-device 事件触发');
197
+ log.debug('WebUSB: 可用设备列表:', JSON.stringify(details.deviceList, null, 2));
198
+
199
+ // 阻止默认行为,以便我们可以自动选择设备
200
+ // 这是 Electron 的优势:不像浏览器必须显示弹窗,桌面应用可以自动化处理
201
+ event.preventDefault();
202
+
203
+ // 直接选择第一个设备
204
+ if (details.deviceList && details.deviceList.length > 0) {
205
+ console.debug(`WebUSB: 选择了第一个设备:`, JSON.stringify(details.deviceList[0], null, 2));
206
+ callback(details.deviceList[0].deviceId);
207
+ } else {
208
+ console.debug('WebUSB: 没有设备可选择,返回空');
209
+ callback();
210
+ }
211
+ });
212
+
213
+ if (!isDevelopment) {
214
+ const PROTOCOL = 'file';
215
+ session.defaultSession.protocol.interceptFileProtocol(PROTOCOL, (request, callback) => {
216
+ const isJsSdkFile = request.url.indexOf('/public/js-sdk') > -1;
217
+ const isIFrameHtml = request.url.indexOf('/public/js-sdk/iframe.html') > -1;
218
+
219
+ // resolve iframe path
220
+ if (isJsSdkFile && isIFrameHtml) {
221
+ callback({
222
+ path: path.join(__dirname, '..', 'public', 'js-sdk', 'iframe.html'),
223
+ });
224
+ return;
225
+ }
226
+
227
+ // resolve jssdk path
228
+ if (isJsSdkFile) {
229
+ const url = request.url.substr(PROTOCOL.length + 1);
230
+ callback(path.join(__dirname, '..', url));
231
+ return;
232
+ }
233
+
234
+ // resolve main app path
235
+ let url = request.url.substr(PROTOCOL.length + 1);
236
+ url = path.join(__dirname, '..', 'web-build', url);
237
+ callback(url);
238
+ });
239
+
240
+ // eslint-disable-next-line @typescript-eslint/naming-convention
241
+ browserWindow.webContents.on('did-fail-load', (_, __, ___, validatedURL) => {
242
+ const redirectPath = validatedURL.replace(`${PROTOCOL}://`, '');
243
+ if (validatedURL.startsWith(PROTOCOL) && !redirectPath.includes('.')) {
244
+ browserWindow.loadURL(src);
245
+ }
246
+ });
247
+ }
248
+
249
+ // @ts-expect-error
250
+ browserWindow.on('close', (event: Event) => {
251
+ // hide() instead of close() on MAC
252
+ if (isMac) {
253
+ event.preventDefault();
254
+ if (!browserWindow.isDestroyed()) {
255
+ browserWindow.blur();
256
+ browserWindow.hide(); // hide window only
257
+ // browserWindow.minimize(); // hide window and minimize to Docker
258
+ }
259
+ }
260
+ });
261
+
262
+ ipcMain.on(ipcMessageKeys.APP_RESTART, () => {
263
+ browserWindow?.reload();
264
+ });
265
+
266
+ return browserWindow;
267
+ }
268
+
269
+ const singleInstance = app.requestSingleInstanceLock();
270
+
271
+ if (!singleInstance && !process.mas) {
272
+ quitOrMinimizeApp();
273
+ } else {
274
+ app.on('second-instance', (e, argv) => {
275
+ if (mainWindow) {
276
+ if (mainWindow.isMinimized()) mainWindow.restore();
277
+ showMainWindow();
278
+ }
279
+ });
280
+
281
+ app.name = APP_NAME;
282
+ app.on('ready', () => {
283
+ if (!mainWindow) {
284
+ mainWindow = createMainWindow();
285
+ }
286
+
287
+ try {
288
+ log.info('Initializing Noble BLE support...');
289
+ initNobleBleSupport(mainWindow.webContents);
290
+ log.info('Noble BLE support initialized successfully.');
291
+ } catch (e) {
292
+ log.error('Failed to initialize Noble BLE support:', e);
293
+ }
294
+
295
+ initChildProcess();
296
+ showMainWindow();
297
+ console.log('日志文件位置:', log.transports.file.getFile().path);
298
+ });
299
+ }
300
+
301
+ ipcMain.on(ipcMessageKeys.UPDATE_RESTART, () => {
302
+ log.info('App Quit And Install');
303
+ autoUpdater.quitAndInstall();
304
+ });
305
+
306
+ ipcMain.on(ipcMessageKeys.APP_RELOAD_BRIDGE_PROCESS, () => {
307
+ restartBridge();
308
+ });
309
+
310
+ // Simplified Bluetooth System API Implementation
311
+ class BluetoothSystemManager {
312
+ openBluetoothSettings(): void {
313
+ try {
314
+ if (process.platform === 'darwin') {
315
+ exec('open "/System/Library/PreferencePanes/Bluetooth.prefPane"');
316
+ } else if (process.platform === 'win32') {
317
+ shell.openExternal('ms-settings:bluetooth');
318
+ } else {
319
+ log.warn('Opening Bluetooth settings not supported on this platform');
320
+ }
321
+ } catch (error) {
322
+ log.error('Failed to open Bluetooth settings:', error);
323
+ }
324
+ }
325
+
326
+ openPrivacySettings(): void {
327
+ try {
328
+ if (process.platform === 'darwin') {
329
+ exec('open "x-apple.systempreferences:com.apple.preference.security?Privacy_Bluetooth"');
330
+ } else if (process.platform === 'win32') {
331
+ shell.openExternal('ms-settings:privacy-bluetooth');
332
+ } else {
333
+ log.warn('Opening privacy settings not supported on this platform');
334
+ }
335
+ } catch (error) {
336
+ log.error('Failed to open privacy settings:', error);
337
+ }
338
+ }
339
+ }
340
+
341
+ // Create global instance
342
+ const bluetoothManager = new BluetoothSystemManager();
343
+
344
+ // Register simplified IPC handlers for Bluetooth system API
345
+ ipcMain.handle('bluetooth-open-bluetooth-settings', () => {
346
+ bluetoothManager.openBluetoothSettings();
347
+ });
348
+
349
+ ipcMain.handle('bluetooth-open-privacy-settings', () => {
350
+ bluetoothManager.openPrivacySettings();
351
+ });
352
+
353
+ // 配置 GitHub 发布提供者
354
+ autoUpdater.setFeedURL({
355
+ provider: 'github',
356
+ owner: 'OneKeyHQ',
357
+ repo: 'hardware-js-sdk',
358
+ private: false,
359
+ releaseType: 'release',
360
+ });
361
+
362
+ // 检查更新
363
+ app.on('ready', () => {
364
+ autoUpdater.on('update-available', () => {
365
+ log.info('Update available.');
366
+ mainWindow?.webContents?.send(ipcMessageKeys.UPDATE_AVAILABLE);
367
+ });
368
+
369
+ autoUpdater.on('update-downloaded', () => {
370
+ log.info('Update downloaded.');
371
+ mainWindow?.webContents?.send(ipcMessageKeys.UPDATE_DOWNLOADED);
372
+ });
373
+
374
+ setTimeout(() => {
375
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
376
+ autoUpdater.checkForUpdatesAndNotify();
377
+ }, 5000);
378
+ });
379
+
380
+ // quit when all windows are closed, except on macOS. There, it's common
381
+ // for applications and their menu bar to stay active until the user quits
382
+ // explicitly with Cmd + Q
383
+ app.on('window-all-closed', (event: Event) => {
384
+ quitOrMinimizeApp(event);
385
+ });
386
+
387
+ app.on('activate', () => {
388
+ if (!mainWindow) {
389
+ mainWindow = createMainWindow();
390
+ }
391
+ showMainWindow();
392
+ });
393
+
394
+ app.on('before-quit', () => {
395
+ if (mainWindow) {
396
+ mainWindow?.removeAllListeners();
397
+ mainWindow?.removeAllListeners('close');
398
+ mainWindow?.close();
399
+ }
400
+ });
package/src/preload.ts ADDED
@@ -0,0 +1,133 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-return */
2
+ /* eslint-disable @typescript-eslint/no-unused-vars,@typescript-eslint/require-await */
3
+ import { contextBridge, ipcRenderer } from 'electron';
4
+ import { EOneKeyBleMessageKeys } from '@onekeyfe/hd-shared';
5
+
6
+ import { ipcMessageKeys } from './config';
7
+
8
+ import type { DesktopAPI as BaseDesktopAPI, NobleBleAPI } from '@onekeyfe/hd-transport-electron';
9
+
10
+ // Simplified Bluetooth system API - only for opening settings
11
+ export interface BluetoothSystemAPI {
12
+ // System integration
13
+ openBluetoothSettings: () => void;
14
+ openPrivacySettings: () => void;
15
+ }
16
+
17
+ // Extend the base DesktopAPI with this specific application's needs
18
+ export interface DesktopAPI extends BaseDesktopAPI {
19
+ restart: () => void;
20
+ reloadBridgeProcess: () => void;
21
+
22
+ // Generic IPC methods
23
+ invoke: (channel: string, ...args: any[]) => Promise<any>;
24
+ on: (channel: string, callback: (...args: any[]) => void) => () => void;
25
+ off?: (channel: string, callback?: (...args: any[]) => void) => void;
26
+
27
+ // Make nobleBle required for this app
28
+ nobleBle: NobleBleAPI;
29
+
30
+ // Simplified Bluetooth system management
31
+ bluetoothSystem: BluetoothSystemAPI;
32
+ }
33
+
34
+ declare global {
35
+ interface Window {
36
+ desktopApi: DesktopAPI;
37
+ INJECT_PATH: string;
38
+ }
39
+ }
40
+
41
+ const validChannels = [
42
+ // Update events
43
+ ipcMessageKeys.UPDATE_AVAILABLE,
44
+ ipcMessageKeys.UPDATE_DOWNLOADED,
45
+ ];
46
+
47
+ ipcRenderer.on(ipcMessageKeys.INJECT_ONEKEY_DESKTOP_GLOBALS, (_, globals) => {
48
+ try {
49
+ contextBridge.exposeInMainWorld('ONEKEY_DESKTOP_GLOBALS', globals);
50
+ } catch (error) {
51
+ // @ts-expect-error
52
+ window.ONEKEY_DESKTOP_GLOBALS = globals;
53
+ // Fallback for development or when contextBridge is not available
54
+ console.warn('Failed to expose ONEKEY_DESKTOP_GLOBALS via contextBridge:', error);
55
+ }
56
+ });
57
+
58
+ const desktopApi = {
59
+ // Generic IPC methods
60
+ invoke: (channel: string, ...args: any[]) => ipcRenderer.invoke(channel, ...args),
61
+ on: (channel: string, func: (...args: any[]) => any) => {
62
+ if (validChannels.includes(channel)) {
63
+ ipcRenderer.on(channel, (_, ...args) => func(...args));
64
+ }
65
+ // For other channels, set up listener and return cleanup function
66
+ const listener = (_: any, ...args: any[]) => func(...args);
67
+ ipcRenderer.on(channel, listener);
68
+ return () => {
69
+ ipcRenderer.removeListener(channel, listener);
70
+ };
71
+ },
72
+ restart: () => {
73
+ ipcRenderer.send(ipcMessageKeys.APP_RESTART);
74
+ },
75
+ updateReload: () => {
76
+ ipcRenderer.send(ipcMessageKeys.UPDATE_RESTART);
77
+ },
78
+ reloadBridgeProcess: () => {
79
+ ipcRenderer.send(ipcMessageKeys.APP_RELOAD_BRIDGE_PROCESS);
80
+ },
81
+
82
+ // Noble BLE specific methods
83
+ nobleBle: {
84
+ enumerate: () => ipcRenderer.invoke(EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE),
85
+ getDevice: (uuid: string) =>
86
+ ipcRenderer.invoke(EOneKeyBleMessageKeys.NOBLE_BLE_GET_DEVICE, uuid),
87
+ connect: (uuid: string) => ipcRenderer.invoke(EOneKeyBleMessageKeys.NOBLE_BLE_CONNECT, uuid),
88
+ disconnect: (uuid: string) =>
89
+ ipcRenderer.invoke(EOneKeyBleMessageKeys.NOBLE_BLE_DISCONNECT, uuid),
90
+ subscribe: (uuid: string) =>
91
+ ipcRenderer.invoke(EOneKeyBleMessageKeys.NOBLE_BLE_SUBSCRIBE, uuid),
92
+ unsubscribe: (uuid: string) =>
93
+ ipcRenderer.invoke(EOneKeyBleMessageKeys.NOBLE_BLE_UNSUBSCRIBE, uuid),
94
+ write: (uuid: string, data: string) =>
95
+ ipcRenderer.invoke(EOneKeyBleMessageKeys.NOBLE_BLE_WRITE, uuid, data),
96
+ onNotification: (callback: (deviceId: string, data: string) => void) => {
97
+ const subscription = (_: unknown, deviceId: string, data: string) => {
98
+ callback(deviceId, data);
99
+ };
100
+ ipcRenderer.on(EOneKeyBleMessageKeys.NOBLE_BLE_NOTIFICATION, subscription);
101
+ return () => {
102
+ ipcRenderer.removeListener(EOneKeyBleMessageKeys.NOBLE_BLE_NOTIFICATION, subscription);
103
+ };
104
+ },
105
+ onDeviceDisconnected: (callback: (device: { id: string; name: string }) => void) => {
106
+ const subscription = (_: unknown, device: { id: string; name: string }) => {
107
+ callback(device);
108
+ };
109
+ ipcRenderer.on(EOneKeyBleMessageKeys.BLE_DEVICE_DISCONNECTED, subscription);
110
+ return () => {
111
+ ipcRenderer.removeListener(EOneKeyBleMessageKeys.BLE_DEVICE_DISCONNECTED, subscription);
112
+ };
113
+ },
114
+ checkAvailability: () => ipcRenderer.invoke(EOneKeyBleMessageKeys.BLE_AVAILABILITY_CHECK),
115
+ },
116
+
117
+ // Simplified Bluetooth system management
118
+ bluetoothSystem: {
119
+ // Open Bluetooth settings when Bluetooth is off
120
+ openBluetoothSettings: () => ipcRenderer.invoke('bluetooth-open-bluetooth-settings'),
121
+ // Open Privacy & Security settings for Bluetooth permission
122
+ openPrivacySettings: () => ipcRenderer.invoke('bluetooth-open-privacy-settings'),
123
+ },
124
+ };
125
+
126
+ // Use contextBridge to safely expose the API
127
+ try {
128
+ contextBridge.exposeInMainWorld('desktopApi', desktopApi);
129
+ } catch (error) {
130
+ // Fallback for development or when contextBridge is not available
131
+ console.warn('Failed to expose desktopApi via contextBridge:', error);
132
+ (window as any).desktopApi = desktopApi;
133
+ }
@@ -0,0 +1,212 @@
1
+ import { spawn } from 'child_process';
2
+ import electron from 'electron';
3
+ import path from 'path';
4
+
5
+ import type { ChildProcess } from 'child_process';
6
+
7
+ export type Status = {
8
+ service: boolean;
9
+ process: boolean;
10
+ };
11
+
12
+ export type Options = {
13
+ startupThrottleTime?: number;
14
+ stopWaitTimes?: number;
15
+ autoRestart?: number;
16
+ };
17
+
18
+ const defaultOptions: Options = {
19
+ startupThrottleTime: 0,
20
+ stopWaitTimes: 10,
21
+ autoRestart: 2,
22
+ } as const;
23
+
24
+ export default abstract class BaseProcess {
25
+ process: ChildProcess | null;
26
+
27
+ resource: string;
28
+
29
+ processName: string;
30
+
31
+ options: Options;
32
+
33
+ launchThrottle: ReturnType<typeof setTimeout> | null;
34
+
35
+ supportedSystems = ['mac-x64', 'win-x64', 'linux-arm64', 'linux-x64'];
36
+
37
+ stopped = false;
38
+
39
+ constructor(resource: string, processName: string, options: Options = defaultOptions) {
40
+ this.process = null;
41
+ this.launchThrottle = null;
42
+ this.resource = resource;
43
+ this.processName = processName;
44
+ this.options = {
45
+ ...defaultOptions,
46
+ ...options,
47
+ };
48
+
49
+ const { system } = this.getPlatformInfo();
50
+ if (!this.isSystemSupported(system)) {
51
+ console.error('Unsupported system:', system);
52
+ }
53
+ }
54
+
55
+ abstract getStatus(): Promise<{ service: boolean; process: boolean }>;
56
+
57
+ async start(params: string[] = [], isDev = false) {
58
+ const { system, ext } = this.getPlatformInfo();
59
+ if (this.launchThrottle) {
60
+ console.debug('Throttling launch, cancel process');
61
+ return;
62
+ }
63
+
64
+ const status = await this.getStatus();
65
+
66
+ if (status.service) {
67
+ console.debug('Service already running');
68
+ return;
69
+ }
70
+
71
+ if (status.process) {
72
+ console.debug('Process already running, service not');
73
+ return;
74
+ }
75
+
76
+ if (this.options.startupThrottleTime && this.options.startupThrottleTime > 0) {
77
+ console.debug('Throttling startup');
78
+
79
+ this.launchThrottle = setTimeout(() => {
80
+ console.debug('Cleaning up launch throttle');
81
+ this.launchThrottle = null;
82
+ }, this.options.startupThrottleTime * 1000);
83
+ }
84
+
85
+ this.stopped = false;
86
+
87
+ const appPath = electron.app.getAppPath();
88
+ let processDir;
89
+ if (isDev) {
90
+ processDir = path.resolve(appPath, '../', 'public', 'bin', this.resource, system);
91
+ } else {
92
+ processDir = path.resolve(appPath, '../', 'bin', this.resource);
93
+ }
94
+
95
+ const processPath = path.join(processDir, `${this.processName}${ext}`);
96
+ const processEnv = { ...process.env };
97
+ // library search path for macOS
98
+ processEnv.DYLD_LIBRARY_PATH = processEnv.DYLD_LIBRARY_PATH
99
+ ? `${processEnv.DYLD_LIBRARY_PATH}:${processDir}`
100
+ : `${processDir}`;
101
+ // library search path for Linux
102
+ processEnv.LD_LIBRARY_PATH = processEnv.LD_LIBRARY_PATH
103
+ ? `${processEnv.LD_LIBRARY_PATH}:${processDir}`
104
+ : `${processDir}`;
105
+
106
+ console.info([
107
+ 'Starting process:',
108
+ `- Path: ${processPath}`,
109
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
110
+ `- Params: ${params}`,
111
+ `- CWD: ${processDir}`,
112
+ ]);
113
+ this.process = spawn(processPath, params, {
114
+ cwd: processDir,
115
+ env: processEnv,
116
+ stdio: ['ignore', 'ignore', 'ignore'],
117
+ });
118
+ this.process.on('error', err => this.onError(err));
119
+ this.process.on('exit', code => this.onExit(code));
120
+ }
121
+
122
+ /**
123
+ * Stops the process
124
+ */
125
+ stop() {
126
+ return new Promise<void>(resolve => {
127
+ this.stopped = true;
128
+
129
+ if (!this.process) {
130
+ console.warn('process already stopped');
131
+ resolve();
132
+ return;
133
+ }
134
+
135
+ console.debug('Stopping process');
136
+ this.process.kill();
137
+
138
+ let timeout = 0;
139
+ const interval = setInterval(() => {
140
+ if (!this.process || this.process.killed) {
141
+ console.info('Process killed successfully');
142
+ clearInterval(interval);
143
+ this.process = null;
144
+ resolve();
145
+ return;
146
+ }
147
+
148
+ if (this.options.stopWaitTimes && timeout < this.options.stopWaitTimes) {
149
+ console.info('Process Still alive, checking again...');
150
+ timeout += 1;
151
+ } else {
152
+ console.info('Process Still alive, going for the SIGKILL');
153
+ this.process.kill('SIGKILL');
154
+ }
155
+ }, 1000);
156
+ });
157
+ }
158
+
159
+ async restart() {
160
+ console.info('Restarting');
161
+ await this.stop();
162
+ await this.start();
163
+ }
164
+
165
+ onError(err: Error) {
166
+ console.error('Process exit error: ', err.message);
167
+ }
168
+
169
+ onExit(code: number | null) {
170
+ console.info(
171
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
172
+ `Exited, code: ${code ?? 'unknown'} (Stopped: ${this.stopped})`
173
+ );
174
+ this.process = null;
175
+
176
+ if (this.options.autoRestart && this.options.autoRestart > 0 && !this.stopped) {
177
+ console.debug('restarting...');
178
+ let restartDelay = this.options.autoRestart;
179
+
180
+ // Add throttle delay to prevent the process from never restarting if the throttle is hit
181
+ if (this.launchThrottle && this.options.startupThrottleTime) {
182
+ restartDelay += this.options.startupThrottleTime;
183
+ }
184
+
185
+ setTimeout(() => this.start(), restartDelay * 1000);
186
+ }
187
+ }
188
+
189
+ isSystemSupported(system: string) {
190
+ return this.supportedSystems.includes(system);
191
+ }
192
+
193
+ getPlatformInfo() {
194
+ const { arch } = process;
195
+ const platform = this.getPlatform();
196
+ const ext = platform === 'win' ? '.exe' : '';
197
+ const system = `${platform}-${arch}`;
198
+ console.debug('arch: ', arch);
199
+ return { system, platform, arch, ext };
200
+ }
201
+
202
+ getPlatform() {
203
+ switch (process.platform) {
204
+ case 'darwin':
205
+ return 'mac';
206
+ case 'win32':
207
+ return 'win';
208
+ default:
209
+ return process.platform;
210
+ }
211
+ }
212
+ }
@@ -0,0 +1,93 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
2
+ import fetch from 'node-fetch';
3
+
4
+ import BaseProcess from './BaseProcess';
5
+
6
+ import type { Status } from './BaseProcess';
7
+
8
+ class BridgeProcess extends BaseProcess {
9
+ constructor() {
10
+ super('bridge', 'onekeyd', {
11
+ startupThrottleTime: 3,
12
+ });
13
+ console.info('logger file name =====> :');
14
+ }
15
+
16
+ async getStatus(): Promise<Status> {
17
+ try {
18
+ const resp = await fetch(`http://127.0.0.1:21320/`, {
19
+ method: 'POST',
20
+ headers: {
21
+ Origin: 'https://electron.onekey.so',
22
+ },
23
+ });
24
+ console.debug(`Checking status (${resp.status})`);
25
+ if (resp.status === 200) {
26
+ const data = await resp.json();
27
+ if (data?.version) {
28
+ return {
29
+ service: true,
30
+ process: true,
31
+ };
32
+ }
33
+ }
34
+ } catch (err: any) {
35
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
36
+ console.error(`Status error: ${err.message}`);
37
+ }
38
+
39
+ // process
40
+ return {
41
+ service: false,
42
+ process: Boolean(this.process),
43
+ };
44
+ }
45
+ }
46
+
47
+ async function fetchWithTimeout(url: string, options: RequestInit & { timeout: number }) {
48
+ const { timeout = 3000 } = options;
49
+
50
+ const controller = new AbortController();
51
+ const id = setTimeout(() => controller.abort(), timeout);
52
+ // @ts-expect-error
53
+ const response = await fetch(url, {
54
+ ...options,
55
+ signal: controller.signal as any,
56
+ });
57
+ clearTimeout(id);
58
+ return response;
59
+ }
60
+
61
+ export const BridgeHeart = {
62
+ timer: null as ReturnType<typeof setInterval> | null,
63
+ start: (callback: () => void) => {
64
+ const checkBridge = async () => {
65
+ try {
66
+ const localBridgeUrl = 'http://127.0.0.1:21320/';
67
+ const resp = await fetchWithTimeout(localBridgeUrl, {
68
+ method: 'POST',
69
+ headers: {
70
+ Origin: 'https://electron.onekey.so',
71
+ },
72
+ timeout: 3000,
73
+ });
74
+
75
+ if (resp.status !== 200) {
76
+ console.debug(`Bridge Heart Checking ${localBridgeUrl} (${resp.status})`);
77
+ // check bridge failed, restart it
78
+ callback?.();
79
+ }
80
+ } catch (err: any) {
81
+ console.error(
82
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
83
+ `Bridge heart check error, will restart bridge process: ${err.message}`
84
+ );
85
+ callback?.();
86
+ }
87
+ };
88
+
89
+ BridgeHeart.timer = setInterval(checkBridge, 10000);
90
+ },
91
+ };
92
+
93
+ export default BridgeProcess;
@@ -0,0 +1,36 @@
1
+ import { app } from 'electron';
2
+
3
+ import BridgeProcess, { BridgeHeart } from './Bridge';
4
+
5
+ let bridgeInstance: BridgeProcess;
6
+ export const launchBridge = async (isDevelopment: boolean) => {
7
+ const bridge = new BridgeProcess();
8
+
9
+ try {
10
+ console.info('bridge: Staring');
11
+ await bridge.start([], isDevelopment);
12
+ bridgeInstance = bridge;
13
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
14
+ BridgeHeart.start(() => restartBridge());
15
+ } catch (err) {
16
+ console.error(`bridge: Start failed: ${(err as Error).message}`);
17
+ console.error(err);
18
+ }
19
+
20
+ app.on('before-quit', () => {
21
+ console.info('bridge', 'Stopping when app quit');
22
+ bridge.stop();
23
+ });
24
+ };
25
+
26
+ export const restartBridge = async () => {
27
+ console.debug('bridge: ', 'Restarting');
28
+ await bridgeInstance?.restart();
29
+ };
30
+
31
+ const init = async ({ isDevelopment }: { isDevelopment: boolean }) => {
32
+ console.info('Electron main process log path: ');
33
+ await launchBridge(isDevelopment);
34
+ };
35
+
36
+ export default init;
package/tsconfig.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "extends": "../../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "allowSyntheticDefaultImports": true,
5
+ "jsx": "preserve",
6
+ "lib": [
7
+ "dom",
8
+ "esnext"
9
+ ],
10
+ "moduleResolution": "node",
11
+ "noEmit": true,
12
+ "resolveJsonModule": true,
13
+ "target": "esnext",
14
+ "esModuleInterop": true,
15
+ "module": "esnext",
16
+ "allowJs": true // Seems to be needed for index.js
17
+ },
18
+ "include": [
19
+ "**/*.ts",
20
+ "**/*.tsx",
21
+ "src/preload.ts"
22
+ ],
23
+ "exclude": [
24
+ "node_modules",
25
+ "web-build/**/*",
26
+ "out/**/*",
27
+ "dist/**/*",
28
+ "public/**/*"
29
+ ]
30
+ }
@@ -0,0 +1,71 @@
1
+ /* eslint-disable @typescript-eslint/no-var-requires */
2
+ /* eslint-disable import/no-import-module-exports */
3
+ import path from 'path';
4
+ import webpack from 'webpack';
5
+ import nodeExternals from 'webpack-node-externals';
6
+ import { CleanWebpackPlugin } from 'clean-webpack-plugin';
7
+ import childProcess from 'child_process';
8
+
9
+ const pkg = require('./package.json');
10
+
11
+ const gitRevision = childProcess.execSync('git rev-parse HEAD').toString().trim();
12
+
13
+ module.exports = {
14
+ mode: 'production',
15
+ entry: {
16
+ index: path.resolve(__dirname, 'src/index.ts'),
17
+ preload: path.resolve(__dirname, 'src/preload.ts'),
18
+ },
19
+ target: 'electron-main', // 针对Electron主进程
20
+ module: {
21
+ rules: [
22
+ {
23
+ test: /\.js$/,
24
+ exclude: /node_modules/,
25
+ use: ['babel-loader'],
26
+ },
27
+ {
28
+ test: /\.ts$/,
29
+ exclude: /node_modules/,
30
+ use: {
31
+ loader: 'babel-loader',
32
+ options: {
33
+ presets: ['@babel/preset-typescript'],
34
+ plugins: ['@babel/plugin-proposal-optional-chaining'],
35
+ },
36
+ },
37
+ },
38
+ ],
39
+ },
40
+ resolve: {
41
+ extensions: ['.ts', '.js'],
42
+ },
43
+ externals: [
44
+ nodeExternals({
45
+ allowlist: [
46
+ // Include all @onekeyfe packages to handle transitive dependencies
47
+ /^@onekeyfe\//,
48
+ ...Object.keys({
49
+ ...pkg.dependencies,
50
+ ...pkg.devDependencies,
51
+ }),
52
+ ],
53
+ }),
54
+ {
55
+ '@stoprocent/noble': 'commonjs @stoprocent/noble',
56
+ '@stoprocent/bluetooth-hci-socket': 'commonjs @stoprocent/bluetooth-hci-socket',
57
+ bufferutil: 'commonjs bufferutil',
58
+ 'utf-8-validate': 'commonjs utf-8-validate',
59
+ },
60
+ ],
61
+ output: {
62
+ path: path.resolve(__dirname, 'dist'),
63
+ filename: '[name].js',
64
+ },
65
+ plugins: [
66
+ new CleanWebpackPlugin(),
67
+ new webpack.DefinePlugin({
68
+ 'process.env.COMMITHASH': JSON.stringify(gitRevision),
69
+ }),
70
+ ],
71
+ };