import React, { useCallback, useRef, useState } from 'react'; import { useFocusEffect, useRoute } from '@react-navigation/native'; import { ActivityIndicator, Alert, findNodeHandle, FlatList, InteractionManager, Keyboard, KeyboardAvoidingView, LayoutAnimation, ListRenderItemInfo, Platform, StyleSheet, Switch, Text, View, } from 'react-native'; import { Badge, Icon } from '@rneui/themed'; import { isDesktop, isTablet } from '../../blue_modules/environment'; import { encodeUR } from '../../blue_modules/ur'; import { BlueButtonLink, BlueFormMultiInput, BlueLoading, BlueSpacing10, BlueSpacing20, BlueSpacing40, BlueText, BlueTextCentered, } from '../../BlueComponents'; import { HDSegwitBech32Wallet, MultisigCosigner, MultisigHDWallet } from '../../class'; import presentAlert from '../../components/Alert'; import BottomModal from '../../components/BottomModal'; import Button from '../../components/Button'; import MultipleStepsListItem, { MultipleStepsListItemButtohType, MultipleStepsListItemDashType, } from '../../components/MultipleStepsListItem'; import QRCodeComponent from '../../components/QRCodeComponent'; import SaveFileButton from '../../components/SaveFileButton'; import { SquareButton } from '../../components/SquareButton'; import SquareEnumeratedWords, { SquareEnumeratedWordsContentAlign } from '../../components/SquareEnumeratedWords'; import { useTheme } from '../../components/themes'; import prompt from '../../helpers/prompt'; import { scanQrHelper } from '../../helpers/scan-qr'; import { unlockWithBiometrics, useBiometrics } from '../../hooks/useBiometrics'; import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; import usePrivacy from '../../hooks/usePrivacy'; import loc from '../../loc'; import ActionSheet from '../ActionSheet'; import { useStorage } from '../../hooks/context/useStorage'; import { useSettings } from '../../hooks/context/useSettings'; const ViewEditMultisigCosigners: React.FC = () => { const hasLoaded = useRef(false); const { colors } = useTheme(); const { wallets, setWalletsWithNewOrder, isElectrumDisabled } = useStorage(); const { isBiometricUseCapableAndEnabled } = useBiometrics(); const { isAdvancedModeEnabled } = useSettings(); const { navigate, dispatch, addListener } = useExtendedNavigation(); const openScannerButtonRef = useRef(); const route = useRoute(); const { walletID } = route.params as { walletID: string }; const w = useRef(wallets.find(wallet => wallet.getID() === walletID)); const tempWallet = useRef(new MultisigHDWallet()); const [wallet, setWallet] = useState(); const [isLoading, setIsLoading] = useState(true); const [isSaveButtonDisabled, setIsSaveButtonDisabled] = useState(true); const [currentlyEditingCosignerNum, setCurrentlyEditingCosignerNum] = useState(false); const [isProvideMnemonicsModalVisible, setIsProvideMnemonicsModalVisible] = useState(false); const [isMnemonicsModalVisible, setIsMnemonicsModalVisible] = useState(false); const [isShareModalVisible, setIsShareModalVisible] = useState(false); const [importText, setImportText] = useState(''); const [exportString, setExportString] = useState('{}'); // used in exportCosigner() const [exportStringURv2, setExportStringURv2] = useState(''); // used in QR const [exportFilename, setExportFilename] = useState('bw-cosigner.json'); const [vaultKeyData, setVaultKeyData] = useState({ keyIndex: 1, xpub: '', seed: '', passphrase: '', path: '', fp: '', isLoading: false }); // string rendered in modal const [askPassphrase, setAskPassphrase] = useState(false); const data = useRef(); /* discardChangesRef is only so the action sheet can be shown on mac catalyst when a user tries to leave the screen with unsaved changes. Why the container view ? It was the easiest to get the ref for. No other reason. */ const discardChangesRef = useRef(null); const { enableBlur, disableBlur } = usePrivacy(); const stylesHook = StyleSheet.create({ root: { backgroundColor: colors.elevated, }, textDestination: { color: colors.foregroundColor, }, modalContent: { backgroundColor: colors.elevated, }, exportButton: { backgroundColor: colors.buttonDisabledBackgroundColor, }, vaultKeyText: { color: colors.alternativeTextColor, }, vaultKeyCircleSuccess: { backgroundColor: colors.msSuccessBG, }, tipKeys: { color: colors.alternativeTextColor, }, tipLabel: { backgroundColor: colors.inputBackgroundColor, borderColor: colors.inputBackgroundColor, }, tipLabelText: { color: colors.buttonTextColor, }, }); useFocusEffect( useCallback(() => { const unsubscribe = addListener('beforeRemove', (e: { preventDefault: () => void; data: { action: any } }) => { // Check if there are unsaved changes if (isSaveButtonDisabled) { // If there are no unsaved changes, let the user leave the screen return; } // Prevent the default action (going back) e.preventDefault(); // Show an alert asking the user to discard changes or cancel if (isDesktop) { if (!discardChangesRef.current) return dispatch(e.data.action); const anchor = findNodeHandle(discardChangesRef.current); if (!anchor) return dispatch(e.data.action); ActionSheet.showActionSheetWithOptions( { options: [loc._.cancel, loc._.ok], cancelButtonIndex: 0, title: loc._.discard_changes, message: loc._.discard_changes_explain, anchor, }, buttonIndex => { if (buttonIndex === 1) { dispatch(e.data.action); } }, ); } else { Alert.alert(loc._.discard_changes, loc._.discard_changes_explain, [ { text: loc._.cancel, style: 'cancel', onPress: () => {} }, { text: loc._.ok, style: 'default', // If the user confirms, then we dispatch the action we blocked earlier onPress: () => dispatch(e.data.action), }, ]); } }); return unsubscribe; }, [isSaveButtonDisabled, addListener, dispatch]), ); const saveFileButtonAfterOnPress = () => { setIsShareModalVisible(false); }; const onSave = async () => { if (!wallet) { throw new Error('Wallet is undefined'); } setIsLoading(true); const isBiometricsEnabled = await isBiometricUseCapableAndEnabled(); if (isBiometricsEnabled) { if (!(await unlockWithBiometrics())) { setIsLoading(false); return; } } // eslint-disable-next-line prefer-const let newWallets = wallets.filter(newWallet => { return newWallet.getID() !== walletID; }) as MultisigHDWallet[]; if (!isElectrumDisabled) { await wallet?.fetchBalance(); } newWallets.push(wallet); navigate('WalletsList'); setTimeout(() => { setWalletsWithNewOrder(newWallets); }, 500); }; useFocusEffect( useCallback(() => { // useFocusEffect is called on willAppear (example: when camera dismisses). we want to avoid this. if (hasLoaded.current) return; setIsLoading(true); enableBlur(); const task = InteractionManager.runAfterInteractions(async () => { if (!w.current) { // lets create fake wallet so renderer wont throw any errors w.current = new MultisigHDWallet(); w.current.setNativeSegwit(); } else { tempWallet.current.setSecret(w.current.getSecret()); data.current = new Array(tempWallet.current.getN()); setWallet(tempWallet.current); } hasLoaded.current = true; setIsLoading(false); }); return () => { disableBlur(); task.cancel(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [walletID]), ); const hideMnemonicsModal = () => { Keyboard.dismiss(); setIsMnemonicsModalVisible(false); }; const renderMnemonicsModal = () => { return ( {loc.formatString(loc.multisig.vault_key, { number: vaultKeyData.keyIndex })} {vaultKeyData.xpub.length > 1 && ( <> {loc._.wallet_key} )} {vaultKeyData.seed.length > 1 && ( <> {loc._.seed} {vaultKeyData.passphrase.length > 1 && ( {vaultKeyData.passphrase} )} )}