Skip to content

Commit

Permalink
feat(suite): ⚠️ add ThpPairingModal
Browse files Browse the repository at this point in the history
  • Loading branch information
szymonlesisz committed Feb 27, 2025
1 parent 027471d commit a2a5a63
Show file tree
Hide file tree
Showing 5 changed files with 269 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const DeviceAcquire = () => {

const ctaButton = (
<Button data-testid="@device-acquire" isLoading={isDeviceLocked} onClick={handleClick}>
<Translation id="TR_TRY_AGAIN" />
Pair Trezor with suite
</Button>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
PassphraseOnDeviceModal,
PinInvalidModal,
PinModal,
ThpPairingModal,
TransactionReviewModal,
WordAdvancedModal,
WordModal,
Expand Down Expand Up @@ -47,6 +48,9 @@ export const DeviceContextModal = ({
case UI.REQUEST_PASSPHRASE:
return <PassphraseModal device={device} />;

case UI.REQUEST_THP_PAIRING:
return <ThpPairingModal device={device} />;

case 'WordRequestType_Plain':
return <WordModal renderer={renderer} />;
case 'WordRequestType_Matrix6':
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
import { ChangeEvent, Suspense, lazy, useRef, useState } from 'react';

import styled from 'styled-components';

import { Button, Icon, IconButton, Input, Paragraph } from '@trezor/components';
import TrezorConnect from '@trezor/connect';

import { BundleLoader, Modal, Translation } from 'src/components/suite';
import type { TrezorDevice } from 'src/types/suite';

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

const StyledModal = styled(Modal)`
width: 460px;
`;

const ContentWrapper = styled.div`
flex-direction: column;
overflow: hidden;
height: 380px;
`;

const CameraPlaceholder = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
flex: 1;
padding: 40px;
height: 240px;
border-radius: 16px;
background: ${({ theme }) => theme.BG_GREY};
`;

const ErrorWraper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
padding: 10px 0;
`;

const ErrorMessage = styled.span`
text-align: center;
color: ${({ theme }) => theme.TYPE_DARK_GREY};
`;

const IconWrapper = styled.div`
margin-bottom: 40px;
`;

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

const InputWrapper = styled.div`
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 24px;
`;

const CameraWrapper = styled.div`
position: relative;
height: 240px;
`;

interface ThpPairingModalModalProps {
device: TrezorDevice;
}

/**
* @param {ThpPairingModalModalProps}
*/
export const ThpPairingModal = (_: ThpPairingModalModalProps) => {
const [isLoading, setLoading] = useState(false);
const [mode, setMode] = useState<'qr-code' | 'code-entry' | 'loading'>('code-entry');
const [codeEntry, setCodeEntry] = useState('');
const codeEntryInputRef = useRef<HTMLInputElement>(null);

const useQrCode = () => {
setMode('qr-code');
TrezorConnect.uiResponse({
type: 'ui-receive_thp_pairing_tag',
payload: {
selectedMethod: 3,
},
});
};
const onQrCode = (tag: string) => {
setLoading(true);
setMode('loading');
TrezorConnect.uiResponse({
type: 'ui-receive_thp_pairing_tag',
payload: {
source: 'qr-code',
tag,
},
});
};

const useCodeEntry = () => {
setMode('code-entry');
TrezorConnect.uiResponse({
type: 'ui-receive_thp_pairing_tag',
payload: {
selectedMethod: 2,
},
});
};
const onCodeEntry = (tag: string) => {
setLoading(true);
TrezorConnect.uiResponse({
type: 'ui-receive_thp_pairing_tag',
payload: {
source: 'code-entry',
tag,
},
});
};

const onInputChange = (e: ChangeEvent<HTMLInputElement>) => {
const pairingCode = e.target.value;
setCodeEntry(pairingCode);
if (pairingCode.length >= 6) {
onCodeEntry(pairingCode);
}
};

const [readerLoaded, setReaderLoaded] = useState(false);
const [error, setError] = useState<JSX.Element | null>(null);

const onLoad = () => {
setReaderLoaded(true);
};

const handleError = (err: any) => {
if (
err.name === 'NotAllowedError' ||
err.name === 'PermissionDeniedError' ||
err.name === 'NotReadableError' ||
err.name === 'TrackStartError'
) {
setError(<Translation id="TR_CAMERA_PERMISSION_DENIED" />);
} else if (err.name === 'NotFoundError' || err.name === 'DevicesNotFoundError') {
setError(<Translation id="TR_CAMERA_NOT_RECOGNIZED" />);
} else {
setError(<Translation id="TR_UNKNOWN_ERROR_SEE_CONSOLE" />);
}
};

const handleScan = (uri: string | null) => {
console.warn('handleScan', uri);
if (uri) {
onQrCode(uri);
}
};

// useEffect(() => {
// navigator.mediaDevices
// .getUserMedia({ video: true })
// .then(() => {
// console.warn('CAMERA ALLOWED!');
// })
// .catch(_e => {
// console.warn('CameraError', _e);
// });
// }, []);

// console.warn('camera?', navigator.mediaDevices.getUserMedia({ video: true }));

return (
<StyledModal heading="Pairing with Trezor" data-test="@modal/thp-paring">
<ContentWrapper>
{mode === 'code-entry' && (
<>
<InputWrapper>
<Input
placeholder="Rewrite pin code from Trezor"
innerRef={codeEntryInputRef}
onChange={onInputChange}
/>
<Button
onClick={() => onCodeEntry(codeEntry)}
isLoading={isLoading}
isDisabled={isLoading}
>
Send code
</Button>
</InputWrapper>
<Button
variant="tertiary"
onClick={useQrCode}
isLoading={isLoading}
isDisabled={isLoading}
>
<Translation id="TR_PLEASE_ALLOW_YOUR_CAMERA" />
</Button>
</>
)}
{mode === 'qr-code' && (
<>
<Suspense fallback={<BundleLoader />}>
<CameraWrapper>
<IconButton
variant="tertiary"
icon="cross"
onClick={useCodeEntry}
size="small"
/>
<StyledQrReader
delay={500}
onError={handleError}
onScan={handleScan}
onLoad={onLoad}
showViewFinder={false}
/>
</CameraWrapper>
</Suspense>

{!readerLoaded && !error && (
<CameraPlaceholder>
<IconWrapper>
<Icon name="qrCode" size={100} />
</IconWrapper>
<Translation id="TR_PLEASE_ALLOW_YOUR_CAMERA" />
</CameraPlaceholder>
)}

{error && (
<CameraPlaceholder>
<ErrorWraper>
<Paragraph>
<Translation id="TR_GENERIC_ERROR_TITLE" />
</Paragraph>
<ErrorMessage>{error}</ErrorMessage>
</ErrorWraper>
</CameraPlaceholder>
)}
</>
)}

{mode === 'loading' && (
<CameraPlaceholder>
<BundleLoader />
</CameraPlaceholder>
)}
</ContentWrapper>
</StyledModal>
);
};
1 change: 1 addition & 0 deletions packages/suite/src/components/suite/modals/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { PinModal } from './ReduxModal/DeviceContextModal/PinModal';
export { ThpPairingModal } from './ReduxModal/DeviceContextModal/ThpPairingModal';
export { PinInvalidModal } from './ReduxModal/DeviceContextModal/PinInvalidModal';
export { PinMismatchModal } from './ReduxModal/UserContextModal/PinMismatchModal';
export { PassphraseModal } from './ReduxModal/DeviceContextModal/PassphraseModal';
Expand Down
1 change: 1 addition & 0 deletions packages/suite/src/reducers/suite/modalReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const modalReducer = (state: State = initialState, action: Action): State => {
case UI.INVALID_PIN:
case UI.REQUEST_PASSPHRASE:
case UI.REQUEST_PASSPHRASE_ON_DEVICE:
case UI.REQUEST_THP_PAIRING:
return {
context: MODAL.CONTEXT_DEVICE,
device: action.payload.device,
Expand Down

0 comments on commit a2a5a63

Please sign in to comment.