diff --git a/Navigation.tsx b/Navigation.tsx index 1664fb202..f3703d4e0 100644 --- a/Navigation.tsx +++ b/Navigation.tsx @@ -27,7 +27,7 @@ import WalletsAddMultisigStep2 from './screen/wallets/addMultisigStep2'; import WalletAddresses from './screen/wallets/addresses'; import WalletDetails from './screen/wallets/details'; import WalletExport from './screen/wallets/export'; -import ExportMultisigCoordinationSetup from './screen/wallets/exportMultisigCoordinationSetup'; +import ExportMultisigCoordinationSetup from './screen/wallets/ExportMultisigCoordinationSetup'; import GenerateWord from './screen/wallets/generateWord'; import ImportWallet from './screen/wallets/import'; import ImportCustomDerivationPath from './screen/wallets/importCustomDerivationPath'; @@ -556,7 +556,12 @@ const ExportMultisigCoordinationSetupRoot = () => { ); diff --git a/components/SquareButton.tsx b/components/SquareButton.tsx index dbf130250..47ea994ca 100644 --- a/components/SquareButton.tsx +++ b/components/SquareButton.tsx @@ -5,7 +5,7 @@ import { useTheme } from './themes'; interface SquareButtonProps { title: string; onPress?: () => void; - style: StyleProp; + style?: StyleProp; testID?: string; } diff --git a/screen/wallets/ExportMultisigCoordinationSetup.tsx b/screen/wallets/ExportMultisigCoordinationSetup.tsx new file mode 100644 index 000000000..fccb695d7 --- /dev/null +++ b/screen/wallets/ExportMultisigCoordinationSetup.tsx @@ -0,0 +1,200 @@ +import React, { useCallback, useContext, useMemo, useReducer, useRef } from 'react'; +import { ActivityIndicator, InteractionManager, ScrollView, StyleSheet, View } from 'react-native'; +import { useFocusEffect, useRoute, RouteProp } from '@react-navigation/native'; +import { BlueSpacing20, BlueText } from '../../BlueComponents'; +import { DynamicQRCode } from '../../components/DynamicQRCode'; +import loc from '../../loc'; +import { SquareButton } from '../../components/SquareButton'; +import { BlueStorageContext } from '../../blue_modules/storage-context'; +import { useTheme } from '../../components/themes'; +import usePrivacy from '../../hooks/usePrivacy'; +import { TWallet } from '../../class/wallets/types'; +import SaveFileButton from '../../components/SaveFileButton'; + +type RootStackParamList = { + ExportMultisigCoordinationSetup: { + walletID: string; + }; +}; + +const enum ActionType { + SET_LOADING = 'SET_LOADING', + SET_SHARE_BUTTON_TAPPED = 'SET_SHARE_BUTTON_TAPPED', + SET_ERROR = 'SET_ERROR', + SET_QR_CODE_CONTENTS = 'SET_QR_CODE_CONTENTS', +} + +type State = { + isLoading: boolean; + isShareButtonTapped: boolean; + qrCodeContents?: string; + error: string | null; +}; + +type Action = + | { type: ActionType.SET_LOADING; isLoading: boolean } + | { type: ActionType.SET_SHARE_BUTTON_TAPPED; isShareButtonTapped: boolean } + | { type: ActionType.SET_ERROR; error: string | null } + | { type: ActionType.SET_QR_CODE_CONTENTS; qrCodeContents: string }; + +function reducer(state: State, action: Action): State { + switch (action.type) { + case ActionType.SET_LOADING: + return { ...state, isLoading: action.isLoading }; + case ActionType.SET_SHARE_BUTTON_TAPPED: + return { ...state, isShareButtonTapped: action.isShareButtonTapped }; + case ActionType.SET_ERROR: + return { ...state, error: action.error, isLoading: false }; + case ActionType.SET_QR_CODE_CONTENTS: + return { ...state, qrCodeContents: action.qrCodeContents, isLoading: false }; + default: + return state; + } +} + +const initialState: State = { + isLoading: true, + isShareButtonTapped: false, + qrCodeContents: undefined, + error: null, +}; + +const ExportMultisigCoordinationSetup: React.FC = () => { + const [state, dispatch] = useReducer(reducer, initialState); + const { isLoading, isShareButtonTapped, qrCodeContents } = state; + const { params } = useRoute>(); + const walletID = params.walletID; + const { wallets } = useContext(BlueStorageContext); + const wallet: TWallet | undefined = wallets.find(w => w.getID() === walletID); + const dynamicQRCode = useRef(); + const { colors } = useTheme(); + const { enableBlur, disableBlur } = usePrivacy(); + const stylesHook = StyleSheet.create({ + scrollViewContent: { + backgroundColor: colors.elevated, + }, + type: { color: colors.foregroundColor }, + secret: { color: colors.foregroundColor }, + exportButton: { + backgroundColor: colors.buttonDisabledBackgroundColor, + }, + }); + + const label = useMemo(() => wallet?.getLabel(), [wallet]); + const xpub = useMemo(() => wallet?.getXpub(), [wallet]); + + const setIsShareButtonTapped = (value: boolean) => { + dispatch({ type: ActionType.SET_SHARE_BUTTON_TAPPED, isShareButtonTapped: value }); + }; + + useFocusEffect( + useCallback(() => { + dispatch({ type: ActionType.SET_LOADING, isLoading: true }); + + const task = InteractionManager.runAfterInteractions(async () => { + enableBlur(); + if (wallet) { + try { + if (typeof xpub === 'string') { + const value = Buffer.from(xpub, 'ascii').toString('hex'); + dispatch({ type: ActionType.SET_QR_CODE_CONTENTS, qrCodeContents: value }); + } else { + throw new Error('Expected getXpub() to return a string.'); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred'; + dispatch({ type: ActionType.SET_ERROR, error: errorMessage }); + } + } else { + dispatch({ type: ActionType.SET_ERROR, error: 'Wallet not found' }); + } + }); + + return () => { + task.cancel(); + disableBlur(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [wallet]), + ); + + const exportTxtFileBeforeOnPress = async () => { + setIsShareButtonTapped(true); + dynamicQRCode.current?.stopAutoMove(); + }; + + const exportTxtFileAfterOnPress = () => { + setIsShareButtonTapped(false); + dynamicQRCode.current?.startAutoMove(); + }; + + const renderView = wallet ? ( + <> + + {wallet.getLabel()} + + + {qrCodeContents && } + + {isShareButtonTapped ? ( + + ) : ( + label && + xpub && ( + + + + ) + )} + + + {wallet.getXpub()} + + ) : null; + + return ( + + {isLoading ? : renderView} + + ); +}; + +const styles = StyleSheet.create({ + scrollViewContent: { + alignItems: 'center', + justifyContent: 'center', + flexGrow: 1, + }, + type: { + fontSize: 17, + fontWeight: '700', + }, + secret: { + alignItems: 'center', + paddingHorizontal: 16, + fontSize: 16, + lineHeight: 24, + }, + exportButton: { + height: 48, + borderRadius: 8, + flex: 1, + justifyContent: 'center', + paddingHorizontal: 16, + width: '80%', + maxWidth: 300, + }, +}); + +export default ExportMultisigCoordinationSetup; diff --git a/screen/wallets/details.js b/screen/wallets/details.js index 7b4d687a4..484b46e09 100644 --- a/screen/wallets/details.js +++ b/screen/wallets/details.js @@ -281,7 +281,7 @@ const WalletDetails = () => { navigate('ExportMultisigCoordinationSetupRoot', { screen: 'ExportMultisigCoordinationSetup', params: { - walletId: wallet.getID(), + walletID: wallet.getID(), }, }); }; diff --git a/screen/wallets/exportMultisigCoordinationSetup.js b/screen/wallets/exportMultisigCoordinationSetup.js deleted file mode 100644 index b40ba8d30..000000000 --- a/screen/wallets/exportMultisigCoordinationSetup.js +++ /dev/null @@ -1,138 +0,0 @@ -import React, { useCallback, useContext, useRef, useState } from 'react'; -import { ActivityIndicator, InteractionManager, ScrollView, StyleSheet, View } from 'react-native'; -import { useFocusEffect, useRoute } from '@react-navigation/native'; -import { BlueSpacing20, BlueText } from '../../BlueComponents'; -import navigationStyle from '../../components/navigationStyle'; -import { DynamicQRCode } from '../../components/DynamicQRCode'; -import loc from '../../loc'; -import { SquareButton } from '../../components/SquareButton'; -import { BlueStorageContext } from '../../blue_modules/storage-context'; -import { useTheme } from '../../components/themes'; -import SafeArea from '../../components/SafeArea'; -import usePrivacy from '../../hooks/usePrivacy'; -import SaveFileButton from '../../components/SaveFileButton'; - -const ExportMultisigCoordinationSetup = () => { - const walletId = useRoute().params.walletId; - const { wallets } = useContext(BlueStorageContext); - const wallet = wallets.find(w => w.getID() === walletId); - const qrCodeContents = useRef(); - const dynamicQRCode = useRef(); - const [isLoading, setIsLoading] = useState(true); - const [isShareButtonTapped, setIsShareButtonTapped] = useState(false); - const { colors } = useTheme(); - const { enableBlur, disableBlur } = usePrivacy(); - const stylesHook = StyleSheet.create({ - loading: { - backgroundColor: colors.elevated, - }, - root: { - backgroundColor: colors.elevated, - }, - type: { color: colors.foregroundColor }, - secret: { color: colors.foregroundColor }, - exportButton: { - backgroundColor: colors.buttonDisabledBackgroundColor, - }, - }); - - const exportTxtFileBeforeOnPress = async () => { - setIsShareButtonTapped(true); - dynamicQRCode.current?.stopAutoMove(); - }; - - const exportTxtFileAfterOnPress = () => { - setIsShareButtonTapped(false); - dynamicQRCode.current?.startAutoMove(); - }; - - useFocusEffect( - useCallback(() => { - enableBlur(); - const task = InteractionManager.runAfterInteractions(async () => { - if (wallet) { - qrCodeContents.current = Buffer.from(wallet.getXpub(), 'ascii').toString('hex'); - setIsLoading(false); - } - }); - return () => { - task.cancel(); - disableBlur(); - }; - }, [disableBlur, enableBlur, wallet]), - ); - - return isLoading ? ( - - - - ) : ( - - - - {wallet.getLabel()} - - - - - {isShareButtonTapped ? ( - - ) : ( - - - - )} - - {wallet.getXpub()} - - - ); -}; - -const styles = StyleSheet.create({ - loading: { - flex: 1, - justifyContent: 'center', - }, - scrollViewContent: { - alignItems: 'center', - justifyContent: 'center', - flexGrow: 1, - }, - type: { - fontSize: 17, - fontWeight: '700', - }, - secret: { - alignItems: 'center', - paddingHorizontal: 16, - fontSize: 16, - lineHeight: 24, - }, - exportButton: { - height: 48, - borderRadius: 8, - flex: 1, - justifyContent: 'center', - paddingHorizontal: 16, - width: '80%', - maxWidth: 300, - }, -}); - -ExportMultisigCoordinationSetup.navigationOptions = navigationStyle( - { - closeButton: true, - headerBackVisible: false, - statusBarStyle: 'light', - }, - opts => ({ ...opts, title: loc.multisig.export_coordination_setup }), -); - -export default ExportMultisigCoordinationSetup;