Skip to content

Commit

Permalink
feat: askForMediaAccess
Browse files Browse the repository at this point in the history
  • Loading branch information
devchenyan committed Oct 22, 2023
1 parent 121e76f commit 2c95557
Show file tree
Hide file tree
Showing 10 changed files with 142 additions and 68 deletions.
84 changes: 51 additions & 33 deletions packages/neuron-ui/src/components/CameraScanDialog/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import React, { useEffect, useRef, useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { askForCameraAccess } from 'services/remote'
import Dialog from 'widgets/Dialog'
import AlertDialog from 'widgets/AlertDialog'
import { isSuccessResponse } from 'utils'
import jsQR from 'jsqr'

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

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

const scan = useCallback(() => {
if (videoRef.current?.readyState === HTMLMediaElement.HAVE_ENOUGH_DATA) {
setLoading(false)
setDialogType('scan')
const canvas2d = canvasRef.current?.getContext('2d')
if (canvas2d) {
canvas2d.drawImage(videoRef.current, 0, 0, IMAGE_SIZE, IMAGE_SIZE)
Expand All @@ -50,24 +53,31 @@ const CameraScanDialog = ({ close, onConfirm }: { close: () => void; onConfirm:
}
}
requestAnimationFrame(scan)
}, [])
}, [setDialogType])

useEffect(() => {
let mediaStream: MediaStream
navigator.mediaDevices
.getUserMedia({
audio: false,
video: { width: IMAGE_SIZE, height: IMAGE_SIZE },
})
.then(res => {
if (res) {
videoRef.current = document.createElement('video')
videoRef.current.srcObject = res
videoRef.current.play()
mediaStream = res
requestAnimationFrame(scan)
}
})

askForCameraAccess().then(sccessRes => {

Check warning on line 61 in packages/neuron-ui/src/components/CameraScanDialog/index.tsx

View workflow job for this annotation

GitHub Actions / Check spell

"sccess" should be "success".
if (isSuccessResponse(sccessRes)) {

Check warning on line 62 in packages/neuron-ui/src/components/CameraScanDialog/index.tsx

View workflow job for this annotation

GitHub Actions / Check spell

"sccess" should be "success".
navigator.mediaDevices
.getUserMedia({
audio: false,
video: { width: IMAGE_SIZE, height: IMAGE_SIZE },
})
.then(res => {
if (res) {
videoRef.current = document.createElement('video')
videoRef.current.srcObject = res
videoRef.current.play()
mediaStream = res
requestAnimationFrame(scan)
}
})
} else {
setDialogType('access-fail')
}
})

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

return (
<Dialog
show
title={t('wallet-connect.scan-with-camera')}
onCancel={close}
showCancel={false}
showConfirm={false}
showFooter={false}
>
<div className={styles.container}>
<div className={styles.scanBox}>
{loading ? (
<div>{t('wallet-connect.waiting-camera')}</div>
) : (
<>
<Dialog
show={dialogType === 'scan'}
title={t('wallet-connect.scan-with-camera')}
onCancel={close}
showCancel={false}
showConfirm={false}
showFooter={false}
>
<div className={styles.container}>
<div className={styles.scanBox}>
<canvas ref={canvasRef} width="400px" height="400px" />
)}
</div>
</div>
</div>
</Dialog>
</Dialog>

<AlertDialog
show={dialogType === 'access-fail'}
title={t('wallet-connect.camera-fail')}
message={t('wallet-connect.camera-msg')}
type="failed"
onCancel={() => {
close()
}}
/>
</>
)
}

Expand Down
75 changes: 44 additions & 31 deletions packages/neuron-ui/src/components/ScreenScanDialog/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Button from 'widgets/Button'
import { isSuccessResponse } from 'utils'
import { useTranslation } from 'react-i18next'
import Dialog from 'widgets/Dialog'
import AlertDialog from 'widgets/AlertDialog'
import jsQR from 'jsqr'

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

const drawLine = (begin: Point, end: Point) => {
Expand Down Expand Up @@ -65,11 +67,14 @@ const ScreenScanDialog = ({ close, onConfirm }: { close: () => void; onConfirm:
useEffect(() => {
captureScreen().then(res => {
if (isSuccessResponse(res)) {
setDialogType('scan')
const result = res.result as Controller.CaptureScreenSource[]
setSources(result)
if (result.length) {
setSelectId(result[0].id)
}
} else {
setDialogType('access-fail')
}
})
}, [])
Expand All @@ -87,40 +92,48 @@ const ScreenScanDialog = ({ close, onConfirm }: { close: () => void; onConfirm:
}

return (
<Dialog
show
title={t('wallet-connect.scan-with-camera')}
onCancel={close}
disabled={!uri}
onConfirm={handleConfirm}
className={styles.scanDialog}
>
<div className={styles.container}>
<div className={styles.chooseBox}>
{sources.map(({ dataUrl, id }) => (
<Button
key={id}
className={styles.chooseItem}
data-idx={id}
data-active={selectId === id}
onClick={handleSelect}
>
<img src={dataUrl} alt="" />
</Button>
))}
<>
<Dialog
show={dialogType === 'scan'}
title={t('wallet-connect.scan-qrcode')}
onCancel={close}
disabled={!uri}
onConfirm={handleConfirm}
className={styles.scanDialog}
>
<div className={styles.container}>
<div className={styles.chooseBox}>
{sources.map(({ dataUrl, id }) => (
<Button
key={id}
className={styles.chooseItem}
data-idx={id}
data-active={selectId === id}
onClick={handleSelect}
>
<img src={dataUrl} alt="" />
</Button>
))}
</div>
<div className={styles.scanBox}>
<canvas ref={canvasRef} />
{source ? <img ref={imgRef} src={source?.dataUrl} alt="" /> : null}
</div>
</div>
<div className={styles.scanBox}>
<canvas ref={canvasRef} />
{source ? <img ref={imgRef} src={source?.dataUrl} alt="" /> : null}
</div>
</div>
</Dialog>
</Dialog>

<AlertDialog
show={dialogType === 'access-fail'}
title={t('wallet-connect.screen-fail')}
message={t('wallet-connect.screen-msg')}
type="failed"
onCancel={() => {
close()
}}
/>
</>
)
}

ScreenScanDialog.displayName = 'ScreenScanDialog'
export default ScreenScanDialog

// {loading ? (
// <div>{t('wallet-connect.waiting-camera')}</div>
// )
4 changes: 4 additions & 0 deletions packages/neuron-ui/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1145,6 +1145,10 @@
"scan-with-camera": "Scanning with the camera",
"scan-qrcode": "Scan the QR code in the screen",
"no-camera-tip": "Don't have a camera? Use the uri to connect",
"camera-fail": "Unable to acquire camera data",
"camera-msg": "Please check that the settings allow Neuron to access the camera",
"screen-fail": "Unable to get screen data",
"screen-msg": "Please check that the settings allow Neuron to access the screen",
"session-request": "Connection Requests",
"no-session": "No connection requests",
"reject": "Reject",
Expand Down
4 changes: 4 additions & 0 deletions packages/neuron-ui/src/locales/zh-tw.json
Original file line number Diff line number Diff line change
Expand Up @@ -1116,6 +1116,10 @@
"scan-with-camera": "使用攝像頭掃描",
"scan-qrcode": "掃描屏幕二維碼",
"no-camera-tip": "沒有攝像頭?使用連結碼連結",
"camera-fail": "無法獲取攝像頭數據",
"camera-msg": "請檢查設置是否允許Neuron訪問攝像頭",
"screen-fail": "無法獲取屏幕數據",
"screen-msg": "請檢查設置是否允許Neuron訪問屏幕",
"session-request": "連結請求",
"no-session": "沒有連結請求",
"reject": "拒絕",
Expand Down
5 changes: 4 additions & 1 deletion packages/neuron-ui/src/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -1136,8 +1136,11 @@
"add-title": "新增WalletConnect连接",
"scan-with-camera": "使用摄像头扫描",
"scan-qrcode": "扫描屏幕二维码",
"waiting-camera": "等待摄像头响应...",
"no-camera-tip": "没有摄像头?使用连接码连接",
"camera-fail": "无法获取摄像头数据",
"camera-msg": "请检查设置是否允许Neuron访问摄像头",
"screen-fail": "无法获取屏幕数据",
"screen-msg": "请检查设置是否允许Neuron访问屏幕",
"session-request": "连接请求",
"no-session": "没有连接请求",
"reject": "拒绝",
Expand Down
1 change: 1 addition & 0 deletions packages/neuron-ui/src/services/remote/hardware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,5 @@ export const connectDevice = remoteApi<DeviceInfo, void>('connect-device')
export const createHardwareWallet = remoteApi<ExtendedPublicKey & { walletName: string }, State.Wallet>(
'create-hardware-wallet'
)
export const askForCameraAccess = remoteApi<void>('ask-camera-access')
export const captureScreen = remoteApi<void>('capture-screen')
1 change: 1 addition & 0 deletions packages/neuron-ui/src/services/remote/remoteApiWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ type Action =
| 'wc-reject-session'
| 'wc-approve-request'
| 'wc-reject-request'
| 'ask-camera-access'
| 'capture-screen'

export const remoteApi =
Expand Down
4 changes: 4 additions & 0 deletions packages/neuron-wallet/src/controllers/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,10 @@ export default class ApiController {
return this.#hardwareController.getPublicKey()
})

handle('ask-camera-access', async () => {
return this.#hardwareController.askForCameraAccess()
})

handle('capture-screen', async () => {
return this.#hardwareController.captureScreen()
})
Expand Down
28 changes: 25 additions & 3 deletions packages/neuron-wallet/src/controllers/hardware.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { desktopCapturer, screen, BrowserWindow } from 'electron'
import { desktopCapturer, screen, BrowserWindow, systemPreferences } from 'electron'
import logger from '../utils/logger'
import { DeviceInfo, ExtendedPublicKey, PublicKey } from '../services/hardware/common'
import { ResponseCode } from '../utils/const'
import HardwareWalletService from '../services/hardware'
import { connectDeviceFailed } from '../exceptions'
import { connectDeviceFailed, AskAccessFailed } from '../exceptions'
import { AccountExtendedPublicKey } from '../models/keys/key'

export default class HardwareController {
Expand Down Expand Up @@ -71,8 +71,30 @@ export default class HardwareController {
}
}

public async askForCameraAccess() {
const status = await systemPreferences.getMediaAccessStatus('camera')
if (status === 'granted') {
return {
status: ResponseCode.Success,
}
}

const canAccess = await systemPreferences.askForMediaAccess('camera')
if (canAccess) {
return {
status: ResponseCode.Success,
}
}

throw new AskAccessFailed()
}

public async captureScreen() {
// TODO: 权限提示
const status = await systemPreferences.getMediaAccessStatus('screen')
if (status === 'denied') {
throw new AskAccessFailed()
}

const currentWindow = BrowserWindow.getFocusedWindow()
currentWindow?.hide()
const display = screen.getPrimaryDisplay()
Expand Down
4 changes: 4 additions & 0 deletions packages/neuron-wallet/src/exceptions/hardware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,7 @@ export class UnsupportedManufacturer extends Error {
super(t('messages.unsupported-manufacturer', { manufacturer }))
}
}

export class AskAccessFailed extends Error {
public code = 408
}

0 comments on commit 2c95557

Please sign in to comment.