import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { RouteProp, useRoute } from '@react-navigation/native'; import { ActivityIndicator, FlatList, LayoutAnimation, Platform, StyleSheet, UIManager, View } from 'react-native'; import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback'; import { BlueButtonLink, BlueFormLabel, BlueSpacing10, BlueSpacing20, BlueSpacing40, BlueText } from '../../BlueComponents'; import { HDSegwitBech32Wallet, WatchOnlyWallet } from '../../class'; import startImport, { TImport } from '../../class/wallet-import'; import presentAlert from '../../components/Alert'; import Button from '../../components/Button'; import SafeArea from '../../components/SafeArea'; import { useTheme } from '../../components/themes'; import WalletToImport from '../../components/WalletToImport'; import prompt from '../../helpers/prompt'; import loc from '../../loc'; import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; import { useStorage } from '../../hooks/context/useStorage'; import { AddWalletStackParamList } from '../../navigation/AddWalletStack'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { THDWalletForWatchOnly, TWallet } from '../../class/wallets/types'; import { keepAwake, disallowScreenshot } from 'react-native-screen-capture'; import { useSettings } from '../../hooks/context/useSettings'; import { isDesktop } from '../../blue_modules/environment'; type RouteProps = RouteProp; type NavigationProp = NativeStackNavigationProp; type WalletEntry = { wallet: TWallet | THDWalletForWatchOnly; subtitle: string; id: string; }; if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) { UIManager.setLayoutAnimationEnabledExperimental(true); } const ImportWalletDiscovery: React.FC = () => { const navigation = useExtendedNavigation(); const { colors } = useTheme(); const route = useRoute(); const { importText, askPassphrase, searchAccounts } = route.params; const { isElectrumDisabled } = useSettings(); const task = useRef(null); const { addAndSaveWallet } = useStorage(); const [loading, setLoading] = useState(true); const [wallets, setWallets] = useState([]); const [password, setPassword] = useState(); const [selected, setSelected] = useState(0); const [progress, setProgress] = useState(); const importing = useRef(false); const bip39 = useMemo(() => { const hd = new HDSegwitBech32Wallet(); hd.setSecret(importText); return hd.validateMnemonic(); }, [importText]); const stylesHook = StyleSheet.create({ root: { backgroundColor: colors.elevated, }, center: { backgroundColor: colors.elevated, }, }); const saveWallet = useCallback( (wallet: TWallet | THDWalletForWatchOnly) => { if (importing.current) return; importing.current = true; addAndSaveWallet(wallet); navigation.getParent()?.goBack(); }, [addAndSaveWallet, navigation], ); const handleSave = () => { if (wallets.length === 0) return; saveWallet(wallets[selected].wallet); }; useEffect(() => { const onProgress = (data: string) => setProgress(data); const onWallet = (wallet: TWallet | THDWalletForWatchOnly) => { LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); const id = wallet.getID(); let subtitle: string | undefined; try { // For watch-only wallets, display the descriptor or xpub if (wallet.type === WatchOnlyWallet.type) { if (wallet.isHd() && wallet.getSecret()) { subtitle = wallet.getSecret(); // Display descriptor } else { subtitle = wallet.getAddress(); // Display address } } else { subtitle = (wallet as THDWalletForWatchOnly).getDerivationPath?.(); } } catch (e) {} setWallets(w => [...w, { wallet, subtitle: subtitle || '', id }]); }; const onPassword = async (title: string, subtitle: string) => { try { const pass = await prompt(title, subtitle); setPassword(pass); return pass; } catch (e: any) { if (e.message === 'Cancel Pressed') { navigation.goBack(); } throw e; } }; if (!isDesktop) keepAwake(true); task.current = startImport(importText, askPassphrase, searchAccounts, isElectrumDisabled, onProgress, onWallet, onPassword); task.current.promise .then(({ cancelled, wallets: w }) => { if (cancelled) return; if (w.length === 1) saveWallet(w[0]); // Instantly save wallet if only one has been discovered if (w.length === 0) { triggerHapticFeedback(HapticFeedbackTypes.ImpactLight); } }) .catch(e => { console.warn('import error', e); console.warn('err.stack', e.stack); presentAlert({ title: 'Import error', message: e.message }); }) .finally(() => { LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); setLoading(false); if (!isDesktop) keepAwake(false); }); return () => { if (!isDesktop) keepAwake(false); task.current?.stop(); }; // ignoring "navigation" here, because it is constantly mutating // removed all deps as they were leading to a rerender and retask loop // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handleCustomDerivation = () => { task.current?.stop(); if (!isDesktop) { keepAwake(false); disallowScreenshot(false); } navigation.navigate('ImportCustomDerivationPath', { importText, password }); }; const renderItem = ({ item, index }: { item: WalletEntry; index: number }) => ( { setSelected(index); triggerHapticFeedback(HapticFeedbackTypes.Selection); }} /> ); const keyExtractor = (w: WalletEntry) => w.id; const ListHeaderComponent = useMemo( () => ( <> {wallets.length > 0 ? ( <> {isElectrumDisabled && ( <> {loc.wallets.import_discovery_offline} )} {loc.wallets.import_discovery_subtitle} ) : null} ), [wallets, isElectrumDisabled], ); const ListEmptyComponent = useMemo( () => ( {loading ? ( <> {progress} ) : ( <> {loc.wallets.import_discovery_no_wallets} )} ), [loading, progress], ); return ( {bip39 && ( )}