import React, { useCallback, useMemo, useRef, useState } from 'react'; import { useFocusEffect, useRoute } from '@react-navigation/native'; import { ActivityIndicator, Alert, findNodeHandle, FlatList, InteractionManager, Keyboard, LayoutAnimation, ListRenderItemInfo, Platform, StyleSheet, Text, View, } from 'react-native'; import { Badge, Icon } from '@rneui/themed'; import { isDesktop } from '../../blue_modules/environment'; import { encodeUR } from '../../blue_modules/ur'; import { BlueButtonLink, BlueCard, BlueFormMultiInput, BlueLoading, BlueSpacing10, BlueSpacing20, BlueTextCentered, } from '../../BlueComponents'; import { HDSegwitBech32Wallet, MultisigCosigner, MultisigHDWallet } from '../../class'; import presentAlert from '../../components/Alert'; import BottomModal, { BottomModalHandle } from '../../components/BottomModal'; import Button from '../../components/Button'; import MultipleStepsListItem, { MultipleStepsListItemButtohType, MultipleStepsListItemDashType, } from '../../components/MultipleStepsListItem'; import QRCodeComponent from '../../components/QRCodeComponent'; 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 { disallowScreenshot } from 'react-native-screen-capture'; import loc from '../../loc'; import ActionSheet from '../ActionSheet'; import { useStorage } from '../../hooks/context/useStorage'; import ToolTipMenu from '../../components/TooltipMenu'; import { CommonToolTipActions } from '../../typings/CommonToolTipActions'; import { useSettings } from '../../hooks/context/useSettings'; const ViewEditMultisigCosigners: React.FC = () => { const hasLoaded = useRef(false); const { colors } = useTheme(); const { wallets, setWalletsWithNewOrder } = useStorage(); const { isBiometricUseCapableAndEnabled } = useBiometrics(); const { isElectrumDisabled, isPrivacyBlurEnabled } = 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 shareModalRef = useRef(null); const provideMnemonicsModalRef = useRef(null); const mnemonicsModalRef = useRef(null); 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 stylesHook = StyleSheet.create({ root: { backgroundColor: colors.elevated, }, textDestination: { color: colors.foregroundColor, }, vaultKeyText: { color: colors.alternativeTextColor, }, askPassphrase: { backgroundColor: colors.lightButton, }, 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 onSave = async () => { dismissAllModals(); 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); disallowScreenshot(isPrivacyBlurEnabled); 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 () => { disallowScreenshot(false); task.cancel(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [walletID]), ); const renderMnemonicsModal = () => { return ( { shareModalRef.current?.present(); }} header={ {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} )} )} {renderShareModal()} ); }; const _renderKeyItem = (el: ListRenderItemInfo) => { if (!wallet) { // failsafe return null; } const isXpub = MultisigHDWallet.isXpubValid(wallet.getCosigner(el.index + 1)); let leftText; if (isXpub) { leftText = wallet.getCosigner(el.index + 1); const currentAddress = leftText; const firstFour = currentAddress.substring(0, 5); const lastFour = currentAddress.substring(currentAddress.length - 5, currentAddress.length); leftText = `${firstFour}...${lastFour}`; } else { const secret = wallet.getCosigner(el.index + 1).split(' '); leftText = `${secret[0]}...${secret[secret.length - 1]}`; } // @ts-ignore not sure which one is correct const length = data?.length ?? data.current?.length ?? 0; return ( {isXpub ? ( {!vaultKeyData.isLoading && ( { const keyIndex = el.index + 1; const xpub = wallet.getCosigner(keyIndex); const fp = wallet.getFingerprint(keyIndex); const path = wallet.getCustomDerivationPathForCosigner(keyIndex); if (!path) { presentAlert({ message: 'Cannot find derivation path for this cosigner' }); return; } setVaultKeyData({ keyIndex, seed: '', passphrase: '', xpub, fp, path, isLoading: false, }); setExportString(MultisigCosigner.exportToJson(fp, xpub, path)); setExportStringURv2(encodeUR(MultisigCosigner.exportToJson(fp, xpub, path))[0]); setExportFilename('bw-cosigner-' + fp + '.json'); mnemonicsModalRef.current?.present(); }, }} dashes={MultipleStepsListItemDashType.topAndBottom} /> )} { setCurrentlyEditingCosignerNum(el.index + 1); provideMnemonicsModalRef.current?.present(); }, }} dashes={el.index === length - 1 ? MultipleStepsListItemDashType.top : MultipleStepsListItemDashType.topAndBottom} /> ) : ( {!vaultKeyData.isLoading && ( { const keyIndex = el.index + 1; const seed = wallet.getCosigner(keyIndex); const passphrase = wallet.getCosignerPassphrase(keyIndex); setVaultKeyData({ keyIndex, seed, xpub: '', fp: '', path: '', passphrase: passphrase ?? '', isLoading: false, }); mnemonicsModalRef.current?.present(); const fp = wallet.getFingerprint(keyIndex); const path = wallet.getCustomDerivationPathForCosigner(keyIndex); if (!path) { presentAlert({ message: 'Cannot find derivation path for this cosigner' }); return; } const xpub = wallet.convertXpubToMultisignatureXpub(MultisigHDWallet.seedToXpub(seed, path, passphrase)); setExportString(MultisigCosigner.exportToJson(fp, xpub, path)); setExportStringURv2(encodeUR(MultisigCosigner.exportToJson(fp, xpub, path))[0]); setExportFilename('bw-cosigner-' + fp + '.json'); }, }} dashes={MultipleStepsListItemDashType.topAndBottom} /> )} { if (buttonIndex === 0) return; LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); setVaultKeyData({ ...vaultKeyData, isLoading: true, keyIndex: el.index + 1, }); setTimeout( () => xpubInsteadOfSeed(el.index + 1).finally(() => { LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); setVaultKeyData({ ...vaultKeyData, isLoading: false, keyIndex: el.index + 1, }); }), 100, ); }, }} /> )} ); }; const dismissAllModals = () => { provideMnemonicsModalRef.current?.dismiss(); shareModalRef.current?.dismiss(); mnemonicsModalRef.current?.dismiss(); }; const handleUseMnemonicPhrase = async () => { let passphrase; if (askPassphrase) { try { passphrase = await prompt(loc.wallets.import_passphrase_title, loc.wallets.import_passphrase_message); } catch (e: any) { if (e.message === 'Cancel Pressed') { setIsLoading(false); return; } throw e; } } return _handleUseMnemonicPhrase(importText, passphrase); }; const _handleUseMnemonicPhrase = (mnemonic: string, passphrase?: string) => { if (!wallet || !currentlyEditingCosignerNum) { // failsafe return; } const hd = new HDSegwitBech32Wallet(); hd.setSecret(mnemonic); if (!hd.validateMnemonic()) return presentAlert({ message: loc.multisig.invalid_mnemonics }); try { wallet.replaceCosignerXpubWithSeed(currentlyEditingCosignerNum, hd.getSecret(), passphrase); } catch (e: any) { console.log(e); return presentAlert({ message: e.message }); } LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); setWallet(wallet); provideMnemonicsModalRef.current?.dismiss(); setIsSaveButtonDisabled(false); setImportText(''); setAskPassphrase(false); }; const xpubInsteadOfSeed = (index: number): Promise => { return new Promise((resolve, reject) => { InteractionManager.runAfterInteractions(() => { try { wallet?.replaceCosignerSeedWithXpub(index); } catch (e: any) { reject(e); return presentAlert({ message: e.message }); } LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); setWallet(wallet); setIsSaveButtonDisabled(false); resolve(); }); }); }; const scanOrOpenFile = async () => { await provideMnemonicsModalRef.current?.dismiss(); const scanned = await scanQrHelper(route.name, true, undefined); setImportText(String(scanned)); provideMnemonicsModalRef.current?.present(); }; const hideProvideMnemonicsModal = () => { Keyboard.dismiss(); provideMnemonicsModalRef.current?.dismiss(); setImportText(''); setAskPassphrase(false); }; const hideShareModal = () => {}; const toolTipActions = useMemo(() => { return [{ ...CommonToolTipActions.Passphrase, menuState: askPassphrase }]; }, [askPassphrase]); const renderProvideMnemonicsModal = () => { return ( {isLoading ? ( ) : (