Skip to content

Commit

Permalink
feat(suite): add 'paste' mode to qr reader
Browse files Browse the repository at this point in the history
  • Loading branch information
marekrjpolak committed Jan 10, 2022
1 parent 449ec6e commit 329a149
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const Options = styled.div`
`;

const Option = styled.div<{ isSelected: boolean }>`
white-space: nowrap;
padding: 0 14px;
margin: 2px 0;
padding-top: 1px;
Expand Down
3 changes: 2 additions & 1 deletion packages/suite/src/actions/suite/modalActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import { createDeferred, Deferred, DeferredResponse } from '@suite-utils/deferre
export type UserContextPayload =
| {
type: 'qr-reader';
decision: Deferred<{ address: string; amount?: string }>;
decision: Deferred<string>;
allowPaste?: boolean;
}
| {
type: 'unverified-address';
Expand Down
154 changes: 102 additions & 52 deletions packages/suite/src/components/suite/modals/QrScanner/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,33 @@ import styled from 'styled-components';

import { TrezorLink, Translation, Modal, BundleLoader } from '@suite-components';
import * as URLS from '@suite-constants/urls';
import { parseQuery } from '@suite-utils/parseUri';
import { Icon, colors, P } from '@trezor/components';
import { Icon, colors, P, Button, Textarea, SelectBar } from '@trezor/components';
import { UserContextPayload } from '@suite-actions/modalActions';
import { useTranslation } from '@suite-hooks';

const QrReader = lazy(() => import(/* webpackChunkName: "react-qr-reader" */ 'react-qr-reader'));

const Description = styled.div`
const DescriptionWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
margin: 0 16px;
`;

const Description = styled.div`
text-align: left;
margin-left: 16px;
& * + * {
margin-left: 8px;
}
`;

const CameraPlaceholderWrapper = styled.div<{ show: boolean }>`
const ContentWrapper = styled.div<{ show: boolean }>`
display: ${props => (props.show ? 'flex' : 'none')};
margin: 12px 0px 20px 0px;
width: 100%;
flex-direction: column;
margin: 16px;
overflow: hidden;
height: 380px;
`;

const CameraPlaceholder = styled.div`
Expand All @@ -29,8 +40,8 @@ const CameraPlaceholder = styled.div`
text-align: center;
flex: 1;
padding: 40px;
height: 320px;
border-radius: 3px;
height: 100%;
border-radius: 16px;
background: ${props => props.theme.BG_GREY};
`;

Expand All @@ -56,22 +67,49 @@ const IconWrapper = styled.div`

const Actions = styled.div`
display: flex;
margin: 12px 0;
justify-content: center;
& > * {
min-width: 200px;
}
`;

const StyledQrReader = styled(QrReader)`
width: 100%;
height: 100%;
position: relative;
& > section {
position: initial !important;
padding-top: initial !important;
& > video {
border-radius: 16px;
}
}
`;

const StyledTextarea = styled(Textarea)`
height: 100%;
& > textarea {
flex: 1;
}
`;

type Props = {
type Props = Omit<Extract<UserContextPayload, { type: 'qr-reader' }>, 'type'> & {
onCancel: () => void;
decision: Extract<UserContextPayload, { type: 'qr-reader' }>['decision'];
};

interface State {
readerLoaded: boolean;
error: JSX.Element | null;
}

const QrScanner = ({ onCancel, decision }: Props) => {
const QrScanner = ({ onCancel, decision, allowPaste }: Props) => {
const [readerLoaded, setReaderLoaded] = useState<State['readerLoaded']>(false);
const [error, setError] = useState<State['error']>(null);
const [isPasteMode, setPasteMode] = useState(false);
const [text, setText] = useState('');

const { translationString } = useTranslation();

const onLoad = () => {
setReaderLoaded(true);
Expand All @@ -95,17 +133,9 @@ const QrScanner = ({ onCancel, decision }: Props) => {
const handleScan = (uri: string | null) => {
if (uri) {
try {
const query = parseQuery(uri);
if (query) {
decision.resolve({
uri,
...query,
});
setReaderLoaded(true);
onCancel();
} else {
handleError({ name: 'unknown' });
}
decision.resolve(uri);
setReaderLoaded(true);
onCancel();
} catch (error) {
handleError(error);
}
Expand All @@ -114,30 +144,62 @@ const QrScanner = ({ onCancel, decision }: Props) => {

return (
<Modal
noPadding
cancelable
onCancel={onCancel}
heading={<Translation id="TR_SCAN_QR_CODE" />}
heading={<Translation id={isPasteMode ? 'TR_PASTE_URI' : 'TR_SCAN_QR_CODE'} />}
description={
<Description>
<Translation id="TR_FOR_EASIER_AND_SAFER_INPUT" />
<TrezorLink icon="EXTERNAL_LINK" size="small" href={URLS.WIKI_QR_CODE}>
<Translation id="TR_LEARN_MORE" />
</TrezorLink>
</Description>
<DescriptionWrapper>
{allowPaste && (
<SelectBar
options={[
{ label: <Translation id="TR_PASTE_URI" />, value: 'paste' },
{ label: <Translation id="TR_QR_CODE" />, value: 'scan' },
]}
selectedOption={isPasteMode ? 'paste' : 'scan'}
onChange={value => setPasteMode(value === 'paste')}
/>
)}
{!isPasteMode && (
<Description>
<Translation id="TR_FOR_EASIER_AND_SAFER_INPUT" />
<TrezorLink icon="EXTERNAL_LINK" size="small" href={URLS.WIKI_QR_CODE}>
<Translation id="TR_LEARN_MORE" />
</TrezorLink>
</Description>
)}
</DescriptionWrapper>
}
>
{!readerLoaded && !error && (
<CameraPlaceholderWrapper show>
{isPasteMode && (
<ContentWrapper show>
<StyledTextarea
noTopLabel
placeholder={`${translationString('TR_PASTE_URI')}…`}
onChange={e => {
setText(e.target.value);
}}
/>
<Actions>
<Button isDisabled={!text} onClick={() => handleScan(text)}>
<Translation id="TR_CONFIRM" />
</Button>
</Actions>
</ContentWrapper>
)}

{!isPasteMode && !readerLoaded && !error && (
<ContentWrapper show>
<CameraPlaceholder>
<IconWrapper>
<Icon icon="QR" size={100} />
</IconWrapper>
<Translation id="TR_PLEASE_ALLOW_YOUR_CAMERA" />
</CameraPlaceholder>
</CameraPlaceholderWrapper>
</ContentWrapper>
)}
{error && (
<CameraPlaceholderWrapper show>
{!isPasteMode && error && (
<ContentWrapper show>
<CameraPlaceholder>
<Error>
<ErrorTitle>
Expand All @@ -146,34 +208,22 @@ const QrScanner = ({ onCancel, decision }: Props) => {
<ErrorMessage>{error}</ErrorMessage>
</Error>
</CameraPlaceholder>
</CameraPlaceholderWrapper>
</ContentWrapper>
)}

{!error && (
<CameraPlaceholderWrapper show={readerLoaded}>
{!isPasteMode && !error && (
<ContentWrapper show={readerLoaded}>
<Suspense fallback={<BundleLoader />}>
<QrReader
<StyledQrReader
delay={500}
onError={handleError}
onScan={handleScan}
onLoad={onLoad}
style={{ width: '100%', borderRadius: '3px' }}
showViewFinder={false}
/>
</Suspense>
</CameraPlaceholderWrapper>
</ContentWrapper>
)}

<Actions>
{/* <Button
variant="secondary"
onClick={() => {
// TODO: enable legacyMode and call openImageDialog? https://github.com/JodusNodus/react-qr-reader#readme
}}
>
<Translation id="TR_UPLOAD_IMAGE" />
</Button> */}
</Actions>
</Modal>
);
};
Expand Down
8 changes: 7 additions & 1 deletion packages/suite/src/components/suite/modals/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,13 @@ const getUserContextModal = ({ modal, onCancel }: SharedProps) => {
case 'wipe-device':
return <WipeDevice onCancel={onCancel} />;
case 'qr-reader':
return <QrScanner decision={payload.decision} onCancel={onCancel} />;
return (
<QrScanner
decision={payload.decision}
allowPaste={payload.allowPaste}
onCancel={onCancel}
/>
);
case 'transaction-detail':
return <TransactionDetail {...payload} onCancel={onCancel} />;
case 'passphrase-duplicate':
Expand Down
8 changes: 8 additions & 0 deletions packages/suite/src/support/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2804,11 +2804,19 @@ export default defineMessages({
defaultMessage: 'Retrying..',
id: 'TR_RETRYING_DOT_DOT',
},
TR_QR_CODE: {
defaultMessage: 'QR code',
id: 'TR_QR_CODE',
},
TR_SCAN_QR_CODE: {
defaultMessage: 'Scan QR code',
description: 'Title for the Scan QR modal dialog',
id: 'TR_SCAN_QR_CODE',
},
TR_PASTE_URI: {
defaultMessage: 'Paste URI',
id: 'TR_PASTE_URI',
},
TR_SECURITY_HEADING: {
defaultMessage: 'Your wallet is almost ready',
description: 'Heading in security page',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { InputError } from '@wallet-components';
import { scanQrRequest } from '@wallet-actions/sendFormActions';
import { useActions, useDevice } from '@suite-hooks';
import { useSendFormContext } from '@wallet-hooks';
import { getProtocolInfo } from '@suite-utils/parseUri';
import {
isAddressValid,
isAddressDeprecated,
Expand All @@ -15,8 +16,10 @@ import {
} from '@wallet-utils/validation';
import { getInputState } from '@wallet-utils/sendFormUtils';
import { MAX_LENGTH } from '@suite-constants/inputs';
import { PROTOCOL_SCHEME } from '@suite-constants/protocol';
import ConvertAddress from './components/Convert';
import { Output } from '@wallet-types/sendForm';
import type { Account } from '@wallet-types';
import type { Output } from '@wallet-types/sendForm';

const Label = styled.div`
display: flex;
Expand Down Expand Up @@ -48,6 +51,14 @@ interface Props {
output: Partial<Output>;
}

const parseQrData = (uri: string, symbol: Account['symbol']) => {
const protocol = getProtocolInfo(uri);
if (protocol?.scheme === PROTOCOL_SCHEME.BITCOIN)
return { address: protocol.address, amount: protocol.amount };
if (isAddressValid(uri, symbol)) return { address: uri };
return {};
};

const Address = ({ output, outputId, outputsCount }: Props) => {
const theme = useTheme();
const { device } = useDevice();
Expand Down Expand Up @@ -91,11 +102,13 @@ const Address = ({ output, outputId, outputsCount }: Props) => {
variant="tertiary"
icon="QR"
onClick={async () => {
const result = await openQrModal();
if (result) {
setValue(inputName, result.address, { shouldValidate: true });
if (result.amount) {
setValue(`outputs[${outputId}].amount`, result.amount, {
const uri = await openQrModal();
if (!uri) return;
const { address, amount } = parseQrData(uri, symbol);
if (address) {
setValue(inputName, address, { shouldValidate: true });
if (amount) {
setValue(`outputs[${outputId}].amount`, amount, {
shouldValidate: true,
});
// if amount is set compose by amount
Expand Down

0 comments on commit 329a149

Please sign in to comment.