import React, { useEffect, useCallback, useRef } from 'react'; import { View, StyleSheet, Text, Image, InteractionManager, ActivityIndicator, Alert, } from 'react-native'; import { fontStyles } from '../../../styles/common'; import { getOnboardingNavbarOptions } from '../../UI/Navbar'; import StyledButton from '../../UI/StyledButton'; import { SafeAreaView } from 'react-native-safe-area-context'; import { strings } from '../../../../locales/i18n'; import PubNubWrapper from '../../../util/syncWithExtension'; import Logger from '../../../util/Logger'; import AnalyticsV2 from '../../../util/analyticsV2'; import Analytics from '../../../core/Analytics/Analytics'; import DefaultPreference from 'react-native-default-preference'; import PreventScreenshot from '../../../core/PreventScreenshot'; import { EXISTING_USER, METRICS_OPT_IN, SEED_PHRASE_HINTS, BIOMETRY_CHOICE, BIOMETRY_CHOICE_DISABLED, NEXT_MAKER_REMINDER, TRUE, } from '../../../constants/storage'; import AsyncStorage from '@react-native-community/async-storage'; import SecureKeychain from '../../../core/SecureKeychain'; import Device from '../../../util/device'; import AppConstants from '../../../core/AppConstants'; import Engine from '../../../core/Engine'; import { useDispatch, useSelector } from 'react-redux'; import { saveOnboardingEvent as saveEvent } from '../../../actions/onboarding'; import { logIn, loadingSet, loadingUnset, seedphraseNotBackedUp as backedUpSeed, passwordSet as passwordIsSet, } from '../../../actions/user'; import { setLockTime as lockTimeSet } from '../../../actions/settings'; import { BIOMETRY_TYPE } from 'react-native-keychain'; import scaling from '../../../util/scaling'; import { useAppThemeFromContext, mockTheme } from '../../../util/theme'; const createStyles = (colors: any) => StyleSheet.create({ container: { flex: 1, backgroundColor: colors.background.default, paddingHorizontal: 16, justifyContent: 'space-between', paddingBottom: 16, }, fill: { flex: 1, }, syncImage: { height: 44, marginTop: 48, width: 112, alignSelf: 'center', }, titleLabel: { textAlign: 'center', color: colors.text.default, fontSize: 24, fontFamily: fontStyles.bold.fontFamily, marginTop: 32, }, stepsContainer: { marginTop: 32, }, stepLabel: { color: colors.text.default, fontSize: scaling.scale(16), fontFamily: fontStyles.normal.fontFamily, marginBottom: 8, }, wrapper: { flex: 1, alignItems: 'center', paddingVertical: 30, }, loader: { marginTop: 180, justifyContent: 'center', textAlign: 'center', }, loadingText: { marginTop: 30, fontSize: 14, textAlign: 'center', color: colors.text.default, fontFamily: fontStyles.normal.fontFamily, }, }); // TODO: This file needs typings const ExtensionSync = ({ navigation, route }: any) => { const pubnubWrapperRef = useRef(null); const passwordRef = useRef(undefined); const seedWordsRef = useRef(null); const importedAccountsRef = useRef(null); const dataToSyncRef = useRef(null); const { colors } = useAppThemeFromContext() || mockTheme; const styles = createStyles(colors); const passwordSet = useSelector((state: any) => state.user.passwordSet); const selectedAddress = useSelector( (state: any) => state.engine.backgroundState.PreferencesController.selectedAddress, ); const loading = useSelector((state: any) => state.user.loadingSet); const loadingMsg = useSelector((state: any) => state.user.loadingMsg); const dispatch = useDispatch(); const saveOnboardingEvent = useCallback( (event: any) => dispatch(saveEvent(event)), [dispatch], ); const setLoading = useCallback( (msg: string) => dispatch(loadingSet(msg)), [dispatch], ); const unsetLoading = useCallback(() => dispatch(loadingUnset()), [dispatch]); const passwordHasBeenSet = useCallback( () => dispatch(passwordIsSet()), [dispatch], ); const seedphraseBackedUp = useCallback( () => dispatch(backedUpSeed()), [dispatch], ); const setLockTime = useCallback( (time: number) => dispatch(lockTimeSet(time)), [dispatch], ); const setLogIn = useCallback(() => dispatch(logIn()), [dispatch]); useEffect( /* eslint-disable-next-line */ () => { // Unmount return () => { pubnubWrapperRef.current?.disconnectWebsockets?.(); unsetLoading(); InteractionManager.runAfterInteractions(PreventScreenshot.allow); }; }, // eslint-disable-next-line react-hooks/exhaustive-deps [], ); useEffect(() => { // Set navigation options navigation.setOptions( getOnboardingNavbarOptions(navigation, route, colors), ); }, [navigation, route, colors]); // TODO: Don't spread this, break it out and type it const track = useCallback( (...eventArgs) => { InteractionManager.runAfterInteractions(async () => { if (Analytics.checkEnabled()) { AnalyticsV2.trackEvent(eventArgs[0], eventArgs[1]); return; } const metricsOptIn = await DefaultPreference.get(METRICS_OPT_IN); if (!metricsOptIn) { saveOnboardingEvent(eventArgs); } }); }, [saveOnboardingEvent], ); const finishSync = useCallback( async (opts) => { if (opts.biometrics) { try { await SecureKeychain.setGenericPassword( opts.password, SecureKeychain.TYPES.BIOMETRICS, ); } catch (e) { await SecureKeychain.resetGenericPassword(); } } else { await SecureKeychain.resetGenericPassword(); } try { await AsyncStorage.removeItem(NEXT_MAKER_REMINDER); await Engine.resetState(); await Engine.sync({ ...dataToSyncRef.current, seed: seedWordsRef.current, importedAccounts: importedAccountsRef.current, pass: opts.password, }); await AsyncStorage.setItem(EXISTING_USER, TRUE); await AsyncStorage.removeItem(SEED_PHRASE_HINTS); passwordHasBeenSet(); setLogIn(); setLockTime(AppConstants.DEFAULT_LOCK_TIMEOUT); seedphraseBackedUp(); dataToSyncRef.current = null; track(AnalyticsV2.ANALYTICS_EVENTS.WALLET_SYNC_SUCCESSFUL); track(AnalyticsV2.ANALYTICS_EVENTS.WALLET_SETUP_COMPLETED, { wallet_setup_type: 'sync', new_wallet: false, }); navigation.push('SyncWithExtensionSuccess'); unsetLoading(); } catch (e) { track(AnalyticsV2.ANALYTICS_EVENTS.WALLET_SETUP_FAILURE, { wallet_setup_type: 'sync', error_type: e.toString(), }); Logger.error(e, 'Sync::disconnect'); Alert.alert( strings('sync_with_extension.error_title'), strings('sync_with_extension.error_message'), ); unsetLoading(); navigation.goBack(); } }, [ setLogIn, unsetLoading, passwordHasBeenSet, setLockTime, seedphraseBackedUp, track, navigation, ], ); const disconnect = useCallback(async () => { let password: string | undefined; try { // If there's a password set, let's keep it if (passwordSet) { // This could also come from the previous step if it's a first time user const credentials = await SecureKeychain.getGenericPassword(); if (credentials) { password = credentials.password; } else { password = passwordRef.current; } // Otherwise use the password from the extension } else { password = passwordRef.current; } } catch (e) { password = passwordRef.current; } if (password === passwordRef.current) { let biometryType: BIOMETRY_TYPE | null | 'biometrics' = await SecureKeychain.getSupportedBiometryType(); if (biometryType) { if (Device.isAndroid()) biometryType = 'biometrics'; Alert.alert( strings('sync_with_extension.allow_biometrics_title', { biometrics: biometryType, }), strings('sync_with_extension.allow_biometrics_desc', { biometrics: biometryType, }), [ { text: strings('sync_with_extension.warning_cancel_button'), onPress: async () => { await AsyncStorage.removeItem(BIOMETRY_CHOICE); await AsyncStorage.setItem(BIOMETRY_CHOICE_DISABLED, TRUE); finishSync({ biometrics: false, password }); }, style: 'cancel', }, { text: strings('sync_with_extension.warning_ok_button'), onPress: async () => { await AsyncStorage.setItem( BIOMETRY_CHOICE, biometryType as string, ); await AsyncStorage.removeItem(BIOMETRY_CHOICE_DISABLED); finishSync({ biometrics: true, biometryType, password }); }, }, ], { cancelable: false }, ); } else { finishSync({ biometrics: false, password }); } } else { finishSync({ biometrics: false, password }); } }, [finishSync, passwordSet, passwordRef]); const initWebsockets = useCallback(() => { setLoading(strings('sync_with_extension.please_wait')); pubnubWrapperRef.current?.addMessageListener?.( () => { Alert.alert( strings('sync_with_extension.error_title'), strings('sync_with_extension.error_message'), ); track(AnalyticsV2.ANALYTICS_EVENTS.WALLET_SETUP_FAILURE, { wallet_setup_type: 'sync', error_type: 'onErrorSync', }); unsetLoading(); return false; }, (data: any) => { // this.incomingDataStr = null; const { pwd, seed, importedAccounts } = data.udata; passwordRef.current = pwd; seedWordsRef.current = seed; importedAccountsRef.current = importedAccounts; delete data.udata; dataToSyncRef.current = { ...data }; pubnubWrapperRef.current?.endSync?.(disconnect); }, ); pubnubWrapperRef.current?.subscribe?.(); }, [ pubnubWrapperRef, passwordRef, track, unsetLoading, disconnect, setLoading, ]); const startSync = useCallback(async () => { try { initWebsockets(); await pubnubWrapperRef.current?.startSync?.(); return true; } catch (e) { unsetLoading(); if (e.message === 'Sync::timeout') { Alert.alert( strings('sync_with_extension.outdated_qr_code'), strings('sync_with_extension.outdated_qr_code_desc'), ); } else { Alert.alert( strings('sync_with_extension.something_wrong'), strings('sync_with_extension.something_wrong_desc'), ); } Logger.error(e, { message: 'Sync::startSync', firstAttempt: true }); track(AnalyticsV2.ANALYTICS_EVENTS.WALLET_SETUP_FAILURE, { wallet_setup_type: 'sync', error_type: e.message(), }); return false; } }, [initWebsockets, pubnubWrapperRef, track, unsetLoading]); const onStartScan = useCallback( async (data) => { if (data.content && data.content.search('metamask-sync:') !== -1) { const [channelName, cipherKey] = data.content .replace('metamask-sync:', '') .split('|@|'); pubnubWrapperRef.current = new PubNubWrapper(channelName, cipherKey); await pubnubWrapperRef.current?.establishConnection?.(selectedAddress); } else { Alert.alert( strings('sync_with_extension.invalid_qr_code'), strings('sync_with_extension.invalid_qr_code_desc'), ); } }, [selectedAddress], ); const onScanSuccess = useCallback( async (data) => { if (data.content && data.content.search('metamask-sync:') !== -1) { await startSync(); } else { Alert.alert( strings('sync_with_extension.invalid_qr_code'), strings('sync_with_extension.invalid_qr_code_desc'), ); } }, [startSync], ); const triggerScan = useCallback(() => { navigation.push('QRScanner', { onStartScan, onScanSuccess, }); }, [onStartScan, onScanSuccess, navigation]); const renderSyncImage = useCallback( () => ( ), [styles], ); const renderTitle = useCallback( () => ( {strings('onboarding.scan_title')} ), [styles], ); const renderSteps = useCallback(() => { const steps = [1, 2, 3, 4]; return ( {steps.map((stepIndex) => { const text = `onboarding.scan_step_${stepIndex}`; return ( {`${stepIndex}. ${strings(text)}`} ); })} ); }, [styles]); const renderScanButton = useCallback( () => ( {strings('onboarding.scan')} ), [triggerScan], ); const renderLoader = useCallback( () => ( {loadingMsg} ), [loadingMsg, styles], ); const renderContent = useCallback( () => ( {renderSyncImage()} {renderTitle()} {renderSteps()} {renderScanButton()} ), [renderSyncImage, renderTitle, renderSteps, renderScanButton, styles], ); return ( {loading ? renderLoader() : renderContent()} ); }; export default ExtensionSync;