diff --git a/src/extension/enums/ErrorCodeEnum.ts b/src/extension/enums/ErrorCodeEnum.ts index 1883a2c3..a8b05489 100644 --- a/src/extension/enums/ErrorCodeEnum.ts +++ b/src/extension/enums/ErrorCodeEnum.ts @@ -20,6 +20,9 @@ enum ErrorCodeEnum { // contract (application) InvalidABIContractError = 5000, ReadABIContractError = 5001, + + // devices + CameraError = 6000, } export default ErrorCodeEnum; diff --git a/src/extension/errors/CameraError.ts b/src/extension/errors/CameraError.ts new file mode 100644 index 00000000..55dcada6 --- /dev/null +++ b/src/extension/errors/CameraError.ts @@ -0,0 +1,17 @@ +// enums +import { ErrorCodeEnum } from '../enums'; + +// errors +import BaseExtensionError from './BaseExtensionError'; + +export default class CameraError extends BaseExtensionError { + public readonly code: ErrorCodeEnum = ErrorCodeEnum.CameraError; + public readonly domExceptionType: string; + public readonly name: string = 'CameraError'; + + constructor(domExceptionType: string, message: string) { + super(message); + + this.domExceptionType = domExceptionType; + } +} diff --git a/src/extension/errors/index.ts b/src/extension/errors/index.ts index 0329a31c..64030065 100644 --- a/src/extension/errors/index.ts +++ b/src/extension/errors/index.ts @@ -1,4 +1,5 @@ export { default as BaseExtensionError } from './BaseExtensionError'; +export { default as CameraError } from './CameraError'; export { default as DecryptionError } from './DecryptionError'; export { default as EncryptionError } from './EncryptionError'; export { default as FailedToSendTransactionError } from './FailedToSendTransactionError'; diff --git a/src/extension/hooks/useCaptureQRCode/index.ts b/src/extension/hooks/useCaptureQRCode/index.ts new file mode 100644 index 00000000..6aed33d3 --- /dev/null +++ b/src/extension/hooks/useCaptureQRCode/index.ts @@ -0,0 +1,2 @@ +export { default } from './useCaptureQRCode'; +export * from './types'; diff --git a/src/extension/hooks/useCaptureQRCode/types/IScanMode.ts b/src/extension/hooks/useCaptureQRCode/types/IScanMode.ts new file mode 100644 index 00000000..da6cc2cc --- /dev/null +++ b/src/extension/hooks/useCaptureQRCode/types/IScanMode.ts @@ -0,0 +1,3 @@ +type IScanMode = 'browserWindow' | 'extensionPopup'; + +export default IScanMode; diff --git a/src/extension/hooks/useCaptureQrCode/types/IUseCaptureQrCodeState.ts b/src/extension/hooks/useCaptureQRCode/types/IUseCaptureQrCodeState.ts similarity index 55% rename from src/extension/hooks/useCaptureQrCode/types/IUseCaptureQrCodeState.ts rename to src/extension/hooks/useCaptureQRCode/types/IUseCaptureQrCodeState.ts index 1ed53184..a19782a4 100644 --- a/src/extension/hooks/useCaptureQrCode/types/IUseCaptureQrCodeState.ts +++ b/src/extension/hooks/useCaptureQRCode/types/IUseCaptureQrCodeState.ts @@ -1,6 +1,10 @@ +// types +import IScanMode from './IScanMode'; + interface IUseCaptureQrCodeState { + resetAction: () => void; scanning: boolean; - startScanningAction: () => void; + startScanningAction: (mode: IScanMode) => void; stopScanningAction: () => void; uri: string | null; } diff --git a/src/extension/hooks/useCaptureQrCode/types/index.ts b/src/extension/hooks/useCaptureQRCode/types/index.ts similarity index 59% rename from src/extension/hooks/useCaptureQrCode/types/index.ts rename to src/extension/hooks/useCaptureQRCode/types/index.ts index 89634631..ad6b1832 100644 --- a/src/extension/hooks/useCaptureQrCode/types/index.ts +++ b/src/extension/hooks/useCaptureQRCode/types/index.ts @@ -1 +1,2 @@ +export type { default as IScanMode } from './IScanMode'; export type { default as IUseCaptureQrCodeState } from './IUseCaptureQrCodeState'; diff --git a/src/extension/hooks/useCaptureQrCode/useCaptureQrCode.ts b/src/extension/hooks/useCaptureQRCode/useCaptureQRCode.ts similarity index 58% rename from src/extension/hooks/useCaptureQrCode/useCaptureQrCode.ts rename to src/extension/hooks/useCaptureQRCode/useCaptureQRCode.ts index 22536993..e838dc48 100644 --- a/src/extension/hooks/useCaptureQrCode/useCaptureQrCode.ts +++ b/src/extension/hooks/useCaptureQRCode/useCaptureQRCode.ts @@ -7,39 +7,46 @@ import { QR_CODE_SCAN_INTERVAL } from '@extension/constants'; import { useSelectLogger } from '@extension/selectors'; // types -import { ILogger } from '@common/types'; -import { IUseCaptureQrCodeState } from './types'; +import type { ILogger } from '@common/types'; +import type { IScanMode, IUseCaptureQrCodeState } from './types'; // utils -import { captureQrCode } from './utils'; +import captureQRCode from './utils/captureQRCode'; -export default function useCaptureQrCode(): IUseCaptureQrCodeState { - const _functionName: string = 'useCaptureQrCode'; +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); + const [uri, setURI] = useState(null); // misc - const captureAction: () => Promise = async () => { + const captureAction = async (mode: IScanMode) => { let capturedURI: string; try { - capturedURI = await captureQrCode(); + capturedURI = await captureQRCode(mode); - setUri(capturedURI); + setURI(capturedURI); return stopScanningAction(); } catch (error) { logger.debug(`${_functionName}(): ${error.message}`); } }; - const startScanningAction: () => void = () => { + const resetAction = () => { + setURI(null); + setScanMode(null); + stopScanningAction(); + }; + const startScanningAction = (mode: IScanMode) => { + setScanMode(mode); setScanning(true); (async () => { - await captureAction(); + await captureAction(mode); // add a three-second interval that attempts to capture a qr code on the screen setIntervalId( @@ -49,12 +56,12 @@ export default function useCaptureQrCode(): IUseCaptureQrCodeState { } // attempt to capture the qr code - await captureAction(); + await captureAction(mode); }, QR_CODE_SCAN_INTERVAL) ); })(); }; - const stopScanningAction: () => void = () => { + const stopScanningAction = () => { if (intervalId) { window.clearInterval(intervalId); @@ -65,6 +72,7 @@ export default function useCaptureQrCode(): IUseCaptureQrCodeState { }; return { + resetAction, scanning, startScanningAction, stopScanningAction, diff --git a/src/extension/hooks/useCaptureQrCode/utils/captureQrCode.ts b/src/extension/hooks/useCaptureQRCode/utils/captureQRCode/captureQRCode.ts similarity index 50% rename from src/extension/hooks/useCaptureQrCode/utils/captureQrCode.ts rename to src/extension/hooks/useCaptureQRCode/utils/captureQRCode/captureQRCode.ts index 758c61c4..2ee95425 100644 --- a/src/extension/hooks/useCaptureQrCode/utils/captureQrCode.ts +++ b/src/extension/hooks/useCaptureQRCode/utils/captureQRCode/captureQRCode.ts @@ -1,21 +1,34 @@ 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(): Promise { +export default async function captureQRCode(mode: IScanMode): Promise { let dataImageUrl: string; let imageData: ImageData | null; let result: QRCode | null; let windows: Windows.Window[]; - let window: Windows.Window | null; + let window: Windows.Window | null = null; windows = await browser.windows.getAll(); - window = windows.find((value) => value.type !== 'popup') || null; // get windows that are not popups, i.e. the extension + + 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; + } if (!window) { - throw new Error('unable to find browser window'); + throw new Error(`unable to find browser window for scan mode "${mode}"`); } dataImageUrl = await browser.tabs.captureVisibleTab(window.id, { @@ -30,7 +43,7 @@ export default async function captureQrCode(): Promise { result = jsQR(imageData.data, imageData.width, imageData.height); if (!result) { - throw new Error('no qr code found'); + throw new Error(`no qr code found for scan mode "${mode}"`); } return result.data; diff --git a/src/extension/hooks/useCaptureQRCode/utils/captureQRCode/index.ts b/src/extension/hooks/useCaptureQRCode/utils/captureQRCode/index.ts new file mode 100644 index 00000000..6dbab566 --- /dev/null +++ b/src/extension/hooks/useCaptureQRCode/utils/captureQRCode/index.ts @@ -0,0 +1 @@ +export { default } from './captureQRCode'; diff --git a/src/extension/hooks/useCaptureQrCode/index.ts b/src/extension/hooks/useCaptureQrCode/index.ts deleted file mode 100644 index 1fec1fb3..00000000 --- a/src/extension/hooks/useCaptureQrCode/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { default } from './useCaptureQrCode'; -export * from './types'; -export * from './utils'; diff --git a/src/extension/hooks/useCaptureQrCode/utils/index.ts b/src/extension/hooks/useCaptureQrCode/utils/index.ts deleted file mode 100644 index 000c1f05..00000000 --- a/src/extension/hooks/useCaptureQrCode/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as captureQrCode } from './captureQrCode'; diff --git a/src/extension/modals/ScanQRCodeModal/QRCodeFrameIcon.tsx b/src/extension/modals/ScanQRCodeModal/QRCodeFrameIcon.tsx new file mode 100644 index 00000000..df232835 --- /dev/null +++ b/src/extension/modals/ScanQRCodeModal/QRCodeFrameIcon.tsx @@ -0,0 +1,36 @@ +import { Icon, IconProps } from '@chakra-ui/react'; +import React, { FC } from 'react'; + +const QRCodeFrameIcon: FC = (props: IconProps) => ( + + + + + + + + + +); + +export default QRCodeFrameIcon; diff --git a/src/extension/modals/ScanQRCodeModal/ScanQRCodeModal.tsx b/src/extension/modals/ScanQRCodeModal/ScanQRCodeModal.tsx index a3bcee95..992d5f22 100644 --- a/src/extension/modals/ScanQRCodeModal/ScanQRCodeModal.tsx +++ b/src/extension/modals/ScanQRCodeModal/ScanQRCodeModal.tsx @@ -1,19 +1,18 @@ -import { Modal, ModalContent } from '@chakra-ui/react'; -import React, { FC, useEffect } from 'react'; +import { Modal } from '@chakra-ui/react'; +import React, { FC, useState } from 'react'; // components import ScanQRCodeModalAccountImportContent from './ScanQRCodeModalAccountImportContent'; +import ScanQRCodeModalCameraStreamContent from './ScanQRCodeModalCameraStreamContent'; import ScanQRCodeModalScanningContent from './ScanQRCodeModalScanningContent'; +import ScanQRCodeModalSelectScanModeContent from './ScanQRCodeModalSelectScanModeContent'; import ScanQRCodeModalUnknownURIContent from './ScanQRCodeModalUnknownURIContent'; -// constants -import { BODY_BACKGROUND_COLOR } from '@extension/constants'; - // enums import { ARC0300AuthorityEnum, ARC0300PathEnum } from '@extension/enums'; // hooks -import useCaptureQrCode from '@extension/hooks/useCaptureQrCode'; +import useCaptureQRCode from '@extension/hooks/useCaptureQRCode'; // selectors import { @@ -21,9 +20,6 @@ import { useSelectScanQRCodeModal, } from '@extension/selectors'; -// theme -import { theme } from '@extension/theme'; - // types import type { ILogger } from '@common/types'; import type { @@ -43,25 +39,31 @@ const ScanQRCodeModal: FC = ({ onClose }: IProps) => { const logger: ILogger = useSelectLogger(); const isOpen: boolean = useSelectScanQRCodeModal(); // hooks - const { scanning, startScanningAction, stopScanningAction, uri } = - useCaptureQrCode(); + const { resetAction, scanning, startScanningAction, uri } = + useCaptureQRCode(); + // state + const [showCamera, setShowCamera] = useState(false); // handlers const handleCancelClick = () => handleClose(); const handleClose = () => { - stopScanningAction(); + resetAction(); onClose(); }; - const handleRetryScan = () => startScanningAction(); + const handlePreviousClick = () => { + resetAction(); + setShowCamera(false); // close the webcam, if open + }; + const handleScanBrowserWindowClick = () => { + startScanningAction('browserWindow'); + }; + const handleScanUsingCameraClick = async () => { + setShowCamera(true); + startScanningAction('extensionPopup'); + }; // renders const renderContent = () => { let arc0300Schema: IARC0300BaseSchema | null; - if (scanning) { - return ( - - ); - } - if (uri) { arc0300Schema = parseURIToARC0300Schema(uri, { logger }); @@ -71,8 +73,8 @@ const ScanQRCodeModal: FC = ({ onClose }: IProps) => { if (arc0300Schema.paths[0] === ARC0300PathEnum.Import) { return ( ); @@ -83,23 +85,39 @@ const ScanQRCodeModal: FC = ({ onClose }: IProps) => { break; } } + + // if the uri cannot be parsed + return ( + + ); + } + + if (showCamera) { + return ( + + ); + } + + if (scanning) { + return ( + + ); } return ( - ); }; - useEffect(() => { - if (isOpen) { - startScanningAction(); - } - }, [isOpen]); - return ( = ({ onClose }: IProps) => { onClose={onClose} size="full" scrollBehavior="inside" + useInert={false} // ensure the camera screen can be captured > - - {renderContent()} - + {renderContent()} ); }; diff --git a/src/extension/modals/ScanQRCodeModal/ScanQRCodeModalAccountImportContent.tsx b/src/extension/modals/ScanQRCodeModal/ScanQRCodeModalAccountImportContent.tsx index 04443ac6..e65f06ff 100644 --- a/src/extension/modals/ScanQRCodeModal/ScanQRCodeModalAccountImportContent.tsx +++ b/src/extension/modals/ScanQRCodeModal/ScanQRCodeModalAccountImportContent.tsx @@ -2,6 +2,7 @@ import { Heading, HStack, ModalBody, + ModalContent, ModalFooter, ModalHeader, Text, @@ -16,6 +17,7 @@ import React, { useState, } from 'react'; import { useTranslation } from 'react-i18next'; +import { IoArrowBackOutline } from 'react-icons/io5'; import { useDispatch } from 'react-redux'; import { Location, @@ -38,7 +40,11 @@ import PasswordInput, { } from '@extension/components/PasswordInput'; // constants -import { ACCOUNTS_ROUTE, DEFAULT_GAP } from '@extension/constants'; +import { + ACCOUNTS_ROUTE, + BODY_BACKGROUND_COLOR, + DEFAULT_GAP, +} from '@extension/constants'; // enums import { ErrorCodeEnum } from '@extension/enums'; @@ -81,16 +87,17 @@ import type { import convertPrivateKeyToAddress from '@extension/utils/convertPrivateKeyToAddress'; import ellipseAddress from '@extension/utils/ellipseAddress'; import decodePrivateKeyFromAccountImportSchema from './utils/decodePrivateKeyFromImportKeySchema'; +import { theme } from '@extension/theme'; interface IProps { - onCancelClick: () => void; onComplete: () => void; + onPreviousClick: () => void; schema: IARC0300AccountImportSchema; } const ScanQRCodeModalAccountImportContent: FC = ({ - onCancelClick, onComplete, + onPreviousClick, schema, }: IProps) => { const { t } = useTranslation(); @@ -125,9 +132,9 @@ const ScanQRCodeModalAccountImportContent: FC = ({ const [address, setAddress] = useState(null); const [saving, setSaving] = useState(false); // handlers - const handleCancelClick = () => { + const handlePreviousClick = () => { reset(); - onCancelClick(); + onPreviousClick(); }; const handleImportClick = async () => { const _functionName: string = 'handleImportClick'; @@ -318,7 +325,11 @@ const ScanQRCodeModalAccountImportContent: FC = ({ }, []); return ( - <> + {/*header*/} @@ -416,14 +427,15 @@ const ScanQRCodeModalAccountImportContent: FC = ({ )} - {/*cancel button*/} + {/*previous button*/} {/*import button*/} @@ -439,7 +451,7 @@ const ScanQRCodeModalAccountImportContent: FC = ({ - + ); }; diff --git a/src/extension/modals/ScanQRCodeModal/ScanQRCodeModalCameraStreamContent.tsx b/src/extension/modals/ScanQRCodeModal/ScanQRCodeModalCameraStreamContent.tsx new file mode 100644 index 00000000..ac867f38 --- /dev/null +++ b/src/extension/modals/ScanQRCodeModal/ScanQRCodeModalCameraStreamContent.tsx @@ -0,0 +1,288 @@ +import { + Heading, + Icon, + Link, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, + Spinner, + Text, + VStack, +} from '@chakra-ui/react'; +import React, { + FC, + MutableRefObject, + useEffect, + useRef, + useState, +} from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { + IoAlertCircleOutline, + IoArrowBackOutline, + IoBanOutline, +} from 'react-icons/io5'; + +// components +import Button from '@extension/components/Button'; +import QRCodeFrameIcon from './QRCodeFrameIcon'; + +// constants +import { + BODY_BACKGROUND_COLOR, + DEFAULT_GAP, + SUPPORT_MAIL_TO_LINK, +} from '@extension/constants'; + +// errors +import { BaseExtensionError, CameraError } from '@extension/errors'; + +// hooks +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; +} + +const ScanQRCodeModalCameraStreamContent: FC = ({ + onPreviousClick, +}: IProps) => { + const { t } = useTranslation(); + const videoRef: MutableRefObject = + useRef(null); + // selectors + const logger: ILogger = useSelectLogger(); + // hooks + 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); + } + }; + // handlers + const handlePreviousClick = () => { + // stop the camera stream + if (stream) { + stream.getTracks().forEach((value) => value.stop()); + } + + setError(null); + setLoading(false); + setStream(null); + setNotAllowed(false); + + onPreviousClick(); + }; + // renders + const renderBody = () => { + if (stream) { + return ; + } + + // show a general error page + if (error) { + return ( + <> + {/*icon*/} + + + {/*heading*/} + + {t('errors.titles.code', { context: error.code })} + + + {/*description*/} + + {t('errors.descriptions.code', { context: error.code })} + + + + + Please{' '} + + contact us + {' '} + for further assistance so we can resolve this issue for you. + + + + ); + } + + if (notAllowed) { + return ( + <> + {/*icon*/} + + + {/*captions*/} + + {t('captions.cameraQRCodeScanNotAllowed1')} + + + {t('captions.cameraQRCodeScanNotAllowed2')} + + + ); + } + + return ( + <> + {/*loader*/} + + + {/*caption*/} + + {t('captions.loadingCameraStream')} + + + ); + }; + + useEffect(() => { + (async () => await startStreaming())(); + }, []); + useEffect(() => { + if (stream && videoRef.current) { + videoRef.current.srcObject = stream; + videoRef.current.play(); + } + }, [stream]); + + return ( + + {/*video element*/} + + ); +}; + +export default ScanQRCodeModalCameraStreamContent; diff --git a/src/extension/modals/ScanQRCodeModal/ScanQRCodeModalScanningContent.tsx b/src/extension/modals/ScanQRCodeModal/ScanQRCodeModalScanningContent.tsx index 4b8d4ea6..3ef92069 100644 --- a/src/extension/modals/ScanQRCodeModal/ScanQRCodeModalScanningContent.tsx +++ b/src/extension/modals/ScanQRCodeModal/ScanQRCodeModalScanningContent.tsx @@ -1,6 +1,7 @@ import { Heading, ModalBody, + ModalContent, ModalFooter, ModalHeader, Spinner, @@ -9,12 +10,13 @@ import { } from '@chakra-ui/react'; import React, { FC } from 'react'; import { useTranslation } from 'react-i18next'; +import { IoArrowBackOutline } from 'react-icons/io5'; // components import Button from '@extension/components/Button'; // constants -import { DEFAULT_GAP } from '@extension/constants'; +import { BODY_BACKGROUND_COLOR, DEFAULT_GAP } from '@extension/constants'; // hooks import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; @@ -24,11 +26,11 @@ import useColorModeValue from '@extension/hooks/useColorModeValue'; import { theme } from '@extension/theme'; interface IProps { - onCancelClick: () => void; + onPreviousClick: () => void; } const ScanQRCodeModalScanningContent: FC = ({ - onCancelClick, + onPreviousClick, }: IProps) => { const { t } = useTranslation(); // hooks @@ -38,10 +40,14 @@ const ScanQRCodeModalScanningContent: FC = ({ theme.colors.primaryDark['500'] ); // handlers - const handleCancelClick = () => onCancelClick(); + const handlePreviousClick = () => onPreviousClick(); return ( - <> + {/*header*/} @@ -74,11 +80,18 @@ const ScanQRCodeModalScanningContent: FC = ({ {/*footer*/} - - + ); }; diff --git a/src/extension/modals/ScanQRCodeModal/ScanQRCodeModalSelectScanModeContent.tsx b/src/extension/modals/ScanQRCodeModal/ScanQRCodeModalSelectScanModeContent.tsx new file mode 100644 index 00000000..ec29acff --- /dev/null +++ b/src/extension/modals/ScanQRCodeModal/ScanQRCodeModalSelectScanModeContent.tsx @@ -0,0 +1,114 @@ +import { + Heading, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, + Text, + VStack, +} from '@chakra-ui/react'; +import React, { FC } from 'react'; +import { useTranslation } from 'react-i18next'; +import { IoBrowsersOutline, IoVideocamOutline } from 'react-icons/io5'; + +// components +import Button from '@extension/components/Button'; + +// constants +import { BODY_BACKGROUND_COLOR, DEFAULT_GAP } from '@extension/constants'; + +// hooks +import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; + +// theme +import { theme } from '@extension/theme'; + +interface IProps { + onCancelClick: () => void; + onScanBrowserWindowClick: () => void; + onScanUsingCameraClick: () => void; +} + +const ScanQRCodeModalSelectScanModeContent: FC = ({ + onCancelClick, + onScanBrowserWindowClick, + onScanUsingCameraClick, +}: IProps) => { + const { t } = useTranslation(); + // hooks + const defaultTextColor: string = useDefaultTextColor(); + // handlers + const handleCancelClick = () => onCancelClick(); + const handleScanUsingCameraClick = () => onScanUsingCameraClick(); + const handleScanBrowserWindowClick = () => onScanBrowserWindowClick(); + + return ( + + {/*header*/} + + + {t('headings.scanQrCode')} + + + + {/*body*/} + + + {/*caption*/} + + {t('captions.selectScanLocation')} + + + + {/*scan browser window*/} + + + {/*scan using camera button*/} + + + + + + {/*footer*/} + + {/*cancel button*/} + + + + ); +}; + +export default ScanQRCodeModalSelectScanModeContent; diff --git a/src/extension/modals/ScanQRCodeModal/ScanQRCodeModalUnknownURIContent.tsx b/src/extension/modals/ScanQRCodeModal/ScanQRCodeModalUnknownURIContent.tsx index 389b1cfa..8df0bbc2 100644 --- a/src/extension/modals/ScanQRCodeModal/ScanQRCodeModalUnknownURIContent.tsx +++ b/src/extension/modals/ScanQRCodeModal/ScanQRCodeModalUnknownURIContent.tsx @@ -1,46 +1,50 @@ import { Heading, - HStack, ModalBody, + ModalContent, ModalFooter, ModalHeader, - Spinner, Text, VStack, } from '@chakra-ui/react'; import React, { FC } from 'react'; import { useTranslation } from 'react-i18next'; +import { IoArrowBackOutline } from 'react-icons/io5'; // components import Button from '@extension/components/Button'; import ModalTextItem from '@extension/components/ModalTextItem'; // constants -import { DEFAULT_GAP } from '@extension/constants'; +import { BODY_BACKGROUND_COLOR, DEFAULT_GAP } from '@extension/constants'; // hooks import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; +// theme +import { theme } from '@extension/theme'; + interface IProps { - onCancelClick: () => void; - onTryAgainClick: () => void; - uri: string | null; + onPreviousClick: () => void; + uri: string; } const ScanQRCodeModalUnknownURIContent: FC = ({ - onCancelClick, - onTryAgainClick, + onPreviousClick, uri, }: IProps) => { const { t } = useTranslation(); // hooks const defaultTextColor: string = useDefaultTextColor(); // handlers - const handleCancelClick = () => onCancelClick(); - const handleTryAgainClick = () => onTryAgainClick(); + const handlePreviousClick = () => onPreviousClick(); return ( - <> + {/*header*/} @@ -51,45 +55,34 @@ const ScanQRCodeModalUnknownURIContent: FC = ({ {/*body*/} + {/*caption*/} {t('captions.unknownQRCode')} - {uri && ( - ('labels.value')}:`} - value={uri} - /> - )} + {/*value*/} + ('labels.value')}:`} + value={uri} + /> {/*footer*/} - - {/*cancel button*/} - - - {/*try again button*/} - - + {/*previous button*/} + - + ); }; diff --git a/src/extension/modals/WalletConnectModal/WalletConnectModal.tsx b/src/extension/modals/WalletConnectModal/WalletConnectModal.tsx index b5357c82..a1dd3f2d 100644 --- a/src/extension/modals/WalletConnectModal/WalletConnectModal.tsx +++ b/src/extension/modals/WalletConnectModal/WalletConnectModal.tsx @@ -29,7 +29,7 @@ import WalletConnectBannerIcon from '@extension/components/WalletConnectBannerIc import { DEFAULT_GAP } from '@extension/constants'; // hooks -import useCaptureQrCode from '@extension/hooks/useCaptureQrCode'; +import useCaptureQRCode from '@extension/hooks/useCaptureQRCode'; import useColorModeValue from '@extension/hooks/useColorModeValue'; import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; import usePrimaryColorScheme from '@extension/hooks/usePrimaryColorScheme'; @@ -71,7 +71,7 @@ const WalletConnectModal: FC = ({ onClose }: IProps) => { const isOpen: boolean = useSelectWalletConnectModalOpen(); // hooks const { scanning, startScanningAction, stopScanningAction, uri } = - useCaptureQrCode(); + useCaptureQRCode(); const defaultTextColor: string = useDefaultTextColor(); const primaryColor: string = useColorModeValue( theme.colors.primaryLight['500'], @@ -345,7 +345,7 @@ const WalletConnectModal: FC = ({ onClose }: IProps) => { useEffect(() => { if (isOpen) { - startScanningAction(); + startScanningAction('browserWindow'); } }, [isOpen]); diff --git a/src/extension/translations/en.ts b/src/extension/translations/en.ts index 89fab88d..0f6a5789 100644 --- a/src/extension/translations/en.ts +++ b/src/extension/translations/en.ts @@ -30,6 +30,8 @@ const translation: IResourceLanguage = { removeAllSessions: 'Remove All Sessions', reset: 'Reset', save: 'Save', + scanBrowserWindow: 'Scan Browser Window', + scanUsingCamera: 'Scan Using Camera', send: 'Send', sign: 'Sign', tryAgain: 'Try Again', @@ -60,6 +62,9 @@ const translation: IResourceLanguage = { 'This transaction will update the application, replacing the approval and clear programs. The application ID will not be changed.', audienceDoesNotMatch: 'The intended recipient of this token, does not match the host', + cameraQRCodeScanNotAllowed1: 'Camera access has been denied.', + cameraQRCodeScanNotAllowed2: + 'You will need to go into your settings and allow access.', changePassword1: 'Enter your new password.', changePassword2: 'You will be prompted to enter your current password when you press "Change Password".', @@ -72,6 +77,8 @@ const translation: IResourceLanguage = { createPassword1: `First, let's create a new password to secure this device.`, createPassword2: 'This password will be used to encrypt your private keys, so make it strong!', + debugLogging: + 'Debugging information will be output to the extension console.', defaultConfirm: 'Are you sure?', deleteApplication: 'Be careful, deleting an application is irreversible!', destroyAsset: 'Be careful, destroying an asset is irreversible!', @@ -101,8 +108,7 @@ const translation: IResourceLanguage = { initializingWalletConnect: 'Putting the final touches into your WalletConnect interface.', invalidAlgorithm: `The suggested signing method does not match the method that will be used to sign this token`, - debugLogging: - 'Debugging information will be output to the extension console.', + loadingCameraStream: 'Loading your camera stream.', managerAddressDoesNotMatch: 'This account does not have the authority to alter this asset. This transaction will likely fail.', maximumNativeCurrencyTransactionAmount: @@ -152,6 +158,7 @@ const translation: IResourceLanguage = { saveMnemonicPhrase2: `Make sure you save this in a secure place.`, scanningForQrCode: 'Scanning for a QR Code. Make sure the QR code is visible in the background.', + selectScanLocation: 'Choose how you would like to scan the QR code.', securityTokenExpired: 'This token has expired', signJwtRequest: 'An application is requesting to sign a security token.', signMessageRequest: 'An application is requesting to sign a message.', @@ -171,10 +178,11 @@ const translation: IResourceLanguage = { }, errors: { descriptions: { - code: `Please contact support with code {{code}} and describe what happened.`, + code: `Please contact support with code "{{code}}" and describe what happened.`, code_1002: `Failed to parse the "{{type}}" data.`, code_2000: 'The password seems to be invalid.', code_2003: 'This account already exists.', + code_6000: 'There was an error starting the camera.', }, inputs: { copySeedPhraseRequired: @@ -189,9 +197,10 @@ const translation: IResourceLanguage = { }, titles: { code: 'Well This Is Embarrassing...', - code_1002: 'Parsing Error', - code_2000: 'Invalid Password', - code_2003: 'Account Already Exists', + code_1002: '1002 Parsing Error', + code_2000: '2000 Invalid Password', + code_2003: '2003 Account Already Exists', + code_6000: '6000 Camera Error', }, }, headings: { @@ -201,6 +210,8 @@ const translation: IResourceLanguage = { allowMainNetConfirm: 'Allow MainNet Networks', authentication: 'Authentication', beta: 'Beta', + cameraDenied: 'Camera Denied', + cameraLoading: 'Camera Loading', comingSoon: 'Coming Soon!', confirm: 'Confirm', createNewAccount: 'Create A New Account', @@ -228,6 +239,7 @@ const translation: IResourceLanguage = { removeAccount: 'Remove Account', removeAllSessions: 'Remove All Sessions', scanningForQRCode: 'Scanning For QR Code', + scanQrCode: 'Scan QR Code', sendAsset: 'Send {{asset}}', shareAddress: 'Share Address', transaction: 'Unknown Transaction 💀',