Skip to content

Commit b682672

Browse files
committed
feat: askForMediaAccess
1 parent 121e76f commit b682672

File tree

10 files changed

+142
-68
lines changed

10 files changed

+142
-68
lines changed

packages/neuron-ui/src/components/CameraScanDialog/index.tsx

Lines changed: 51 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import React, { useEffect, useRef, useCallback, useState } from 'react'
22
import { useTranslation } from 'react-i18next'
3+
import { askForCameraAccess } from 'services/remote'
34
import Dialog from 'widgets/Dialog'
5+
import AlertDialog from 'widgets/AlertDialog'
6+
import { isSuccessResponse } from 'utils'
47
import jsQR from 'jsqr'
58

69
import styles from './cameraScanDialog.module.scss'
@@ -17,7 +20,7 @@ const CameraScanDialog = ({ close, onConfirm }: { close: () => void; onConfirm:
1720
const videoRef = useRef<HTMLVideoElement>()
1821
const canvasRef = useRef<HTMLCanvasElement>(null)
1922
const canvas2dRef = useRef<CanvasRenderingContext2D>()
20-
const [loading, setLoading] = useState(true)
23+
const [dialogType, setDialogType] = useState<'' | 'no-camera' | 'access-fail' | 'scan'>('')
2124

2225
const drawLine = (begin: Point, end: Point) => {
2326
if (!canvas2dRef.current) return
@@ -31,7 +34,7 @@ const CameraScanDialog = ({ close, onConfirm }: { close: () => void; onConfirm:
3134

3235
const scan = useCallback(() => {
3336
if (videoRef.current?.readyState === HTMLMediaElement.HAVE_ENOUGH_DATA) {
34-
setLoading(false)
37+
setDialogType('scan')
3538
const canvas2d = canvasRef.current?.getContext('2d')
3639
if (canvas2d) {
3740
canvas2d.drawImage(videoRef.current, 0, 0, IMAGE_SIZE, IMAGE_SIZE)
@@ -50,24 +53,31 @@ const CameraScanDialog = ({ close, onConfirm }: { close: () => void; onConfirm:
5053
}
5154
}
5255
requestAnimationFrame(scan)
53-
}, [])
56+
}, [setDialogType])
5457

5558
useEffect(() => {
5659
let mediaStream: MediaStream
57-
navigator.mediaDevices
58-
.getUserMedia({
59-
audio: false,
60-
video: { width: IMAGE_SIZE, height: IMAGE_SIZE },
61-
})
62-
.then(res => {
63-
if (res) {
64-
videoRef.current = document.createElement('video')
65-
videoRef.current.srcObject = res
66-
videoRef.current.play()
67-
mediaStream = res
68-
requestAnimationFrame(scan)
69-
}
70-
})
60+
61+
askForCameraAccess().then(accessRes => {
62+
if (isSuccessResponse(accessRes)) {
63+
navigator.mediaDevices
64+
.getUserMedia({
65+
audio: false,
66+
video: { width: IMAGE_SIZE, height: IMAGE_SIZE },
67+
})
68+
.then(res => {
69+
if (res) {
70+
videoRef.current = document.createElement('video')
71+
videoRef.current.srcObject = res
72+
videoRef.current.play()
73+
mediaStream = res
74+
requestAnimationFrame(scan)
75+
}
76+
})
77+
} else {
78+
setDialogType('access-fail')
79+
}
80+
})
7181

7282
return () => {
7383
if (mediaStream) {
@@ -79,24 +89,32 @@ const CameraScanDialog = ({ close, onConfirm }: { close: () => void; onConfirm:
7989
}, [])
8090

8191
return (
82-
<Dialog
83-
show
84-
title={t('wallet-connect.scan-with-camera')}
85-
onCancel={close}
86-
showCancel={false}
87-
showConfirm={false}
88-
showFooter={false}
89-
>
90-
<div className={styles.container}>
91-
<div className={styles.scanBox}>
92-
{loading ? (
93-
<div>{t('wallet-connect.waiting-camera')}</div>
94-
) : (
92+
<>
93+
<Dialog
94+
show={dialogType === 'scan'}
95+
title={t('wallet-connect.scan-with-camera')}
96+
onCancel={close}
97+
showCancel={false}
98+
showConfirm={false}
99+
showFooter={false}
100+
>
101+
<div className={styles.container}>
102+
<div className={styles.scanBox}>
95103
<canvas ref={canvasRef} width="400px" height="400px" />
96-
)}
104+
</div>
97105
</div>
98-
</div>
99-
</Dialog>
106+
</Dialog>
107+
108+
<AlertDialog
109+
show={dialogType === 'access-fail'}
110+
title={t('wallet-connect.camera-fail')}
111+
message={t('wallet-connect.camera-msg')}
112+
type="failed"
113+
onCancel={() => {
114+
close()
115+
}}
116+
/>
117+
</>
100118
)
101119
}
102120

packages/neuron-ui/src/components/ScreenScanDialog/index.tsx

Lines changed: 44 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import Button from 'widgets/Button'
44
import { isSuccessResponse } from 'utils'
55
import { useTranslation } from 'react-i18next'
66
import Dialog from 'widgets/Dialog'
7+
import AlertDialog from 'widgets/AlertDialog'
78
import jsQR from 'jsqr'
89

910
import styles from './screenScanDialog.module.scss'
@@ -20,6 +21,7 @@ const ScreenScanDialog = ({ close, onConfirm }: { close: () => void; onConfirm:
2021
const canvas2dRef = useRef<CanvasRenderingContext2D>()
2122
const [sources, setSources] = useState<Controller.CaptureScreenSource[]>([])
2223
const [selectId, setSelectId] = useState('')
24+
const [dialogType, setDialogType] = useState<'' | 'access-fail' | 'scan'>('')
2325
const [uri, setUri] = useState('')
2426

2527
const drawLine = (begin: Point, end: Point) => {
@@ -65,11 +67,14 @@ const ScreenScanDialog = ({ close, onConfirm }: { close: () => void; onConfirm:
6567
useEffect(() => {
6668
captureScreen().then(res => {
6769
if (isSuccessResponse(res)) {
70+
setDialogType('scan')
6871
const result = res.result as Controller.CaptureScreenSource[]
6972
setSources(result)
7073
if (result.length) {
7174
setSelectId(result[0].id)
7275
}
76+
} else {
77+
setDialogType('access-fail')
7378
}
7479
})
7580
}, [])
@@ -87,40 +92,48 @@ const ScreenScanDialog = ({ close, onConfirm }: { close: () => void; onConfirm:
8792
}
8893

8994
return (
90-
<Dialog
91-
show
92-
title={t('wallet-connect.scan-with-camera')}
93-
onCancel={close}
94-
disabled={!uri}
95-
onConfirm={handleConfirm}
96-
className={styles.scanDialog}
97-
>
98-
<div className={styles.container}>
99-
<div className={styles.chooseBox}>
100-
{sources.map(({ dataUrl, id }) => (
101-
<Button
102-
key={id}
103-
className={styles.chooseItem}
104-
data-idx={id}
105-
data-active={selectId === id}
106-
onClick={handleSelect}
107-
>
108-
<img src={dataUrl} alt="" />
109-
</Button>
110-
))}
95+
<>
96+
<Dialog
97+
show={dialogType === 'scan'}
98+
title={t('wallet-connect.scan-qrcode')}
99+
onCancel={close}
100+
disabled={!uri}
101+
onConfirm={handleConfirm}
102+
className={styles.scanDialog}
103+
>
104+
<div className={styles.container}>
105+
<div className={styles.chooseBox}>
106+
{sources.map(({ dataUrl, id }) => (
107+
<Button
108+
key={id}
109+
className={styles.chooseItem}
110+
data-idx={id}
111+
data-active={selectId === id}
112+
onClick={handleSelect}
113+
>
114+
<img src={dataUrl} alt="" />
115+
</Button>
116+
))}
117+
</div>
118+
<div className={styles.scanBox}>
119+
<canvas ref={canvasRef} />
120+
{source ? <img ref={imgRef} src={source?.dataUrl} alt="" /> : null}
121+
</div>
111122
</div>
112-
<div className={styles.scanBox}>
113-
<canvas ref={canvasRef} />
114-
{source ? <img ref={imgRef} src={source?.dataUrl} alt="" /> : null}
115-
</div>
116-
</div>
117-
</Dialog>
123+
</Dialog>
124+
125+
<AlertDialog
126+
show={dialogType === 'access-fail'}
127+
title={t('wallet-connect.screen-fail')}
128+
message={t('wallet-connect.screen-msg')}
129+
type="failed"
130+
onCancel={() => {
131+
close()
132+
}}
133+
/>
134+
</>
118135
)
119136
}
120137

121138
ScreenScanDialog.displayName = 'ScreenScanDialog'
122139
export default ScreenScanDialog
123-
124-
// {loading ? (
125-
// <div>{t('wallet-connect.waiting-camera')}</div>
126-
// )

packages/neuron-ui/src/locales/en.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1145,6 +1145,10 @@
11451145
"scan-with-camera": "Scanning with the camera",
11461146
"scan-qrcode": "Scan the QR code in the screen",
11471147
"no-camera-tip": "Don't have a camera? Use the uri to connect",
1148+
"camera-fail": "Unable to acquire camera data",
1149+
"camera-msg": "Please check that the settings allow Neuron to access the camera",
1150+
"screen-fail": "Unable to get screen data",
1151+
"screen-msg": "Please check that the settings allow Neuron to access the screen",
11481152
"session-request": "Connection Requests",
11491153
"no-session": "No connection requests",
11501154
"reject": "Reject",

packages/neuron-ui/src/locales/zh-tw.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1116,6 +1116,10 @@
11161116
"scan-with-camera": "使用攝像頭掃描",
11171117
"scan-qrcode": "掃描屏幕二維碼",
11181118
"no-camera-tip": "沒有攝像頭?使用連結碼連結",
1119+
"camera-fail": "無法獲取攝像頭數據",
1120+
"camera-msg": "請檢查設置是否允許Neuron訪問攝像頭",
1121+
"screen-fail": "無法獲取屏幕數據",
1122+
"screen-msg": "請檢查設置是否允許Neuron訪問屏幕",
11191123
"session-request": "連結請求",
11201124
"no-session": "沒有連結請求",
11211125
"reject": "拒絕",

packages/neuron-ui/src/locales/zh.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1136,8 +1136,11 @@
11361136
"add-title": "新增WalletConnect连接",
11371137
"scan-with-camera": "使用摄像头扫描",
11381138
"scan-qrcode": "扫描屏幕二维码",
1139-
"waiting-camera": "等待摄像头响应...",
11401139
"no-camera-tip": "没有摄像头?使用连接码连接",
1140+
"camera-fail": "无法获取摄像头数据",
1141+
"camera-msg": "请检查设置是否允许Neuron访问摄像头",
1142+
"screen-fail": "无法获取屏幕数据",
1143+
"screen-msg": "请检查设置是否允许Neuron访问屏幕",
11411144
"session-request": "连接请求",
11421145
"no-session": "没有连接请求",
11431146
"reject": "拒绝",

packages/neuron-ui/src/services/remote/hardware.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,5 @@ export const connectDevice = remoteApi<DeviceInfo, void>('connect-device')
3939
export const createHardwareWallet = remoteApi<ExtendedPublicKey & { walletName: string }, State.Wallet>(
4040
'create-hardware-wallet'
4141
)
42+
export const askForCameraAccess = remoteApi<void>('ask-camera-access')
4243
export const captureScreen = remoteApi<void>('capture-screen')

packages/neuron-ui/src/services/remote/remoteApiWrapper.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ type Action =
158158
| 'wc-reject-session'
159159
| 'wc-approve-request'
160160
| 'wc-reject-request'
161+
| 'ask-camera-access'
161162
| 'capture-screen'
162163

163164
export const remoteApi =

packages/neuron-wallet/src/controllers/api.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -733,6 +733,10 @@ export default class ApiController {
733733
return this.#hardwareController.getPublicKey()
734734
})
735735

736+
handle('ask-camera-access', async () => {
737+
return this.#hardwareController.askForCameraAccess()
738+
})
739+
736740
handle('capture-screen', async () => {
737741
return this.#hardwareController.captureScreen()
738742
})

packages/neuron-wallet/src/controllers/hardware.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { desktopCapturer, screen, BrowserWindow } from 'electron'
1+
import { desktopCapturer, screen, BrowserWindow, systemPreferences } from 'electron'
22
import logger from '../utils/logger'
33
import { DeviceInfo, ExtendedPublicKey, PublicKey } from '../services/hardware/common'
44
import { ResponseCode } from '../utils/const'
55
import HardwareWalletService from '../services/hardware'
6-
import { connectDeviceFailed } from '../exceptions'
6+
import { connectDeviceFailed, AskAccessFailed } from '../exceptions'
77
import { AccountExtendedPublicKey } from '../models/keys/key'
88

99
export default class HardwareController {
@@ -71,8 +71,30 @@ export default class HardwareController {
7171
}
7272
}
7373

74+
public async askForCameraAccess() {
75+
const status = await systemPreferences.getMediaAccessStatus('camera')
76+
if (status === 'granted') {
77+
return {
78+
status: ResponseCode.Success,
79+
}
80+
}
81+
82+
const canAccess = await systemPreferences.askForMediaAccess('camera')
83+
if (canAccess) {
84+
return {
85+
status: ResponseCode.Success,
86+
}
87+
}
88+
89+
throw new AskAccessFailed()
90+
}
91+
7492
public async captureScreen() {
75-
// TODO: 权限提示
93+
const status = await systemPreferences.getMediaAccessStatus('screen')
94+
if (status === 'denied') {
95+
throw new AskAccessFailed()
96+
}
97+
7698
const currentWindow = BrowserWindow.getFocusedWindow()
7799
currentWindow?.hide()
78100
const display = screen.getPrimaryDisplay()

packages/neuron-wallet/src/exceptions/hardware.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,7 @@ export class UnsupportedManufacturer extends Error {
1919
super(t('messages.unsupported-manufacturer', { manufacturer }))
2020
}
2121
}
22+
23+
export class AskAccessFailed extends Error {
24+
public code = 408
25+
}

0 commit comments

Comments
 (0)