import { useFocusEffect } from '@react-navigation/native'; import { NativeStackScreenProps } from '@react-navigation/native-stack'; import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'; import { ActivityIndicator, Alert, FlatList, InteractionManager, Keyboard, KeyboardAvoidingView, LayoutAnimation, ListRenderItemInfo, Platform, StyleSheet, Switch, Text, View, findNodeHandle, } from 'react-native'; import { Badge, Icon } from 'react-native-elements'; import { BlueButtonLink, BlueFormMultiInput, BlueLoading, BlueSpacing10, BlueSpacing20, BlueSpacing40, BlueText, BlueTextCentered, } from '../../BlueComponents'; import { ViewEditMultisigCosignersStackParamsList } from '../../Navigation'; import * as NavigationService from '../../NavigationService'; import { BlueStorageContext } from '../../blue_modules/storage-context'; import { encodeUR } from '../../blue_modules/ur'; import { HDSegwitBech32Wallet, MultisigCosigner, MultisigHDWallet } from '../../class'; import Biometric from '../../class/biometrics'; 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 { SquareButton } from '../../components/SquareButton'; import SquareEnumeratedWords, { SquareEnumeratedWordsContentAlign } from '../../components/SquareEnumeratedWords'; import navigationStyle from '../../components/navigationStyle'; import { useTheme } from '../../components/themes'; import { scanQrHelper } from '../../helpers/scan-qr'; import usePrivacy from '../../hooks/usePrivacy'; import loc from '../../loc'; import { isDesktop } from '../../blue_modules/environment'; import ActionSheet from '../ActionSheet'; import SaveFileButton from '../../components/SaveFileButton'; import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; import prompt from '../../helpers/prompt'; type Props = NativeStackScreenProps; const ViewEditMultisigCosigners = ({ route }: Props) => { const hasLoaded = useRef(false); const { colors } = useTheme(); const { wallets, setWalletsWithNewOrder, isElectrumDisabled, isAdvancedModeEnabled } = useContext(BlueStorageContext); const { navigate, dispatch, addListener } = useExtendedNavigation(); const openScannerButtonRef = useRef(); const { walletId } = route.params; 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: '', path: '', fp: '', isLoading: false }); // string rendered in modal const [askPassphrase, setAskPassphrase] = useState(false); const [isAdvancedModeEnabledRender, setIsAdvancedModeEnabledRender] = 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 => { // 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]), ); useEffect(() => { isAdvancedModeEnabled().then(setIsAdvancedModeEnabledRender); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const saveFileButtonAfterOnPress = () => { setIsShareModalVisible(false); }; const onSave = async () => { if (!wallet) { throw new Error('Wallet is undefined'); } setIsLoading(true); const isBiometricsEnabled = await Biometric.isBiometricUseCapableAndEnabled(); if (isBiometricsEnabled) { if (!(await Biometric.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); // @ts-ignore wtf 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} )}