diff --git a/src/extension/modals/ScanQRCodeModal/QRCodeFrameIcon.tsx b/src/extension/components/ScanQRCodeViaCameraModalContent/QRCodeFrameIcon.tsx similarity index 100% rename from src/extension/modals/ScanQRCodeModal/QRCodeFrameIcon.tsx rename to src/extension/components/ScanQRCodeViaCameraModalContent/QRCodeFrameIcon.tsx diff --git a/src/extension/modals/ScanQRCodeModal/ScanQRCodeModalCameraStreamContent.tsx b/src/extension/components/ScanQRCodeViaCameraModalContent/ScanQRCodeViaCameraModalContent.tsx similarity index 64% rename from src/extension/modals/ScanQRCodeModal/ScanQRCodeModalCameraStreamContent.tsx rename to src/extension/components/ScanQRCodeViaCameraModalContent/ScanQRCodeViaCameraModalContent.tsx index ac867f38..c3d52554 100644 --- a/src/extension/modals/ScanQRCodeModal/ScanQRCodeModalCameraStreamContent.tsx +++ b/src/extension/components/ScanQRCodeViaCameraModalContent/ScanQRCodeViaCameraModalContent.tsx @@ -10,13 +10,7 @@ import { Text, VStack, } from '@chakra-ui/react'; -import React, { - FC, - MutableRefObject, - useEffect, - useRef, - useState, -} from 'react'; +import React, { FC, MutableRefObject, useEffect, useRef } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { IoAlertCircleOutline, @@ -35,101 +29,58 @@ import { SUPPORT_MAIL_TO_LINK, } from '@extension/constants'; -// errors -import { BaseExtensionError, CameraError } from '@extension/errors'; +// enums +import { ErrorCodeEnum, ScanModeEnum } from '@extension/enums'; // hooks +import useCameraStream from '@extension/hooks/useCameraStream'; +import useCaptureQRCode from '@extension/hooks/useCaptureQRCode'; import useColorModeValue from '@extension/hooks/useColorModeValue'; import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; import useSubTextColor from '@extension/hooks/useSubTextColor'; -// selectors -import { useSelectLogger } from '@extension/selectors'; - // theme import { theme } from '@extension/theme'; // types -import type { ILogger } from '@common/types'; - -interface IProps { - onPreviousClick: () => void; -} +import type { IScanQRCodeModalContentProps } from '@extension/types'; -const ScanQRCodeModalCameraStreamContent: FC = ({ +const ScanQRCodeViaCameraModalContent: FC = ({ onPreviousClick, -}: IProps) => { - const { t } = useTranslation(); + onURI, +}) => { const videoRef: MutableRefObject = useRef(null); - // selectors - const logger: ILogger = useSelectLogger(); + const { t } = useTranslation(); // hooks + const { + error, + loading, + reset: resetCameraStream, + startStream, + stream, + } = useCameraStream(); + const { + resetAction: resetScanAction, + startScanningAction, + uri, + } = useCaptureQRCode(); const defaultTextColor: string = useDefaultTextColor(); const primaryColor: string = useColorModeValue( theme.colors.primaryLight['500'], theme.colors.primaryDark['500'] ); const subTextColor: string = useSubTextColor(); - // state - const [error, setError] = useState(null); - const [loading, setLoading] = useState(false); - const [notAllowed, setNotAllowed] = useState(false); - const [stream, setStream] = useState(null); // misc - const startStreaming = async () => { - const _functionName: string = 'useEffect'; - let _stream: MediaStream; - - if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { - try { - setError(null); - setLoading(true); - - _stream = await navigator.mediaDevices.getUserMedia({ - video: { - height: window.innerHeight, - width: window.innerWidth, - }, - }); - - setStream(_stream); - } catch (error) { - logger.error( - `${ScanQRCodeModalCameraStreamContent.name}#${_functionName}: `, - error - ); - - // if the user denied access, inform the user - if ( - (error as DOMException).name === 'NotAllowedError' || - (error as DOMException).name === 'SecurityError' - ) { - setNotAllowed(true); - setLoading(false); - - return; - } - - setError(new CameraError((error as DOMException).name, error.message)); - } - - setLoading(false); - } + const reset = () => { + // stop scanning and stop streaming + resetCameraStream(); + resetScanAction(); }; // handlers const handlePreviousClick = () => { - // stop the camera stream - if (stream) { - stream.getTracks().forEach((value) => value.stop()); - } - - setError(null); - setLoading(false); - setStream(null); - setNotAllowed(false); - onPreviousClick(); + reset(); }; // renders const renderBody = () => { @@ -137,8 +88,25 @@ const ScanQRCodeModalCameraStreamContent: FC = ({ return ; } - // show a general error page if (error) { + // if the camera was denied access, show a special error + if (error.code === ErrorCodeEnum.CameraNotAllowedError) { + return ( + <> + {/*icon*/} + + + {/*captions*/} + + {t('captions.cameraQRCodeScanNotAllowed1')} + + + {t('captions.cameraQRCodeScanNotAllowed2')} + + + ); + } + return ( <> {/*icon*/} @@ -171,23 +139,6 @@ const ScanQRCodeModalCameraStreamContent: FC = ({ ); } - if (notAllowed) { - return ( - <> - {/*icon*/} - - - {/*captions*/} - - {t('captions.cameraQRCodeScanNotAllowed1')} - - - {t('captions.cameraQRCodeScanNotAllowed2')} - - - ); - } - return ( <> {/*loader*/} @@ -208,14 +159,27 @@ const ScanQRCodeModalCameraStreamContent: FC = ({ }; useEffect(() => { - (async () => await startStreaming())(); + (async () => await startStream())(); }, []); useEffect(() => { if (stream && videoRef.current) { videoRef.current.srcObject = stream; videoRef.current.play(); + + // once we start the stream, start scanning for a qr code + startScanningAction({ + mode: ScanModeEnum.Camera, + videoElement: videoRef.current, + }); } }, [stream]); + useEffect(() => { + if (uri) { + onURI(uri); + + reset(); + } + }, [uri]); return ( = ({ /> {/*header*/} - {notAllowed && ( + {error && error.code === ErrorCodeEnum.CameraNotAllowedError && ( {t('headings.cameraDenied')} @@ -285,4 +249,4 @@ const ScanQRCodeModalCameraStreamContent: FC = ({ ); }; -export default ScanQRCodeModalCameraStreamContent; +export default ScanQRCodeViaCameraModalContent; diff --git a/src/extension/components/ScanQRCodeViaCameraModalContent/index.ts b/src/extension/components/ScanQRCodeViaCameraModalContent/index.ts new file mode 100644 index 00000000..f09bfa71 --- /dev/null +++ b/src/extension/components/ScanQRCodeViaCameraModalContent/index.ts @@ -0,0 +1 @@ +export { default } from './ScanQRCodeViaCameraModalContent'; diff --git a/src/extension/modals/ScanQRCodeModal/ScanQRCodeModalScanningContent.tsx b/src/extension/components/ScanQRCodeViaTabModalContent/ScanQRCodeViaTabModalContent.tsx similarity index 73% rename from src/extension/modals/ScanQRCodeModal/ScanQRCodeModalScanningContent.tsx rename to src/extension/components/ScanQRCodeViaTabModalContent/ScanQRCodeViaTabModalContent.tsx index 3ef92069..df51d828 100644 --- a/src/extension/modals/ScanQRCodeModal/ScanQRCodeModalScanningContent.tsx +++ b/src/extension/components/ScanQRCodeViaTabModalContent/ScanQRCodeViaTabModalContent.tsx @@ -8,7 +8,7 @@ import { Text, VStack, } from '@chakra-ui/react'; -import React, { FC } from 'react'; +import React, { FC, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { IoArrowBackOutline } from 'react-icons/io5'; @@ -18,29 +18,55 @@ import Button from '@extension/components/Button'; // constants import { BODY_BACKGROUND_COLOR, DEFAULT_GAP } from '@extension/constants'; +// enums +import { ScanModeEnum } from '@extension/enums'; + // hooks -import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; +import useCaptureQRCode from '@extension/hooks/useCaptureQRCode'; import useColorModeValue from '@extension/hooks/useColorModeValue'; +import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; // theme import { theme } from '@extension/theme'; -interface IProps { - onPreviousClick: () => void; -} +// types +import type { IScanQRCodeModalContentProps } from '@extension/types'; -const ScanQRCodeModalScanningContent: FC = ({ +const ScanQRCodeViaTabModalContent: FC = ({ onPreviousClick, -}: IProps) => { + onURI, +}) => { const { t } = useTranslation(); // hooks + const { + resetAction: resetScanAction, + startScanningAction, + uri, + } = useCaptureQRCode(); const defaultTextColor: string = useDefaultTextColor(); const primaryColor: string = useColorModeValue( theme.colors.primaryLight['500'], theme.colors.primaryDark['500'] ); // handlers - const handlePreviousClick = () => onPreviousClick(); + const handlePreviousClick = () => { + onPreviousClick(); + resetScanAction(); + }; + + useEffect(() => { + startScanningAction({ + mode: ScanModeEnum.Tab, + }); + }, []); + useEffect(() => { + if (uri) { + onURI(uri); + + // stop scanning + resetScanAction(); + } + }, [uri]); return ( = ({ ); }; -export default ScanQRCodeModalScanningContent; +export default ScanQRCodeViaTabModalContent; diff --git a/src/extension/components/ScanQRCodeViaTabModalContent/index.ts b/src/extension/components/ScanQRCodeViaTabModalContent/index.ts new file mode 100644 index 00000000..7e03b120 --- /dev/null +++ b/src/extension/components/ScanQRCodeViaTabModalContent/index.ts @@ -0,0 +1 @@ +export { default } from './ScanQRCodeViaTabModalContent'; diff --git a/src/extension/enums/ErrorCodeEnum.ts b/src/extension/enums/ErrorCodeEnum.ts index a8b05489..0abe90c6 100644 --- a/src/extension/enums/ErrorCodeEnum.ts +++ b/src/extension/enums/ErrorCodeEnum.ts @@ -21,8 +21,10 @@ enum ErrorCodeEnum { InvalidABIContractError = 5000, ReadABIContractError = 5001, - // devices + // camera CameraError = 6000, + CameraNotAllowedError = 6001, + CameraNotFoundError = 6002, } export default ErrorCodeEnum; diff --git a/src/extension/enums/ScanModeEnum.ts b/src/extension/enums/ScanModeEnum.ts new file mode 100644 index 00000000..47b31b23 --- /dev/null +++ b/src/extension/enums/ScanModeEnum.ts @@ -0,0 +1,6 @@ +enum ScanModeEnum { + Camera = 'camera', + Tab = 'tab', +} + +export default ScanModeEnum; diff --git a/src/extension/enums/index.ts b/src/extension/enums/index.ts index cedcb902..45b2c756 100644 --- a/src/extension/enums/index.ts +++ b/src/extension/enums/index.ts @@ -16,6 +16,7 @@ export { default as NetworksThunkEnum } from './NetworksThunkEnum'; export { default as NetworkTypeEnum } from './NetworkTypeEnum'; export { default as PasswordLockThunkEnum } from './PasswordLockThunkEnum'; export { default as RegisterThunkEnum } from './RegisterThunkEnum'; +export { default as ScanModeEnum } from './ScanModeEnum'; export { default as SendAssetsThunkEnum } from './SendAssetsThunkEnum'; export { default as SessionsThunkEnum } from './SessionsThunkEnum'; export { default as SettingsThunkEnum } from './SettingsThunkEnum'; diff --git a/src/extension/errors/CameraNotAllowedError.ts b/src/extension/errors/CameraNotAllowedError.ts new file mode 100644 index 00000000..ea4ef415 --- /dev/null +++ b/src/extension/errors/CameraNotAllowedError.ts @@ -0,0 +1,10 @@ +// enums +import { ErrorCodeEnum } from '../enums'; + +// errors +import BaseExtensionError from './BaseExtensionError'; + +export default class CameraNotAllowedError extends BaseExtensionError { + public readonly code: ErrorCodeEnum = ErrorCodeEnum.CameraNotAllowedError; + public readonly name: string = 'CameraNotAllowedError'; +} diff --git a/src/extension/errors/CameraNotFoundError.ts b/src/extension/errors/CameraNotFoundError.ts new file mode 100644 index 00000000..382761ac --- /dev/null +++ b/src/extension/errors/CameraNotFoundError.ts @@ -0,0 +1,10 @@ +// enums +import { ErrorCodeEnum } from '../enums'; + +// errors +import BaseExtensionError from './BaseExtensionError'; + +export default class CameraNotFoundError extends BaseExtensionError { + public readonly code: ErrorCodeEnum = ErrorCodeEnum.CameraNotFoundError; + public readonly name: string = 'CameraNotFoundError'; +} diff --git a/src/extension/errors/index.ts b/src/extension/errors/index.ts index 64030065..5250d958 100644 --- a/src/extension/errors/index.ts +++ b/src/extension/errors/index.ts @@ -1,5 +1,7 @@ export { default as BaseExtensionError } from './BaseExtensionError'; export { default as CameraError } from './CameraError'; +export { default as CameraNotAllowedError } from './CameraNotAllowedError'; +export { default as CameraNotFoundError } from './CameraNotFoundError'; export { default as DecryptionError } from './DecryptionError'; export { default as EncryptionError } from './EncryptionError'; export { default as FailedToSendTransactionError } from './FailedToSendTransactionError'; diff --git a/src/extension/hooks/useCameraStream/index.ts b/src/extension/hooks/useCameraStream/index.ts new file mode 100644 index 00000000..46d52504 --- /dev/null +++ b/src/extension/hooks/useCameraStream/index.ts @@ -0,0 +1 @@ +export { default } from './useCameraStream'; diff --git a/src/extension/hooks/useCameraStream/types/IUseCameraStreamState.ts b/src/extension/hooks/useCameraStream/types/IUseCameraStreamState.ts new file mode 100644 index 00000000..3d25123f --- /dev/null +++ b/src/extension/hooks/useCameraStream/types/IUseCameraStreamState.ts @@ -0,0 +1,13 @@ +// errors +import { BaseExtensionError } from '@extension/errors'; + +interface IUseCameraStreamState { + error: BaseExtensionError | null; + loading: boolean; + reset: () => void; + startStream: () => Promise; + stopStream: () => void; + stream: MediaStream | null; +} + +export default IUseCameraStreamState; diff --git a/src/extension/hooks/useCameraStream/types/index.ts b/src/extension/hooks/useCameraStream/types/index.ts new file mode 100644 index 00000000..c337523a --- /dev/null +++ b/src/extension/hooks/useCameraStream/types/index.ts @@ -0,0 +1 @@ +export type { default as IUseCameraStreamState } from './IUseCameraStreamState'; diff --git a/src/extension/hooks/useCameraStream/useCameraStream.ts b/src/extension/hooks/useCameraStream/useCameraStream.ts new file mode 100644 index 00000000..ee157b67 --- /dev/null +++ b/src/extension/hooks/useCameraStream/useCameraStream.ts @@ -0,0 +1,90 @@ +import { useState } from 'react'; + +// errors +import { + BaseExtensionError, + CameraError, + CameraNotAllowedError, + CameraNotFoundError, +} from '@extension/errors'; + +// selectors +import { useSelectLogger } from '@extension/selectors'; + +// types +import type { ILogger } from '@common/types'; +import type { IUseCameraStreamState } from './types'; + +// utils +import isCameraAvailable from '@extension/utils/isCameraAvailable'; + +export default function useCameraStream(): IUseCameraStreamState { + // selectors + const logger: ILogger = useSelectLogger(); + // state + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + const [stream, setStream] = useState(null); + // handlers + const reset = () => { + stopStream(); + setError(null); + setLoading(false); + setStream(null); + }; + const startStream = async () => { + const _functionName: string = 'startStream'; + let _stream: MediaStream; + + if (!isCameraAvailable()) { + setError(new CameraNotFoundError('camera not found')); + + return; + } + + try { + setError(null); + setLoading(true); + + _stream = await navigator.mediaDevices.getUserMedia({ + video: { + height: window.innerHeight, + width: window.innerWidth, + }, + }); + + // set the stream + setStream(_stream); + } catch (error) { + logger.error(`${_functionName}: `, error); + + // if the user denied access, inform the user + if ( + (error as DOMException).name === 'NotAllowedError' || + (error as DOMException).name === 'SecurityError' + ) { + setError(new CameraNotAllowedError('camera was denied permission')); + setLoading(false); + + return; + } + + setError(new CameraError((error as DOMException).name, error.message)); + } + }; + const stopStream = () => { + // stop the camera stream + if (stream) { + stream.getTracks().forEach((value) => value.stop()); + } + }; + + return { + error, + loading, + reset, + startStream, + stopStream, + stream, + }; +} diff --git a/src/extension/hooks/useCaptureQRCode/types/IScanMode.ts b/src/extension/hooks/useCaptureQRCode/types/IScanMode.ts deleted file mode 100644 index da6cc2cc..00000000 --- a/src/extension/hooks/useCaptureQRCode/types/IScanMode.ts +++ /dev/null @@ -1,3 +0,0 @@ -type IScanMode = 'browserWindow' | 'extensionPopup'; - -export default IScanMode; diff --git a/src/extension/hooks/useCaptureQRCode/types/IStartScanningOptions.ts b/src/extension/hooks/useCaptureQRCode/types/IStartScanningOptions.ts new file mode 100644 index 00000000..b44f5e3a --- /dev/null +++ b/src/extension/hooks/useCaptureQRCode/types/IStartScanningOptions.ts @@ -0,0 +1,13 @@ +// enums +import { ScanModeEnum } from '@extension/enums'; + +type IStartScanningOptions = + | { + mode: ScanModeEnum.Camera; + videoElement: HTMLVideoElement; + } + | { + mode: ScanModeEnum.Tab; + }; + +export default IStartScanningOptions; diff --git a/src/extension/hooks/useCaptureQRCode/types/IUseCaptureQrCodeState.ts b/src/extension/hooks/useCaptureQRCode/types/IUseCaptureQrCodeState.ts index a19782a4..718b47aa 100644 --- a/src/extension/hooks/useCaptureQRCode/types/IUseCaptureQrCodeState.ts +++ b/src/extension/hooks/useCaptureQRCode/types/IUseCaptureQrCodeState.ts @@ -1,10 +1,10 @@ // types -import IScanMode from './IScanMode'; +import IStartScanningOptions from './IStartScanningOptions'; interface IUseCaptureQrCodeState { resetAction: () => void; scanning: boolean; - startScanningAction: (mode: IScanMode) => void; + startScanningAction: (options: IStartScanningOptions) => void; stopScanningAction: () => void; uri: string | null; } diff --git a/src/extension/hooks/useCaptureQRCode/types/index.ts b/src/extension/hooks/useCaptureQRCode/types/index.ts index ad6b1832..778f26fb 100644 --- a/src/extension/hooks/useCaptureQRCode/types/index.ts +++ b/src/extension/hooks/useCaptureQRCode/types/index.ts @@ -1,2 +1,2 @@ -export type { default as IScanMode } from './IScanMode'; +export type { default as IStartScanningOptions } from './IStartScanningOptions'; export type { default as IUseCaptureQrCodeState } from './IUseCaptureQrCodeState'; diff --git a/src/extension/hooks/useCaptureQRCode/useCaptureQRCode.ts b/src/extension/hooks/useCaptureQRCode/useCaptureQRCode.ts index e838dc48..6aa63ab1 100644 --- a/src/extension/hooks/useCaptureQRCode/useCaptureQRCode.ts +++ b/src/extension/hooks/useCaptureQRCode/useCaptureQRCode.ts @@ -3,50 +3,60 @@ import { useState } from 'react'; // constants import { QR_CODE_SCAN_INTERVAL } from '@extension/constants'; +// enums +import { ScanModeEnum } from '@extension/enums'; + // selectors import { useSelectLogger } from '@extension/selectors'; // types import type { ILogger } from '@common/types'; -import type { IScanMode, IUseCaptureQrCodeState } from './types'; +import type { IStartScanningOptions, IUseCaptureQrCodeState } from './types'; // utils -import captureQRCode from './utils/captureQRCode'; +import captureQRCodeFromCamera from './utils/captureQRCodeFromCamera'; +import captureQRCodeFromTab from './utils/captureQRCodeFromTab'; export default function useCaptureQRCode(): IUseCaptureQrCodeState { - const _functionName: string = 'useCaptureQRCode'; // selectors const logger: ILogger = useSelectLogger(); // states const [intervalId, setIntervalId] = useState(null); - const [_, setScanMode] = useState(null); const [scanning, setScanning] = useState(false); const [uri, setURI] = useState(null); // misc - const captureAction = async (mode: IScanMode) => { - let capturedURI: string; + const captureAction = async (options: IStartScanningOptions) => { + const _functionName: string = 'captureAction'; + let capturedURI: string | null = null; try { - capturedURI = await captureQRCode(mode); + switch (options.mode) { + case ScanModeEnum.Camera: + capturedURI = await captureQRCodeFromCamera(options.videoElement); + break; + case ScanModeEnum.Tab: + capturedURI = await captureQRCodeFromTab(); + break; + default: + break; + } setURI(capturedURI); return stopScanningAction(); } catch (error) { - logger.debug(`${_functionName}(): ${error.message}`); + logger.debug(`${_functionName}: ${error.message}`); } }; const resetAction = () => { setURI(null); - setScanMode(null); stopScanningAction(); }; - const startScanningAction = (mode: IScanMode) => { - setScanMode(mode); + const startScanningAction = (options: IStartScanningOptions) => { setScanning(true); (async () => { - await captureAction(mode); + await captureAction(options); // add a three-second interval that attempts to capture a qr code on the screen setIntervalId( @@ -56,7 +66,7 @@ export default function useCaptureQRCode(): IUseCaptureQrCodeState { } // attempt to capture the qr code - await captureAction(mode); + await captureAction(options); }, QR_CODE_SCAN_INTERVAL) ); })(); diff --git a/src/extension/hooks/useCaptureQRCode/utils/captureQRCode/index.ts b/src/extension/hooks/useCaptureQRCode/utils/captureQRCode/index.ts deleted file mode 100644 index 6dbab566..00000000 --- a/src/extension/hooks/useCaptureQRCode/utils/captureQRCode/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './captureQRCode'; diff --git a/src/extension/hooks/useCaptureQRCode/utils/captureQRCodeFromCamera/captureQRCodeFromCamera.ts b/src/extension/hooks/useCaptureQRCode/utils/captureQRCodeFromCamera/captureQRCodeFromCamera.ts new file mode 100644 index 00000000..9a91a569 --- /dev/null +++ b/src/extension/hooks/useCaptureQRCode/utils/captureQRCodeFromCamera/captureQRCodeFromCamera.ts @@ -0,0 +1,38 @@ +import jsQR, { QRCode } from 'jsqr'; + +// utils +import convertDataUriToImageData from '@extension/utils/convertDataUriToImageData'; + +export default async function captureQRCodeFromCamera( + videoElement: HTMLVideoElement +): Promise { + const canvas: HTMLCanvasElement = document.createElement('canvas'); + const context: CanvasRenderingContext2D | null = canvas.getContext('2d'); + let dataImageUrl: string; + let imageData: ImageData | null; + let result: QRCode | null; + + if (!context) { + throw new Error(`unable get canvas`); + } + + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + + context.drawImage(videoElement, 0, 0, window.innerWidth, window.innerHeight); + + dataImageUrl = canvas.toDataURL('image/png'); + imageData = await convertDataUriToImageData(dataImageUrl); + + if (!imageData) { + throw new Error('unable to get image data for current window'); + } + + result = jsQR(imageData.data, imageData.width, imageData.height); + + if (!result) { + throw new Error(`no qr code found in camera`); + } + + return result.data; +} diff --git a/src/extension/hooks/useCaptureQRCode/utils/captureQRCodeFromCamera/index.ts b/src/extension/hooks/useCaptureQRCode/utils/captureQRCodeFromCamera/index.ts new file mode 100644 index 00000000..968dd05f --- /dev/null +++ b/src/extension/hooks/useCaptureQRCode/utils/captureQRCodeFromCamera/index.ts @@ -0,0 +1 @@ +export { default } from './captureQRCodeFromCamera'; diff --git a/src/extension/hooks/useCaptureQRCode/utils/captureQRCode/captureQRCode.ts b/src/extension/hooks/useCaptureQRCode/utils/captureQRCodeFromTab/captureQRCodeFromTab.ts similarity index 50% rename from src/extension/hooks/useCaptureQRCode/utils/captureQRCode/captureQRCode.ts rename to src/extension/hooks/useCaptureQRCode/utils/captureQRCodeFromTab/captureQRCodeFromTab.ts index 2ee95425..91cf64ef 100644 --- a/src/extension/hooks/useCaptureQRCode/utils/captureQRCode/captureQRCode.ts +++ b/src/extension/hooks/useCaptureQRCode/utils/captureQRCodeFromTab/captureQRCodeFromTab.ts @@ -1,34 +1,21 @@ import jsQR, { QRCode } from 'jsqr'; import browser, { Windows } from 'webextension-polyfill'; -// types -import { IScanMode } from '@extension/hooks/useCaptureQRCode'; - // utils import convertDataUriToImageData from '@extension/utils/convertDataUriToImageData'; -export default async function captureQRCode(mode: IScanMode): Promise { +export default async function captureQRCodeFromTab(): Promise { let dataImageUrl: string; let imageData: ImageData | null; let result: QRCode | null; let windows: Windows.Window[]; - let window: Windows.Window | null = null; + let window: Windows.Window | null; windows = await browser.windows.getAll(); - - switch (mode) { - case 'browserWindow': - window = windows.find((value) => value.type !== 'popup') || null; // get windows that are not the extension window - break; - case 'extensionPopup': - window = windows.find((value) => value.type === 'popup') || null; // get extension window as we will be showing a video from teh webcam - break; - default: - break; - } + window = windows.find((value) => value.type !== 'popup') || null; // get windows that are not the extension window if (!window) { - throw new Error(`unable to find browser window for scan mode "${mode}"`); + throw new Error(`unable to find any browser windows`); } dataImageUrl = await browser.tabs.captureVisibleTab(window.id, { @@ -43,7 +30,7 @@ export default async function captureQRCode(mode: IScanMode): Promise { result = jsQR(imageData.data, imageData.width, imageData.height); if (!result) { - throw new Error(`no qr code found for scan mode "${mode}"`); + throw new Error(`no qr code found on window "${window.title}"`); } return result.data; diff --git a/src/extension/hooks/useCaptureQRCode/utils/captureQRCodeFromTab/index.ts b/src/extension/hooks/useCaptureQRCode/utils/captureQRCodeFromTab/index.ts new file mode 100644 index 00000000..042bc78a --- /dev/null +++ b/src/extension/hooks/useCaptureQRCode/utils/captureQRCodeFromTab/index.ts @@ -0,0 +1 @@ +export { default } from './captureQRCodeFromTab'; diff --git a/src/extension/modals/ScanQRCodeModal/ScanQRCodeModal.tsx b/src/extension/modals/ScanQRCodeModal/ScanQRCodeModal.tsx index d90f14ce..f09eafb6 100644 --- a/src/extension/modals/ScanQRCodeModal/ScanQRCodeModal.tsx +++ b/src/extension/modals/ScanQRCodeModal/ScanQRCodeModal.tsx @@ -2,18 +2,16 @@ import { Modal } from '@chakra-ui/react'; import React, { FC, useState } from 'react'; // components +import ScanQRCodeViaCameraModalContent from '@extension/components/ScanQRCodeViaCameraModalContent'; +import ScanQRCodeViaTabModalContent from '@extension/components/ScanQRCodeViaTabModalContent'; +import ScanQRCodeModalAssetAddContent from './ScanQRCodeModalAssetAddContent'; import ScanQRCodeModalAccountImportContent from './ScanQRCodeModalAccountImportContent'; -import ScanQRCodeModalCameraStreamContent from './ScanQRCodeModalCameraStreamContent'; -import ScanQRCodeModalScanningContent from './ScanQRCodeModalScanningContent'; import ScanQRCodeModalSelectScanModeContent from './ScanQRCodeModalSelectScanModeContent'; import ScanQRCodeModalUnknownURIContent from './ScanQRCodeModalUnknownURIContent'; // enums import { ARC0300AuthorityEnum, ARC0300PathEnum } from '@extension/enums'; -// hooks -import useCaptureQRCode from '@extension/hooks/useCaptureQRCode'; - // selectors import { useSelectLogger, @@ -32,7 +30,6 @@ import type { // utils import parseURIToARC0300Schema from '@extension/utils/parseURIToARC0300Schema'; -import ScanQRCodeModalAssetAddContent from '@extension/modals/ScanQRCodeModal/ScanQRCodeModalAssetAddContent'; interface IProps { onClose: () => void; @@ -43,28 +40,26 @@ const ScanQRCodeModal: FC = ({ onClose }: IProps) => { const logger: ILogger = useSelectLogger(); const networks: INetwork[] = useSelectNetworks(); const isOpen: boolean = useSelectScanQRCodeModal(); - // hooks - const { resetAction, scanning, startScanningAction, uri } = - useCaptureQRCode(); // state - const [showCamera, setShowCamera] = useState(false); + const [scanViaCamera, setScanViaCamera] = useState(false); + const [scanViaTab, setScanViaTab] = useState(false); + const [uri, setURI] = useState(null); + // misc + const reset = () => { + setURI(null); + setScanViaCamera(false); + setScanViaTab(false); + }; // handlers const handleCancelClick = () => handleClose(); const handleClose = () => { - resetAction(); + reset(); onClose(); }; - const handlePreviousClick = () => { - resetAction(); - setShowCamera(false); // close the webcam, if open - }; - const handleScanBrowserWindowClick = () => { - startScanningAction('browserWindow'); - }; - const handleScanUsingCameraClick = async () => { - setShowCamera(true); - startScanningAction('extensionPopup'); - }; + const handleOnURI = (uri: string) => setURI(uri); + const handlePreviousClick = () => reset(); + const handleScanBrowserWindowClick = () => setScanViaTab(true); + const handleScanUsingCameraClick = () => setScanViaCamera(true); // renders const renderContent = () => { let arc0300Schema: IARC0300BaseSchema | null; @@ -115,17 +110,21 @@ const ScanQRCodeModal: FC = ({ onClose }: IProps) => { ); } - if (showCamera) { + if (scanViaCamera) { return ( - ); } - if (scanning) { + if (scanViaTab) { return ( - + ); } @@ -145,7 +144,6 @@ const ScanQRCodeModal: FC = ({ onClose }: IProps) => { onClose={onClose} size="full" scrollBehavior="inside" - useInert={false} // ensure the camera screen can be captured > {renderContent()} diff --git a/src/extension/modals/ScanQRCodeModal/ScanQRCodeModalSelectScanModeContent.tsx b/src/extension/modals/ScanQRCodeModal/ScanQRCodeModalSelectScanModeContent.tsx index ec29acff..8527a904 100644 --- a/src/extension/modals/ScanQRCodeModal/ScanQRCodeModalSelectScanModeContent.tsx +++ b/src/extension/modals/ScanQRCodeModal/ScanQRCodeModalSelectScanModeContent.tsx @@ -23,6 +23,9 @@ import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; // theme import { theme } from '@extension/theme'; +// utils +import isCameraAvailable from '@extension/utils/isCameraAvailable'; + interface IProps { onCancelClick: () => void; onScanBrowserWindowClick: () => void; @@ -82,15 +85,17 @@ const ScanQRCodeModalSelectScanModeContent: FC = ({ {/*scan using camera button*/} - + {isCameraAvailable() && ( + + )} diff --git a/src/extension/modals/WalletConnectModal/WalletConnectModal.tsx b/src/extension/modals/WalletConnectModal/WalletConnectModal.tsx index a1dd3f2d..81efea7b 100644 --- a/src/extension/modals/WalletConnectModal/WalletConnectModal.tsx +++ b/src/extension/modals/WalletConnectModal/WalletConnectModal.tsx @@ -28,6 +28,9 @@ import WalletConnectBannerIcon from '@extension/components/WalletConnectBannerIc // constants import { DEFAULT_GAP } from '@extension/constants'; +// enums +import { ScanModeEnum } from '@extension/enums'; + // hooks import useCaptureQRCode from '@extension/hooks/useCaptureQRCode'; import useColorModeValue from '@extension/hooks/useColorModeValue'; @@ -345,7 +348,9 @@ const WalletConnectModal: FC = ({ onClose }: IProps) => { useEffect(() => { if (isOpen) { - startScanningAction('browserWindow'); + startScanningAction({ + mode: ScanModeEnum.Tab, + }); } }, [isOpen]); diff --git a/src/extension/types/IScanQRCodeModalContentProps.ts b/src/extension/types/IScanQRCodeModalContentProps.ts new file mode 100644 index 00000000..f4ef3bfc --- /dev/null +++ b/src/extension/types/IScanQRCodeModalContentProps.ts @@ -0,0 +1,6 @@ +interface IScanQRCodeModalContentProps { + onPreviousClick: () => void; + onURI: (uri: string) => void; +} + +export default IScanQRCodeModalContentProps; diff --git a/src/extension/types/index.ts b/src/extension/types/index.ts index 9821628f..50c9c775 100644 --- a/src/extension/types/index.ts +++ b/src/extension/types/index.ts @@ -83,6 +83,7 @@ export type { default as IPrivateKey } from './IPrivateKey'; export type { default as IRegistrationRootState } from './IRegistrationRootState'; export type { default as IRejectedActionMeta } from './IRejectedActionMeta'; export type { default as IResourceLanguage } from './IResourceLanguage'; +export type { default as IScanQRCodeModalContentProps } from './IScanQRCodeModalContentProps'; export type { default as ISecuritySettings } from './ISecuritySettings'; export type { default as ISession } from './ISession'; export type { default as ISettings } from './ISettings'; diff --git a/src/extension/utils/isCameraAvailable/index.ts b/src/extension/utils/isCameraAvailable/index.ts new file mode 100644 index 00000000..1a6da9a8 --- /dev/null +++ b/src/extension/utils/isCameraAvailable/index.ts @@ -0,0 +1 @@ +export { default } from './isCameraAvailable'; diff --git a/src/extension/utils/isCameraAvailable/isCameraAvailable.ts b/src/extension/utils/isCameraAvailable/isCameraAvailable.ts new file mode 100644 index 00000000..81d3e78e --- /dev/null +++ b/src/extension/utils/isCameraAvailable/isCameraAvailable.ts @@ -0,0 +1,8 @@ +/** + * Convenience function that simply checks if the device's camera is accessible. + * NOTE: This is not the same as allowed/denied, this is to actually determine if the device has a camera or not. + * @return {boolean} true, if the device has a camera, false otherwise. + */ +export default function isCameraAvailable(): boolean { + return !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia); +}