REF: Reuse existing component

This commit is contained in:
Marcos Rodriguez Velez 2025-02-23 18:18:12 -04:00
parent 34db010bde
commit 7f97c340f8
6 changed files with 181 additions and 170 deletions

View file

@ -1,7 +1,7 @@
import React, { useCallback, useMemo } from 'react';
import React, { useCallback, useMemo, forwardRef } from 'react';
import { Image, Keyboard, Platform, StyleSheet, Text } from 'react-native';
import Clipboard from '@react-native-clipboard/clipboard';
import ToolTipMenu from './TooltipMenu';
import ToolTipMenu, { ToolTipMenuRef } from './TooltipMenu';
import loc from '../loc';
import { showFilePickerAndReadFile, showImagePickerAndReadImage } from '../blue_modules/fs';
import presentAlert from './Alert';
@ -10,145 +10,160 @@ import RNQRGenerator from 'rn-qr-generator';
import { CommonToolTipActions } from '../typings/CommonToolTipActions';
import { useSettings } from '../hooks/context/useSettings';
import { useExtendedNavigation } from '../hooks/useExtendedNavigation';
import { isDesktop } from '../blue_modules/environment';
interface AddressInputScanButtonProps {
isLoading: boolean;
isLoading?: boolean;
onChangeText: (text: string) => void;
type?: 'default' | 'link';
testID?: string;
beforePress?: () => Promise<void> | void;
}
export const AddressInputScanButton = ({
isLoading,
export const AddressInputScanButton = forwardRef<ToolTipMenuRef, AddressInputScanButtonProps>(
({ isLoading, onChangeText, type = 'default', testID = 'BlueAddressInputScanQrButton', beforePress }, ref) => {
const { colors } = useTheme();
const { isClipboardGetContentEnabled } = useSettings();
onChangeText,
}: AddressInputScanButtonProps) => {
const { colors } = useTheme();
const { isClipboardGetContentEnabled } = useSettings();
const navigation = useExtendedNavigation();
const stylesHook = StyleSheet.create({
scan: {
backgroundColor: colors.scanLabel,
},
scanText: {
color: colors.inverseForegroundColor,
},
});
const toolTipOnPress = useCallback(async () => {
Keyboard.dismiss();
navigation.navigate('ScanQRCode', {
showFileImportButton: true,
});
}, [navigation]);
const actions = useMemo(() => {
const availableActions = [
CommonToolTipActions.ScanQR,
CommonToolTipActions.ChoosePhoto,
CommonToolTipActions.ImportFile,
{
...CommonToolTipActions.PasteFromClipboard,
hidden: !isClipboardGetContentEnabled,
const navigation = useExtendedNavigation();
const stylesHook = StyleSheet.create({
scan: {
backgroundColor: colors.scanLabel,
},
];
scanText: {
color: colors.inverseForegroundColor,
},
});
return availableActions;
}, [isClipboardGetContentEnabled]);
const onMenuItemPressed = useCallback(
async (action: string) => {
switch (action) {
case CommonToolTipActions.ScanQR.id:
navigation.navigate('ScanQRCode', {
showFileImportButton: true,
});
break;
case CommonToolTipActions.PasteFromClipboard.id:
try {
let getImage: string | null = null;
let hasImage = false;
if (Platform.OS === 'android') {
hasImage = true;
} else {
hasImage = await Clipboard.hasImage();
}
if (hasImage) {
getImage = await Clipboard.getImage();
}
if (getImage) {
try {
const base64Data = getImage.replace(/^data:image\/(png|jpeg|jpg);base64,/, '');
const values = await RNQRGenerator.detect({
base64: base64Data,
});
if (values && values.values.length > 0) {
onChangeText(values.values[0]);
} else {
presentAlert({ message: loc.send.qr_error_no_qrcode });
}
} catch (error) {
presentAlert({ message: (error as Error).message });
}
} else {
const clipboardText = await Clipboard.getString();
onChangeText(clipboardText);
}
} catch (error) {
presentAlert({ message: (error as Error).message });
}
break;
case CommonToolTipActions.ChoosePhoto.id:
showImagePickerAndReadImage()
.then(value => {
if (value) {
onChangeText(value);
}
})
.catch(error => {
presentAlert({ message: error.message });
});
break;
case CommonToolTipActions.ImportFile.id:
showFilePickerAndReadFile()
.then(value => {
if (value.data) {
onChangeText(value.data);
}
})
.catch(error => {
presentAlert({ message: error.message });
});
break;
const toolTipOnPress = useCallback(async () => {
if (beforePress) {
await beforePress();
}
Keyboard.dismiss();
},
[navigation, onChangeText],
);
navigation.navigate('ScanQRCode', {
showFileImportButton: true,
});
}, [navigation, beforePress]);
const buttonStyle = useMemo(() => [styles.scan, stylesHook.scan], [stylesHook.scan]);
const actions = useMemo(() => {
const availableActions = [
CommonToolTipActions.ScanQR,
CommonToolTipActions.ChoosePhoto,
CommonToolTipActions.ImportFile,
{
...CommonToolTipActions.PasteFromClipboard,
hidden: !isClipboardGetContentEnabled,
},
];
return (
<ToolTipMenu
actions={actions}
isButton
onPressMenuItem={onMenuItemPressed}
testID="BlueAddressInputScanQrButton"
disabled={isLoading}
onPress={toolTipOnPress}
buttonStyle={buttonStyle}
accessibilityLabel={loc.send.details_scan}
accessibilityHint={loc.send.details_scan_hint}
>
<Image source={require('../img/scan-white.png')} accessible={false} />
<Text style={[styles.scanText, stylesHook.scanText]} accessible={false}>
{loc.send.details_scan}
</Text>
</ToolTipMenu>
);
};
return availableActions;
}, [isClipboardGetContentEnabled]);
const onMenuItemPressed = useCallback(
async (action: string) => {
switch (action) {
case CommonToolTipActions.ScanQR.id:
navigation.navigate('ScanQRCode', {
showFileImportButton: true,
});
break;
case CommonToolTipActions.PasteFromClipboard.id:
try {
let getImage: string | null = null;
let hasImage = false;
if (Platform.OS === 'android') {
hasImage = true;
} else {
hasImage = await Clipboard.hasImage();
}
if (hasImage) {
getImage = await Clipboard.getImage();
}
if (getImage) {
try {
const base64Data = getImage.replace(/^data:image\/(png|jpeg|jpg);base64,/, '');
const values = await RNQRGenerator.detect({
base64: base64Data,
});
if (values && values.values.length > 0) {
onChangeText(values.values[0]);
} else {
presentAlert({ message: loc.send.qr_error_no_qrcode });
}
} catch (error) {
presentAlert({ message: (error as Error).message });
}
} else {
const clipboardText = await Clipboard.getString();
onChangeText(clipboardText);
}
} catch (error) {
presentAlert({ message: (error as Error).message });
}
break;
case CommonToolTipActions.ChoosePhoto.id:
showImagePickerAndReadImage()
.then(value => {
if (value) {
onChangeText(value);
}
})
.catch(error => {
presentAlert({ message: error.message });
});
break;
case CommonToolTipActions.ImportFile.id:
showFilePickerAndReadFile()
.then(value => {
if (value.data) {
onChangeText(value.data);
}
})
.catch(error => {
presentAlert({ message: error.message });
});
break;
}
Keyboard.dismiss();
},
[navigation, onChangeText],
);
const buttonStyle = useMemo(() => [styles.scan, stylesHook.scan], [stylesHook.scan]);
return (
<ToolTipMenu
ref={ref}
actions={actions}
isButton
onPressMenuItem={onMenuItemPressed}
testID={testID}
disabled={isLoading}
onPress={toolTipOnPress}
isMenuPrimaryAction={isDesktop}
buttonStyle={type === 'default' ? buttonStyle : undefined}
accessibilityLabel={loc.send.details_scan}
accessibilityHint={loc.send.details_scan_hint}
>
{type === 'default' ? (
<>
<Image source={require('../img/scan-white.png')} accessible={false} />
<Text style={[styles.scanText, stylesHook.scanText]} accessible={false}>
{loc.send.details_scan}
</Text>
</>
) : (
<Text style={[styles.linkText, { color: colors.foregroundColor }]}>{loc.wallets.import_scan_qr}</Text>
)}
</ToolTipMenu>
);
},
);
AddressInputScanButton.displayName = 'AddressInputScanButton';
const styles = StyleSheet.create({
scan: {
@ -164,4 +179,8 @@ const styles = StyleSheet.create({
scanText: {
marginLeft: 4,
},
linkText: {
textAlign: 'center',
fontSize: 16,
},
});

View file

@ -1,11 +1,17 @@
import React, { Ref, useCallback, useMemo } from 'react';
import React, { forwardRef, useCallback, useMemo } from 'react';
import { Platform, Pressable, TouchableOpacity } from 'react-native';
import { MenuView, MenuAction, NativeActionEvent } from '@react-native-menu/menu';
import { ContextMenuView, RenderItem, OnPressMenuItemEventObject, IconConfig, MenuElementConfig } from 'react-native-ios-context-menu';
import { ToolTipMenuProps, Action } from './types';
import { useSettings } from '../hooks/context/useSettings';
const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
export type ToolTipMenuRef = {
// Add any methods you want to expose via ref
showMenu?: () => void;
hideMenu?: () => void;
};
const ToolTipMenu = forwardRef<ToolTipMenuRef, ToolTipMenuProps>((props, _ref) => {
const {
title = '',
isMenuPrimaryAction = false,

View file

@ -366,4 +366,4 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
</ToolTipMenu>
);
},
);
);

View file

@ -3,7 +3,7 @@ import { RouteProp, useRoute } from '@react-navigation/native';
import Clipboard from '@react-native-clipboard/clipboard';
import { Keyboard, Platform, ScrollView, StyleSheet, TouchableWithoutFeedback, View, TouchableOpacity, Image } from 'react-native';
import { disallowScreenshot } from 'react-native-screen-capture';
import { BlueButtonLink, BlueFormLabel, BlueFormMultiInput, BlueSpacing20 } from '../../BlueComponents';
import { BlueFormLabel, BlueFormMultiInput, BlueSpacing20 } from '../../BlueComponents';
import Button from '../../components/Button';
import {
DoneAndDismissKeyboardInputAccessory,
@ -19,6 +19,7 @@ import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
import { AddWalletStackParamList } from '../../navigation/AddWalletStack';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { isDesktop } from '../../blue_modules/environment';
import { AddressInputScanButton } from '../../components/AddressInputScanButton';
type RouteProps = RouteProp<AddWalletStackParamList, 'ImportWallet'>;
type NavigationProps = NativeStackNavigationProp<AddWalletStackParamList, 'ImportWallet'>;
@ -109,12 +110,6 @@ const ImportWallet = () => {
[importMnemonic],
);
const importScan = useCallback(async () => {
navigation.navigate('ScanQRCode', {
showFileImportButton: true,
});
}, [navigation]);
useEffect(() => {
const data = route.params?.onBarScanned;
if (data) {
@ -199,7 +194,7 @@ const ImportWallet = () => {
<>
<Button disabled={importText.trim().length === 0} title={loc.wallets.import_do_import} testID="DoImport" onPress={handleImport} />
<BlueSpacing20 />
<BlueButtonLink title={loc.wallets.import_scan_qr} onPress={importScan} testID="ScanImport" />
<AddressInputScanButton type="link" onChangeText={setImportText} />
</>
</View>
</>

View file

@ -18,15 +18,7 @@ import {
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 { 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';
@ -45,13 +37,14 @@ 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 ToolTipMenu, { ToolTipMenuRef } from '../../components/TooltipMenu';
import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
import { useSettings } from '../../hooks/context/useSettings';
import { ViewEditMultisigCosignersStackParamList } from '../../navigation/ViewEditMultisigCosignersStack';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import SafeArea from '../../components/SafeArea';
import { TWallet } from '../../class/wallets/types';
import { AddressInputScanButton } from '../../components/AddressInputScanButton';
type RouteParams = RouteProp<ViewEditMultisigCosignersStackParamList, 'ViewEditMultisigCosigners'>;
type NavigationProp = NativeStackNavigationProp<ViewEditMultisigCosignersStackParamList, 'ViewEditMultisigCosigners'>;
@ -62,8 +55,8 @@ const ViewEditMultisigCosigners: React.FC = () => {
const { wallets, setWalletsWithNewOrder } = useStorage();
const { isBiometricUseCapableAndEnabled } = useBiometrics();
const { isElectrumDisabled, isPrivacyBlurEnabled } = useSettings();
const { navigate, dispatch, setParams, setOptions } = useExtendedNavigation<NavigationProp>();
const openScannerButtonRef = useRef();
const { dispatch, setParams, setOptions } = useExtendedNavigation<NavigationProp>();
const openScannerButtonRef = useRef<ToolTipMenuRef>(null);
const route = useRoute<RouteParams>();
const { walletID } = route.params;
const w = useRef(wallets.find(wallet => wallet.getID() === walletID));
@ -516,11 +509,6 @@ const ViewEditMultisigCosigners: React.FC = () => {
});
};
const scanOrOpenFile = async () => {
await provideMnemonicsModalRef.current?.dismiss();
navigate('ScanQRCode', { showFileImportButton: true });
};
useEffect(() => {
const scannedData = route.params.onBarScanned;
if (scannedData) {
@ -573,11 +561,14 @@ const ViewEditMultisigCosigners: React.FC = () => {
{!isLoading && (
<>
<BlueButtonLink
<AddressInputScanButton
ref={openScannerButtonRef}
disabled={isLoading}
onPress={scanOrOpenFile}
title={loc.wallets.import_scan_qr}
beforePress={async () => {
await provideMnemonicsModalRef.current?.dismiss();
}}
isLoading={isLoading}
type="link"
onChangeText={setImportText}
/>
<BlueSpacing20 />
</>

View file

@ -17,7 +17,7 @@ import { Icon } from '@rneui/themed';
import A from '../../blue_modules/analytics';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import { encodeUR } from '../../blue_modules/ur';
import { BlueButtonLink, BlueFormMultiInput, BlueSpacing10, BlueSpacing20, BlueTextCentered } from '../../BlueComponents';
import { BlueFormMultiInput, BlueSpacing10, BlueSpacing20, BlueTextCentered } from '../../BlueComponents';
import { HDSegwitBech32Wallet, MultisigCosigner, MultisigHDWallet } from '../../class';
import presentAlert from '../../components/Alert';
import BottomModal from '../../components/BottomModal';
@ -44,6 +44,7 @@ import MultipleStepsListItem, {
MultipleStepsListItemButtonType,
MultipleStepsListItemDashType,
} from '../../components/MultipleStepsListItem';
import { AddressInputScanButton } from '../../components/AddressInputScanButton';
const staticCache = {};
@ -433,11 +434,6 @@ const WalletsAddMultisigStep2 = () => {
[cosigners, format, getXpubCacheForMnemonics, tryUsingXpub],
);
const scanOrOpenFile = async () => {
await provideMnemonicsModalRef.current.dismiss();
navigation.navigate('ScanQRCode', { showFileImportButton: true });
};
const utilizeMnemonicPhrase = useCallback(async () => {
try {
await provideMnemonicsModalRef.current.dismiss();
@ -659,12 +655,16 @@ const WalletsAddMultisigStep2 = () => {
onPress={utilizeMnemonicPhrase}
/>
<View style={styles.height16} />
<BlueButtonLink
<AddressInputScanButton
beforePress={async () => {
await provideMnemonicsModalRef.current.dismiss();
}}
onBarScanned={onBarScanned}
testID="ScanOrOpenFile"
type="link"
ref={openScannerButton}
disabled={isLoading}
onPress={scanOrOpenFile}
title={loc.wallets.import_scan_qr}
/>
</>
)}