From 1bd239bf782d3b5a69665423e2058898c1266f6b Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Sun, 16 Jun 2024 22:26:43 -0400 Subject: [PATCH] REF: Send options to use Tooltip --- components/TooltipMenu.android.tsx | 71 ----- components/TooltipMenu.ios.tsx | 140 -------- components/TooltipMenu.tsx | 145 ++++++++- components/types.ts | 2 +- loc/en.json | 1 - screen/send/SendDetails.tsx | 497 +++++++++++++++-------------- 6 files changed, 391 insertions(+), 465 deletions(-) delete mode 100644 components/TooltipMenu.android.tsx delete mode 100644 components/TooltipMenu.ios.tsx diff --git a/components/TooltipMenu.android.tsx b/components/TooltipMenu.android.tsx deleted file mode 100644 index 1e4adf942..000000000 --- a/components/TooltipMenu.android.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React, { forwardRef, useCallback, useMemo } from 'react'; -import { Pressable } from 'react-native'; -import { MenuView, NativeActionEvent } from '@react-native-menu/menu'; -import { ToolTipMenuProps } from './types'; - -const BaseToolTipMenu = (props: ToolTipMenuProps, ref: any) => { - const { - actions, - children, - onPressMenuItem, - isMenuPrimaryAction = false, - buttonStyle = {}, - enableAndroidRipple = true, - disabled = false, - onPress, - title = 'Menu', - ...restProps - } = props; - - console.debug('ToolTipMenu.android.tsx ref:', ref); - - const menuItems = useMemo(() => { - return actions.flatMap(action => { - if (Array.isArray(action)) { - return action.map(actionToMap => ({ - id: actionToMap.id.toString(), - title: actionToMap.text, - titleColor: actionToMap.disabled ? 'gray' : 'black', - image: actionToMap.icon.iconValue, - imageColor: actionToMap.disabled ? 'gray' : 'black', - attributes: { disabled: actionToMap.disabled }, - })); - } - return { - id: action.id.toString(), - title: action.text, - titleColor: action.disabled ? 'gray' : 'black', - image: action.icon.iconValue, - imageColor: action.disabled ? 'gray' : 'black', - attributes: { disabled: action.disabled }, - }; - }); - }, [actions]); - - const handleToolTipSelection = useCallback( - ({ nativeEvent }: NativeActionEvent) => { - if (nativeEvent) { - onPressMenuItem(nativeEvent.event); - } - }, - [onPressMenuItem], - ); - - return ( - - {} } : { onPress })} - {...restProps} - > - {children} - - - ); -}; - -const ToolTipMenu = BaseToolTipMenu; - -export default forwardRef(ToolTipMenu); diff --git a/components/TooltipMenu.ios.tsx b/components/TooltipMenu.ios.tsx deleted file mode 100644 index d8917501a..000000000 --- a/components/TooltipMenu.ios.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import React, { forwardRef, Ref, useCallback, useMemo } from 'react'; -import { Pressable, TouchableOpacity, View } from 'react-native'; -import { ContextMenuView, RenderItem, OnPressMenuItemEventObject, MenuState, IconConfig } from 'react-native-ios-context-menu'; -import { MenuView, MenuAction, NativeActionEvent } from '@react-native-menu/menu'; -import { ToolTipMenuProps, Action } from './types'; - -const BaseToolTipMenu = (props: ToolTipMenuProps, ref: Ref) => { - const { - title = '', - isMenuPrimaryAction = false, - renderPreview, - disabled = false, - onPress, - onMenuWillShow, - onMenuWillHide, - buttonStyle, - onPressMenuItem, - children, - ...restProps - } = props; - - const mapMenuItemForContextMenuView = useCallback((action: Action) => { - return { - actionKey: action.id.toString(), - actionTitle: action.text, - icon: action.icon?.iconValue ? ({ iconType: 'SYSTEM', iconValue: action.icon.iconValue } as IconConfig) : undefined, - state: action.menuStateOn ? ('on' as MenuState) : ('off' as MenuState), - attributes: action.disabled ? ['disabled'] : [], - }; - }, []); - - const mapMenuItemForMenuView = useCallback((action: Action): MenuAction => { - return { - id: action.id.toString(), - title: action.text, - image: action.icon?.iconValue || undefined, - state: action.menuStateOn ? ('on' as MenuState) : undefined, - attributes: { disabled: action.disabled }, - }; - }, []); - - const contextMenuItems = useMemo(() => { - const flattenedActions = props.actions.flat(); - return flattenedActions.map(mapMenuItemForContextMenuView); - }, [props.actions, mapMenuItemForContextMenuView]); - - const menuViewItems = useMemo(() => { - return props.actions.map(actionGroup => { - if (Array.isArray(actionGroup)) { - return { - id: actionGroup[0].id.toString(), - title: '', - subactions: actionGroup.map(mapMenuItemForMenuView), - displayInline: true, - }; - } else { - return mapMenuItemForMenuView(actionGroup); - } - }); - }, [props.actions, mapMenuItemForMenuView]); - - const handlePressMenuItemForContextMenuView = useCallback( - (event: OnPressMenuItemEventObject) => { - onPressMenuItem(event.nativeEvent.actionKey); - }, - [onPressMenuItem], - ); - - const handlePressMenuItemForMenuView = useCallback( - ({ nativeEvent }: NativeActionEvent) => { - onPressMenuItem(nativeEvent.event); - }, - [onPressMenuItem], - ); - - const renderContextMenuView = () => { - console.debug('ToolTipMenu.tsx rendering: renderContextMenuView'); - return ( - - {onPress ? ( - - {children} - - ) : ( - children - )} - - ); - }; - - const renderMenuView = () => { - console.debug('ToolTipMenu.tsx rendering: renderMenuView'); - return ( - - - {isMenuPrimaryAction ? ( - - {children} - - ) : ( - children - )} - - - ); - }; - - return renderPreview ? renderContextMenuView() : renderMenuView(); -}; - -const ToolTipMenu = forwardRef(BaseToolTipMenu); - -export default ToolTipMenu; diff --git a/components/TooltipMenu.tsx b/components/TooltipMenu.tsx index 629dc02c4..4d309afe7 100644 --- a/components/TooltipMenu.tsx +++ b/components/TooltipMenu.tsx @@ -1,10 +1,145 @@ -import { forwardRef, Ref } from 'react'; - -import { ToolTipMenuProps } from './types'; +import React, { forwardRef, Ref, useCallback, useMemo } from 'react'; +import { Platform, Pressable, TouchableOpacity, View } from 'react-native'; +import { ContextMenuView, RenderItem, OnPressMenuItemEventObject, MenuState, IconConfig } from 'react-native-ios-context-menu'; +import { MenuView, MenuAction, NativeActionEvent } from '@react-native-menu/menu'; +import { ToolTipMenuProps, Action } from './types'; const BaseToolTipMenu = (props: ToolTipMenuProps, ref: Ref) => { - console.debug('ToolTipMenu.tsx ref:', ref); - return props.children; + const { + title = '', + isMenuPrimaryAction = false, + renderPreview, + disabled = false, + onPress, + onMenuWillShow, + onMenuWillHide, + buttonStyle, + onPressMenuItem, + children, + isButton = false, + ...restProps + } = props; + + const mapMenuItemForContextMenuView = useCallback((action: Action) => { + return { + actionKey: action.id.toString(), + actionTitle: action.text, + icon: action.icon?.iconValue ? ({ iconType: 'SYSTEM', iconValue: action.icon.iconValue } as IconConfig) : undefined, + state: action.menuStateOn ? ('on' as MenuState) : ('off' as MenuState), + attributes: action.disabled ? ['disabled'] : [], + }; + }, []); + + const mapMenuItemForMenuView = useCallback((action: Action): MenuAction => { + return { + id: action.id.toString(), + title: action.text, + image: action.menuStateOn && Platform.OS === 'android' ? 'checkbox_on_background' : action.icon?.iconValue || undefined, + state: action.menuStateOn ? ('on' as MenuState) : undefined, + attributes: { disabled: action.disabled }, + }; + }, []); + + const contextMenuItems = useMemo(() => { + const flattenedActions = props.actions.flat(); + return flattenedActions.map(mapMenuItemForContextMenuView); + }, [props.actions, mapMenuItemForContextMenuView]); + + const menuViewItemsIOS = useMemo(() => { + return props.actions.map(actionGroup => { + if (Array.isArray(actionGroup)) { + return { + id: actionGroup[0].id.toString(), + title: '', + subactions: actionGroup.map(mapMenuItemForMenuView), + displayInline: true, + }; + } else { + return mapMenuItemForMenuView(actionGroup); + } + }); + }, [props.actions, mapMenuItemForMenuView]); + + const menuViewItemsAndroid = useMemo(() => { + const mergedActions = props.actions.flat(); + return mergedActions.map(mapMenuItemForMenuView); + }, [props.actions, mapMenuItemForMenuView]); + + const handlePressMenuItemForContextMenuView = useCallback( + (event: OnPressMenuItemEventObject) => { + onPressMenuItem(event.nativeEvent.actionKey); + }, + [onPressMenuItem], + ); + + const handlePressMenuItemForMenuView = useCallback( + ({ nativeEvent }: NativeActionEvent) => { + onPressMenuItem(nativeEvent.event); + }, + [onPressMenuItem], + ); + + const renderContextMenuView = () => { + console.debug('ToolTipMenu.tsx rendering: renderContextMenuView'); + return ( + + {onPress ? ( + + {children} + + ) : ( + children + )} + + ); + }; + + const renderMenuView = () => { + console.debug('ToolTipMenu.tsx rendering: renderMenuView'); + return ( + + + {isMenuPrimaryAction || isButton ? ( + + {children} + + ) : ( + children + )} + + + ); + }; + + return Platform.OS === 'ios' && renderPreview ? renderContextMenuView() : renderMenuView(); }; const ToolTipMenu = forwardRef(BaseToolTipMenu); diff --git a/components/types.ts b/components/types.ts index ef9d3c2c9..8b4f1def9 100644 --- a/components/types.ts +++ b/components/types.ts @@ -3,7 +3,7 @@ import { AccessibilityRole, ViewStyle } from 'react-native'; export interface Action { id: string | number; text: string; - icon: { + icon?: { iconType: string; iconValue: string; }; diff --git a/loc/en.json b/loc/en.json index c9ca242bd..ef523da2e 100644 --- a/loc/en.json +++ b/loc/en.json @@ -21,7 +21,6 @@ "close": "Close", "change_input_currency": "Change input currency", "refresh": "Refresh", - "more": "More", "pick_image": "Choose image from library", "pick_file": "Choose a file", "enter_amount": "Enter amount", diff --git a/screen/send/SendDetails.tsx b/screen/send/SendDetails.tsx index cde41a14a..9bcb5e959 100644 --- a/screen/send/SendDetails.tsx +++ b/screen/send/SendDetails.tsx @@ -57,6 +57,7 @@ import { isTablet } from '../../blue_modules/environment'; import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; import { ContactList } from '../../class/contact-list'; import { useStorage } from '../../hooks/context/useStorage'; +import { Action } from '../../components/types'; interface IPaymentDestinations { address: string; // btc address or payment code @@ -686,12 +687,40 @@ const SendDetails = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [routeParams.walletID]); + const importQrTransactionOnBarScanned = useCallback( + (ret: any) => { + navigation.getParent()?.getParent()?.dispatch(popAction); + if (!wallet) return; + if (!ret.data) ret = { data: ret }; + if (ret.data.toUpperCase().startsWith('UR')) { + presentAlert({ title: loc.errors.error, message: 'BC-UR not decoded. This should never happen' }); + } else if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1 && ret.data.indexOf('=') === -1) { + // this looks like NOT base64, so maybe its transaction's hex + // we dont support it in this flow + } else { + // psbt base64? + + // we construct PSBT object and pass to next screen + // so user can do smth with it: + const psbt = bitcoin.Psbt.fromBase64(ret.data); + navigation.navigate('PsbtWithHardwareWallet', { + memo: transactionMemo, + fromWallet: wallet, + psbt, + }); + setIsLoading(false); + setOptionsVisible(false); + } + }, + [navigation, popAction, wallet, transactionMemo], + ); + /** * same as `importTransaction`, but opens camera instead. * * @returns {Promise} */ - const importQrTransaction = () => { + const importQrTransaction = useCallback(() => { if (wallet?.type !== WatchOnlyWallet.type) { return presentAlert({ title: loc.errors.error, message: 'Importing transaction in non-watchonly wallet (this should never happen)' }); } @@ -706,32 +735,7 @@ const SendDetails = () => { }, }); }); - }; - - const importQrTransactionOnBarScanned = (ret: any) => { - navigation.getParent()?.getParent()?.dispatch(popAction); - if (!wallet) return; - if (!ret.data) ret = { data: ret }; - if (ret.data.toUpperCase().startsWith('UR')) { - presentAlert({ title: loc.errors.error, message: 'BC-UR not decoded. This should never happen' }); - } else if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1 && ret.data.indexOf('=') === -1) { - // this looks like NOT base64, so maybe its transaction's hex - // we dont support it in this flow - } else { - // psbt base64? - - // we construct PSBT object and pass to next screen - // so user can do smth with it: - const psbt = bitcoin.Psbt.fromBase64(ret.data); - navigation.navigate('PsbtWithHardwareWallet', { - memo: transactionMemo, - fromWallet: wallet, - psbt, - }); - setIsLoading(false); - setOptionsVisible(false); - } - }; + }, [wallet?.type, navigation, importQrTransactionOnBarScanned]); /** * watch-only wallets with enabled HW wallet support have different flow. we have to show PSBT to user as QR code @@ -741,7 +745,7 @@ const SendDetails = () => { * * @returns {Promise} */ - const importTransaction = async () => { + const importTransaction = useCallback(async () => { if (wallet?.type !== WatchOnlyWallet.type) { return presentAlert({ title: loc.errors.error, message: 'Importing transaction in non-watchonly wallet (this should never happen)' }); } @@ -792,7 +796,7 @@ const SendDetails = () => { presentAlert({ title: loc.errors.error, message: loc.send.details_no_signed_tx }); } } - }; + }, [wallet, navigation, transactionMemo]); const askCosignThisTransaction = async () => { return new Promise(resolve => { @@ -815,54 +819,65 @@ const SendDetails = () => { }); }; - const _importTransactionMultisig = async (base64arg: string | false) => { - try { - const base64 = base64arg || (await fs.openSignedTransaction()); - if (!base64) return; - const psbt = bitcoin.Psbt.fromBase64(base64); // if it doesnt throw - all good, its valid - - if ((wallet as MultisigHDWallet)?.howManySignaturesCanWeMake() > 0 && (await askCosignThisTransaction())) { - hideOptions(); - setIsLoading(true); - await sleep(100); - (wallet as MultisigHDWallet).cosignPsbt(psbt); - setIsLoading(false); - await sleep(100); - } - - if (wallet) { - navigation.navigate('PsbtMultisig', { - memo: transactionMemo, - psbtBase64: psbt.toBase64(), - walletID: wallet.getID(), - }); - } - } catch (error: any) { - presentAlert({ title: loc.send.problem_with_psbt, message: error.message }); - } - setIsLoading(false); + const hideOptions = useCallback(() => { + Keyboard.dismiss(); setOptionsVisible(false); - }; + }, []); - const importTransactionMultisig = () => { + const _importTransactionMultisig = useCallback( + async (base64arg: string | false) => { + try { + const base64 = base64arg || (await fs.openSignedTransaction()); + if (!base64) return; + const psbt = bitcoin.Psbt.fromBase64(base64); // if it doesnt throw - all good, its valid + + if ((wallet as MultisigHDWallet)?.howManySignaturesCanWeMake() > 0 && (await askCosignThisTransaction())) { + hideOptions(); + setIsLoading(true); + await sleep(100); + (wallet as MultisigHDWallet).cosignPsbt(psbt); + setIsLoading(false); + await sleep(100); + } + + if (wallet) { + navigation.navigate('PsbtMultisig', { + memo: transactionMemo, + psbtBase64: psbt.toBase64(), + walletID: wallet.getID(), + }); + } + } catch (error: any) { + presentAlert({ title: loc.send.problem_with_psbt, message: error.message }); + } + setIsLoading(false); + setOptionsVisible(false); + }, + [wallet, hideOptions, sleep, navigation, transactionMemo], + ); + + const importTransactionMultisig = useCallback(() => { return _importTransactionMultisig(false); - }; + }, [_importTransactionMultisig]); - const onBarScanned = (ret: any) => { - navigation.getParent()?.dispatch(popAction); - if (!ret.data) ret = { data: ret }; - if (ret.data.toUpperCase().startsWith('UR')) { - presentAlert({ title: loc.errors.error, message: 'BC-UR not decoded. This should never happen' }); - } else if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1 && ret.data.indexOf('=') === -1) { - // this looks like NOT base64, so maybe its transaction's hex - // we dont support it in this flow - } else { - // psbt base64? - return _importTransactionMultisig(ret.data); - } - }; + const onBarScanned = useCallback( + (ret: any) => { + navigation.getParent()?.dispatch(popAction); + if (!ret.data) ret = { data: ret }; + if (ret.data.toUpperCase().startsWith('UR')) { + presentAlert({ title: loc.errors.error, message: 'BC-UR not decoded. This should never happen' }); + } else if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1 && ret.data.indexOf('=') === -1) { + // this looks like NOT base64, so maybe its transaction's hex + // we dont support it in this flow + } else { + // psbt base64? + return _importTransactionMultisig(ret.data); + } + }, + [navigation, popAction, _importTransactionMultisig], + ); - const importTransactionMultisigScanQr = () => { + const importTransactionMultisigScanQr = useCallback(() => { setOptionsVisible(false); requestCameraAuthorization().then(() => { navigation.navigate('ScanQRCodeRoot', { @@ -873,9 +888,9 @@ const SendDetails = () => { }, }); }); - }; + }, [navigation, onBarScanned]); - const handleAddRecipient = async () => { + const handleAddRecipient = useCallback(async () => { console.log('handleAddRecipient'); setAddresses(addrs => [...addrs, { address: '', key: String(Math.random()) } as IPaymentDestinations]); setOptionsVisible(false); @@ -883,9 +898,9 @@ const SendDetails = () => { scrollView.current?.scrollToEnd(); if (addresses.length === 0) return; scrollView.current?.flashScrollIndicators(); - }; + }, [addresses.length, sleep]); - const handleRemoveRecipient = async () => { + const handleRemoveRecipient = useCallback(async () => { const last = scrollIndex.current === addresses.length - 1; LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); setAddresses(addrs => { @@ -897,24 +912,24 @@ const SendDetails = () => { await sleep(200); // wait for animation scrollView.current?.flashScrollIndicators(); if (last && Platform.OS === 'android') scrollView.current?.scrollToEnd(); // fix white screen on android - }; + }, [addresses.length, sleep]); - const handleCoinControl = () => { + const handleCoinControl = useCallback(() => { if (!wallet) return; setOptionsVisible(false); navigation.navigate('CoinControl', { walletID: wallet?.getID(), onUTXOChoose: (u: CreateTransactionUtxo[]) => setUtxo(u), }); - }; + }, [wallet, setOptionsVisible, navigation]); - const handleInsertContact = () => { + const handleInsertContact = useCallback(() => { if (!wallet) return; setOptionsVisible(false); navigation.navigate('PaymentCodeList', { walletID: wallet.getID() }); - }; + }, [wallet, navigation, setOptionsVisible]); - const handlePsbtSign = async () => { + const handlePsbtSign = useCallback(async () => { setIsLoading(true); setOptionsVisible(false); await new Promise(resolve => setTimeout(resolve, 100)); // sleep for animations @@ -954,170 +969,9 @@ const SendDetails = () => { showAnimatedQr: true, psbt, }); - }; + }, [name, navigation, wallet]); - const hideOptions = () => { - Keyboard.dismiss(); - setOptionsVisible(false); - }; - - // Header Right Button - - const headerRightOnPress = (id: string) => { - if (id === SendDetails.actionKeys.AddRecipient) { - handleAddRecipient(); - } else if (id === SendDetails.actionKeys.RemoveRecipient) { - handleRemoveRecipient(); - } else if (id === SendDetails.actionKeys.SignPSBT) { - handlePsbtSign(); - } else if (id === SendDetails.actionKeys.SendMax) { - onUseAllPressed(); - } else if (id === SendDetails.actionKeys.AllowRBF) { - onReplaceableFeeSwitchValueChanged(!isTransactionReplaceable); - } else if (id === SendDetails.actionKeys.ImportTransaction) { - importTransaction(); - } else if (id === SendDetails.actionKeys.ImportTransactionQR) { - importQrTransaction(); - } else if (id === SendDetails.actionKeys.ImportTransactionMultsig) { - importTransactionMultisig(); - } else if (id === SendDetails.actionKeys.CoSignTransaction) { - importTransactionMultisigScanQr(); - } else if (id === SendDetails.actionKeys.CoinControl) { - handleCoinControl(); - } else if (id === SendDetails.actionKeys.InsertContact) { - handleInsertContact(); - } - }; - - const headerRightActions = () => { - const actions = []; - if (isEditable) { - if (wallet?.allowBIP47() && wallet?.isBIP47Enabled()) { - actions.push([ - { id: SendDetails.actionKeys.InsertContact, text: loc.send.details_insert_contact, icon: SendDetails.actionIcons.InsertContact }, - ]); - } - - if (Number(wallet?.getBalance()) > 0) { - const isSendMaxUsed = addresses.some(element => element.amount === BitcoinUnit.MAX); - - actions.push([{ id: SendDetails.actionKeys.SendMax, text: loc.send.details_adv_full, disabled: balance === 0 || isSendMaxUsed }]); - } - if (wallet?.type === HDSegwitBech32Wallet.type) { - actions.push([{ id: SendDetails.actionKeys.AllowRBF, text: loc.send.details_adv_fee_bump, menuStateOn: isTransactionReplaceable }]); - } - const transactionActions = []; - if (wallet?.type === WatchOnlyWallet.type && wallet.isHd()) { - transactionActions.push( - { - id: SendDetails.actionKeys.ImportTransaction, - text: loc.send.details_adv_import, - icon: SendDetails.actionIcons.ImportTransaction, - }, - { - id: SendDetails.actionKeys.ImportTransactionQR, - text: loc.send.details_adv_import_qr, - icon: SendDetails.actionIcons.ImportTransactionQR, - }, - ); - } - if (wallet?.type === MultisigHDWallet.type) { - transactionActions.push({ - id: SendDetails.actionKeys.ImportTransactionMultsig, - text: loc.send.details_adv_import, - icon: SendDetails.actionIcons.ImportTransactionMultsig, - }); - } - if (wallet?.type === MultisigHDWallet.type && wallet.howManySignaturesCanWeMake() > 0) { - transactionActions.push({ - id: SendDetails.actionKeys.CoSignTransaction, - text: loc.multisig.co_sign_transaction, - icon: SendDetails.actionIcons.SignPSBT, - }); - } - if ((wallet as MultisigHDWallet)?.allowCosignPsbt()) { - transactionActions.push({ id: SendDetails.actionKeys.SignPSBT, text: loc.send.psbt_sign, icon: SendDetails.actionIcons.SignPSBT }); - } - actions.push(transactionActions, [ - { - id: SendDetails.actionKeys.AddRecipient, - text: loc.send.details_add_rec_add, - icon: SendDetails.actionIcons.AddRecipient, - }, - { - id: SendDetails.actionKeys.RemoveRecipient, - text: loc.send.details_add_rec_rem, - disabled: addresses.length < 2, - icon: SendDetails.actionIcons.RemoveRecipient, - }, - ]); - } - - actions.push({ id: SendDetails.actionKeys.CoinControl, text: loc.cc.header, icon: SendDetails.actionIcons.CoinControl }); - - return actions; - }; - const setHeaderRightOptions = () => { - navigation.setOptions({ - headerRight: Platform.select({ - // eslint-disable-next-line react/no-unstable-nested-components - ios: () => ( - - - - ), - // eslint-disable-next-line react/no-unstable-nested-components - default: () => ( - { - Keyboard.dismiss(); - setOptionsVisible(true); - }} - testID="advancedOptionsMenuButton" - > - - - ), - }), - }); - }; - - const onReplaceableFeeSwitchValueChanged = (value: boolean) => { - setIsTransactionReplaceable(value); - }; - - // - - // because of https://github.com/facebook/react-native/issues/21718 we use - // onScroll for android and onMomentumScrollEnd for iOS - const handleRecipientsScrollEnds = (e: NativeSyntheticEvent) => { - if (Platform.OS === 'android') return; // for android we use handleRecipientsScroll - const contentOffset = e.nativeEvent.contentOffset; - const viewSize = e.nativeEvent.layoutMeasurement; - const index = Math.floor(contentOffset.x / viewSize.width); - scrollIndex.current = index; - }; - - const handleRecipientsScroll = (e: NativeSyntheticEvent) => { - if (Platform.OS === 'ios') return; // for iOS we use handleRecipientsScrollEnds - const contentOffset = e.nativeEvent.contentOffset; - const viewSize = e.nativeEvent.layoutMeasurement; - const index = Math.floor(contentOffset.x / viewSize.width); - scrollIndex.current = index; - }; - - const onUseAllPressed = () => { + const onUseAllPressed = useCallback(() => { triggerHapticFeedback(HapticFeedbackTypes.NotificationWarning); const message = frozenBalance > 0 ? loc.send.details_adv_full_sure_frozen : loc.send.details_adv_full_sure; Alert.alert( @@ -1146,6 +1000,155 @@ const SendDetails = () => { ], { cancelable: false }, ); + }, [frozenBalance]); + + // Header Right Button + + const headerRightOnPress = useCallback( + (id: string) => { + if (id === actionKeys.AddRecipient) { + handleAddRecipient(); + } else if (id === actionKeys.RemoveRecipient) { + handleRemoveRecipient(); + } else if (id === actionKeys.SignPSBT) { + handlePsbtSign(); + } else if (id === actionKeys.SendMax) { + onUseAllPressed(); + } else if (id === actionKeys.AllowRBF) { + onReplaceableFeeSwitchValueChanged(!isTransactionReplaceable); + } else if (id === actionKeys.ImportTransaction) { + importTransaction(); + } else if (id === actionKeys.ImportTransactionQR) { + importQrTransaction(); + } else if (id === actionKeys.ImportTransactionMultsig) { + importTransactionMultisig(); + } else if (id === actionKeys.CoSignTransaction) { + importTransactionMultisigScanQr(); + } else if (id === actionKeys.CoinControl) { + handleCoinControl(); + } else if (id === actionKeys.InsertContact) { + handleInsertContact(); + } + }, + [ + handleAddRecipient, + handleCoinControl, + handleInsertContact, + handlePsbtSign, + handleRemoveRecipient, + importQrTransaction, + importTransaction, + importTransactionMultisig, + importTransactionMultisigScanQr, + isTransactionReplaceable, + onUseAllPressed, + ], + ); + + const headerRightActions = useCallback(() => { + const actions: Action[] & Action[][] = []; + if (isEditable) { + if (wallet?.allowBIP47() && wallet?.isBIP47Enabled()) { + actions.push([{ id: actionKeys.InsertContact, text: loc.send.details_insert_contact, icon: actionIcons.InsertContact }]); + } + + if (Number(wallet?.getBalance()) > 0) { + const isSendMaxUsed = addresses.some(element => element.amount === BitcoinUnit.MAX); + + actions.push([{ id: actionKeys.SendMax, text: loc.send.details_adv_full, disabled: balance === 0 || isSendMaxUsed }]); + } + if (wallet?.type === HDSegwitBech32Wallet.type) { + actions.push([{ id: actionKeys.AllowRBF, text: loc.send.details_adv_fee_bump, menuStateOn: isTransactionReplaceable }]); + } + const transactionActions = []; + if (wallet?.type === WatchOnlyWallet.type && wallet.isHd()) { + transactionActions.push( + { + id: actionKeys.ImportTransaction, + text: loc.send.details_adv_import, + icon: actionIcons.ImportTransaction, + }, + { + id: actionKeys.ImportTransactionQR, + text: loc.send.details_adv_import_qr, + icon: actionIcons.ImportTransactionQR, + }, + ); + } + if (wallet?.type === MultisigHDWallet.type) { + transactionActions.push({ + id: actionKeys.ImportTransactionMultsig, + text: loc.send.details_adv_import, + icon: actionIcons.ImportTransactionMultsig, + }); + } + if (wallet?.type === MultisigHDWallet.type && wallet.howManySignaturesCanWeMake() > 0) { + transactionActions.push({ + id: actionKeys.CoSignTransaction, + text: loc.multisig.co_sign_transaction, + icon: actionIcons.SignPSBT, + }); + } + if ((wallet as MultisigHDWallet)?.allowCosignPsbt()) { + transactionActions.push({ id: actionKeys.SignPSBT, text: loc.send.psbt_sign, icon: actionIcons.SignPSBT }); + } + actions.push(transactionActions, [ + { + id: actionKeys.AddRecipient, + text: loc.send.details_add_rec_add, + icon: actionIcons.AddRecipient, + }, + { + id: actionKeys.RemoveRecipient, + text: loc.send.details_add_rec_rem, + disabled: addresses.length < 2, + icon: actionIcons.RemoveRecipient, + }, + ]); + } + + actions.push({ id: actionKeys.CoinControl, text: loc.cc.header, icon: actionIcons.CoinControl }); + + return actions; + }, [isEditable, wallet, addresses, balance, isTransactionReplaceable]); + + const HeaderRight = useMemo( + () => ( + + + + ), + [isLoading, headerRightOnPress, headerRightActions, colors.foregroundColor], + ); + + const setHeaderRightOptions = () => { + navigation.setOptions({ + headerRight: () => HeaderRight, + }); + }; + + const onReplaceableFeeSwitchValueChanged = (value: boolean) => { + setIsTransactionReplaceable(value); + }; + + // + + // because of https://github.com/facebook/react-native/issues/21718 we use + // onScroll for android and onMomentumScrollEnd for iOS + const handleRecipientsScrollEnds = (e: NativeSyntheticEvent) => { + if (Platform.OS === 'android') return; // for android we use handleRecipientsScroll + const contentOffset = e.nativeEvent.contentOffset; + const viewSize = e.nativeEvent.layoutMeasurement; + const index = Math.floor(contentOffset.x / viewSize.width); + scrollIndex.current = index; + }; + + const handleRecipientsScroll = (e: NativeSyntheticEvent) => { + if (Platform.OS === 'ios') return; // for iOS we use handleRecipientsScrollEnds + const contentOffset = e.nativeEvent.contentOffset; + const viewSize = e.nativeEvent.layoutMeasurement; + const index = Math.floor(contentOffset.x / viewSize.width); + scrollIndex.current = index; }; const formatFee = (fee: number) => formatBalance(fee, feeUnit!, true); @@ -1605,7 +1608,7 @@ const SendDetails = () => { export default SendDetails; -SendDetails.actionKeys = { +const actionKeys = { InsertContact: 'InsertContact', SignPSBT: 'SignPSBT', SendMax: 'SendMax', @@ -1619,7 +1622,7 @@ SendDetails.actionKeys = { CoSignTransaction: 'CoSignTransaction', }; -SendDetails.actionIcons = { +const actionIcons = { InsertContact: { iconType: 'SYSTEM', iconValue: 'at.badge.plus' }, SignPSBT: { iconType: 'SYSTEM', iconValue: 'signature' }, SendMax: 'SendMax',