import React, {
Fragment,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import Engine from '../../../core/Engine';
import {
StyleSheet,
Text,
View,
ScrollView,
// eslint-disable-next-line react-native/split-platform-components
PermissionsAndroid,
Linking,
AppState,
AppStateStatus,
} from 'react-native';
import { strings } from '../../../../locales/i18n';
import AnimatedQRCode from './AnimatedQRCode';
import AnimatedQRScannerModal from './AnimatedQRScanner';
import { fontStyles } from '../../../styles/common';
import AccountInfoCard from '../AccountInfoCard';
import ActionView from '../ActionView';
import { IQRState } from './types';
import { UR } from '@ngraveio/bc-ur';
import { ETHSignature } from '@keystonehq/bc-ur-registry-eth';
import { stringify as uuidStringify } from 'uuid';
import Alert, { AlertType } from '../../Base/Alert';
import AnalyticsV2 from '../../../util/analyticsV2';
import { useNavigation } from '@react-navigation/native';
import { mockTheme, useAppThemeFromContext } from '../../../util/theme';
import Device from '../../../util/device';
interface IQRSigningDetails {
QRState: IQRState;
successCallback?: () => void;
failureCallback?: (error: string) => void;
cancelCallback?: () => void;
confirmButtonMode?: 'normal' | 'sign' | 'confirm';
showCancelButton?: boolean;
tighten?: boolean;
showHint?: boolean;
shouldStartAnimated?: boolean;
bypassAndroidCameraAccessCheck?: boolean;
}
const createStyles = (colors: any) =>
StyleSheet.create({
wrapper: {
flex: 1,
},
container: {
flex: 1,
width: '100%',
flexDirection: 'column',
alignItems: 'center',
backgroundColor: colors.background.default,
},
accountInfoCardWrapper: {
paddingHorizontal: 24,
paddingBottom: 12,
},
containerTighten: {
paddingHorizontal: 8,
},
title: {
flexDirection: 'row',
justifyContent: 'center',
marginTop: 54,
marginBottom: 30,
},
titleTighten: {
marginTop: 12,
marginBottom: 6,
},
titleText: {
...fontStyles.normal,
fontSize: 14,
color: colors.text.default,
},
description: {
marginVertical: 24,
alignItems: 'center',
...fontStyles.normal,
fontSize: 14,
},
descriptionTighten: {
marginVertical: 12,
},
padding: {
height: 40,
},
descriptionText: {
...fontStyles.normal,
fontSize: 14,
color: colors.text.default,
},
descriptionTextTighten: {
fontSize: 12,
},
errorText: {
...fontStyles.normal,
fontSize: 12,
color: colors.error.default,
},
alert: {
marginHorizontal: 16,
marginTop: 12,
},
});
const QRSigningDetails = ({
QRState,
successCallback,
failureCallback,
cancelCallback,
confirmButtonMode = 'confirm',
showCancelButton = false,
tighten = false,
showHint = true,
shouldStartAnimated = true,
bypassAndroidCameraAccessCheck = true,
}: IQRSigningDetails) => {
const { colors } = useAppThemeFromContext() || mockTheme;
const styles = createStyles(colors);
const navigation = useNavigation();
const KeyringController = useMemo(() => {
const { KeyringController: keyring } = Engine.context as any;
return keyring;
}, []);
const [scannerVisible, setScannerVisible] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const [shouldPause, setShouldPause] = useState(false);
const [cameraError, setCameraError] = useState('');
// ios handled camera perfectly in this situation, we just need to check permission with android.
const [hasCameraPermission, setCameraPermission] = useState(
Device.isIos() || bypassAndroidCameraAccessCheck,
);
const checkAndroidCamera = useCallback(() => {
if (Device.isAndroid() && !hasCameraPermission) {
PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.CAMERA).then(
(_hasPermission) => {
setCameraPermission(_hasPermission);
if (!_hasPermission) {
setCameraError(strings('transaction.no_camera_permission_android'));
} else {
setCameraError('');
}
},
);
}
}, [hasCameraPermission]);
const handleAppState = useCallback(
(appState: AppStateStatus) => {
if (appState === 'active') {
checkAndroidCamera();
}
},
[checkAndroidCamera],
);
useEffect(() => {
checkAndroidCamera();
}, [checkAndroidCamera]);
useEffect(() => {
AppState.addEventListener('change', handleAppState);
return () => {
AppState.removeEventListener('change', handleAppState);
};
}, [handleAppState]);
const [hasSentOrCanceled, setSentOrCanceled] = useState(false);
useEffect(() => {
navigation.addListener('beforeRemove', (e) => {
if (hasSentOrCanceled) {
return;
}
e.preventDefault();
KeyringController.cancelQRSignRequest().then(() => {
navigation.dispatch(e.data.action);
});
});
}, [KeyringController, hasSentOrCanceled, navigation]);
const resetError = () => {
setErrorMessage('');
};
const showScanner = () => {
setScannerVisible(true);
resetError();
};
const hideScanner = () => {
setScannerVisible(false);
};
const onCancel = useCallback(async () => {
await KeyringController.cancelQRSignRequest();
setSentOrCanceled(true);
hideScanner();
cancelCallback?.();
}, [KeyringController, cancelCallback]);
const onScanSuccess = useCallback(
(ur: UR) => {
hideScanner();
const signature = ETHSignature.fromCBOR(ur.cbor);
const buffer = signature.getRequestId();
const requestId = uuidStringify(buffer);
if (QRState.sign.request?.requestId === requestId) {
KeyringController.submitQRSignature(
QRState.sign.request?.requestId as string,
ur.cbor.toString('hex'),
);
setSentOrCanceled(true);
successCallback?.();
} else {
AnalyticsV2.trackEvent(
AnalyticsV2.ANALYTICS_EVENTS.HARDWARE_WALLET_ERROR,
{
error:
'received signature request id is not matched with origin request',
},
);
setErrorMessage(strings('transaction.mismatched_qr_request_id'));
failureCallback?.(strings('transaction.mismatched_qr_request_id'));
}
},
[
KeyringController,
QRState.sign.request?.requestId,
failureCallback,
successCallback,
],
);
const onScanError = useCallback(
(_errorMessage: string) => {
hideScanner();
setErrorMessage(_errorMessage);
failureCallback?.(_errorMessage);
},
[failureCallback],
);
const renderAlert = () =>
errorMessage !== '' && (
{errorMessage}
);
const renderCameraAlert = () =>
cameraError !== '' && (
{cameraError}
);
return (
{QRState?.sign?.request && (
{renderAlert()}
{renderCameraAlert()}
{strings('transactions.sign_title_scan')}
{strings('transactions.sign_title_device')}
{showHint ? (
{strings('transactions.sign_description_1')}
{strings('transactions.sign_description_2')}
) : !tighten ? (
) : null}
)}
);
};
export default QRSigningDetails;