diff --git a/.circleci/config.yml b/.circleci/config.yml index cb1d4e3f8..ce6bbf961 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,7 +3,7 @@ jobs: lint: docker: - - image: cimg/node:20.16.0 + - image: cimg/node:20.17.0 working_directory: ~/repo @@ -26,7 +26,7 @@ jobs: unit: docker: - - image: cimg/node:20.16.0 + - image: cimg/node:20.17.0 working_directory: ~/repo @@ -50,7 +50,7 @@ jobs: integration: docker: - - image: cimg/node:20.16.0 + - image: cimg/node:20.17.0 environment: RETRY: "1" diff --git a/android/app/build.gradle b/android/app/build.gradle index 35df9cca0..16b614d25 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -80,7 +80,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode 1 - versionName "7.0.2" + versionName "7.0.3" testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' } diff --git a/components/Context/SettingsProvider.tsx b/components/Context/SettingsProvider.tsx index 5df603e2a..7e9e5203b 100644 --- a/components/Context/SettingsProvider.tsx +++ b/components/Context/SettingsProvider.tsx @@ -11,6 +11,54 @@ import { getEnabled as getIsDeviceQuickActionsEnabled, setEnabled as setIsDevice import { getIsHandOffUseEnabled, setIsHandOffUseEnabled } from '../HandOffComponent'; import { isBalanceDisplayAllowed, setBalanceDisplayAllowed } from '../WidgetCommunication'; import { useStorage } from '../../hooks/context/useStorage'; +import { BitcoinUnit } from '../../models/bitcoinUnits'; +import { TotalWalletsBalanceKey, TotalWalletsBalancePreferredUnit } from '../TotalWalletsBalance'; +import { LayoutAnimation } from 'react-native'; + +// DefaultPreference and AsyncStorage get/set + +// TotalWalletsBalance + +export const setTotalBalanceViewEnabled = async (value: boolean) => { + await DefaultPreference.setName(GROUP_IO_BLUEWALLET); + await DefaultPreference.set(TotalWalletsBalanceKey, value ? 'true' : 'false'); + console.debug('setTotalBalanceViewEnabled value:', value); + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); +}; + +export const getIsTotalBalanceViewEnabled = async (): Promise => { + try { + await DefaultPreference.setName(GROUP_IO_BLUEWALLET); + + const isEnabledValue = (await DefaultPreference.get(TotalWalletsBalanceKey)) ?? 'true'; + console.debug('getIsTotalBalanceViewEnabled', isEnabledValue); + return isEnabledValue === 'true'; + } catch (e) { + console.debug('getIsTotalBalanceViewEnabled error', e); + await setTotalBalanceViewEnabled(true); + } + await setTotalBalanceViewEnabled(true); + return true; +}; + +export const setTotalBalancePreferredUnit = async (unit: BitcoinUnit) => { + await DefaultPreference.setName(GROUP_IO_BLUEWALLET); + await DefaultPreference.set(TotalWalletsBalancePreferredUnit, unit); + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); // Add animation when changing unit +}; + +// + +export const getTotalBalancePreferredUnit = async (): Promise => { + try { + await DefaultPreference.setName(GROUP_IO_BLUEWALLET); + const unit = ((await DefaultPreference.get(TotalWalletsBalancePreferredUnit)) as BitcoinUnit) ?? BitcoinUnit.BTC; + return unit; + } catch (e) { + console.debug('getPreferredUnit error', e); + } + return BitcoinUnit.BTC; +}; interface SettingsContextType { preferredFiatCurrency: TFiatUnit; @@ -33,6 +81,10 @@ interface SettingsContextType { setIsClipboardGetContentEnabledStorage: (value: boolean) => Promise; isQuickActionsEnabled: boolean; setIsQuickActionsEnabledStorage: (value: boolean) => Promise; + isTotalBalanceEnabled: boolean; + setIsTotalBalanceEnabledStorage: (value: boolean) => Promise; + totalBalancePreferredUnit: BitcoinUnit; + setTotalBalancePreferredUnitStorage: (unit: BitcoinUnit) => Promise; } const defaultSettingsContext: SettingsContextType = { @@ -56,6 +108,10 @@ const defaultSettingsContext: SettingsContextType = { setIsClipboardGetContentEnabledStorage: async () => {}, isQuickActionsEnabled: true, setIsQuickActionsEnabledStorage: async () => {}, + isTotalBalanceEnabled: true, + setIsTotalBalanceEnabledStorage: async () => {}, + totalBalancePreferredUnit: BitcoinUnit.BTC, + setTotalBalancePreferredUnitStorage: async (unit: BitcoinUnit) => {}, }; export const SettingsContext = createContext(defaultSettingsContext); @@ -81,6 +137,9 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil const [isClipboardGetContentEnabled, setIsClipboardGetContentEnabled] = useState(false); // Quick Actions const [isQuickActionsEnabled, setIsQuickActionsEnabled] = useState(true); + // Total Balance + const [isTotalBalanceEnabled, setIsTotalBalanceEnabled] = useState(true); + const [totalBalancePreferredUnit, setTotalBalancePreferredUnitState] = useState(BitcoinUnit.BTC); const advancedModeStorage = useAsyncStorage(BlueApp.ADVANCED_MODE_ENABLED); const languageStorage = useAsyncStorage(STORAGE_KEY); @@ -146,6 +205,20 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil setDoNotTrackStorage(value ?? false); }) .catch(error => console.error('Error fetching do not track settings:', error)); + + getIsTotalBalanceViewEnabled() + .then(value => { + console.debug('SettingsContext totalBalance:', value); + setIsTotalBalanceEnabledStorage(value); + }) + .catch(error => console.error('Error fetching total balance settings:', error)); + + getTotalBalancePreferredUnit() + .then(unit => { + console.debug('SettingsContext totalBalancePreferredUnit:', unit); + setTotalBalancePreferredUnit(unit); + }) + .catch(error => console.error('Error fetching total balance preferred unit:', error)); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -228,6 +301,16 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil [isPrivacyBlurEnabled], ); + const setIsTotalBalanceEnabledStorage = useCallback(async (value: boolean) => { + setTotalBalanceViewEnabled(value); + setIsTotalBalanceEnabled(value); + }, []); + + const setTotalBalancePreferredUnitStorage = useCallback(async (unit: BitcoinUnit) => { + await setTotalBalancePreferredUnit(unit); + setTotalBalancePreferredUnitState(unit); + }, []); + const value = useMemo( () => ({ preferredFiatCurrency, @@ -250,6 +333,10 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil setIsClipboardGetContentEnabledStorage, isQuickActionsEnabled, setIsQuickActionsEnabledStorage, + isTotalBalanceEnabled, + setIsTotalBalanceEnabledStorage, + totalBalancePreferredUnit, + setTotalBalancePreferredUnitStorage, }), [ preferredFiatCurrency, @@ -272,6 +359,10 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil setIsClipboardGetContentEnabledStorage, isQuickActionsEnabled, setIsQuickActionsEnabledStorage, + isTotalBalanceEnabled, + setIsTotalBalanceEnabledStorage, + totalBalancePreferredUnit, + setTotalBalancePreferredUnitStorage, ], ); diff --git a/components/FloatButtons.tsx b/components/FloatButtons.tsx index f9d9b3a1a..8a02a0ef8 100644 --- a/components/FloatButtons.tsx +++ b/components/FloatButtons.tsx @@ -130,11 +130,12 @@ interface FButtonProps { first?: boolean; last?: boolean; disabled?: boolean; + testID?: string; onPress: () => void; onLongPress?: () => void; } -export const FButton = ({ text, icon, width, first, last, ...props }: FButtonProps) => { +export const FButton = ({ text, icon, width, first, last, testID, ...props }: FButtonProps) => { const { colors } = useTheme(); const bStylesHook = StyleSheet.create({ root: { @@ -163,6 +164,7 @@ export const FButton = ({ text, icon, width, first, last, ...props }: FButtonPro diff --git a/components/TotalWalletsBalance.tsx b/components/TotalWalletsBalance.tsx new file mode 100644 index 000000000..280679ccf --- /dev/null +++ b/components/TotalWalletsBalance.tsx @@ -0,0 +1,139 @@ +import React, { useMemo } from 'react'; +import { TouchableOpacity, Text, StyleSheet, LayoutAnimation, View } from 'react-native'; +import { useStorage } from '../hooks/context/useStorage'; +import loc, { formatBalanceWithoutSuffix } from '../loc'; +import { BitcoinUnit } from '../models/bitcoinUnits'; +import ToolTipMenu from './TooltipMenu'; +import { CommonToolTipActions } from '../typings/CommonToolTipActions'; +import { useSettings } from '../hooks/context/useSettings'; +import Clipboard from '@react-native-clipboard/clipboard'; +import { useTheme } from './themes'; + +export const TotalWalletsBalancePreferredUnit = 'TotalWalletsBalancePreferredUnit'; +export const TotalWalletsBalanceKey = 'TotalWalletsBalance'; + +const TotalWalletsBalance: React.FC = () => { + const { wallets } = useStorage(); + const { preferredFiatCurrency, setIsTotalBalanceEnabledStorage, totalBalancePreferredUnit, setTotalBalancePreferredUnitStorage } = + useSettings(); + const { colors } = useTheme(); + + const styleHooks = StyleSheet.create({ + balance: { + color: colors.foregroundColor, + }, + currency: { + color: colors.foregroundColor, + }, + }); + + // Calculate total balance from all wallets + const totalBalance = wallets.reduce((prev, curr) => { + if (!curr.hideBalance) { + return prev + curr.getBalance(); + } + return prev; + }, 0); + + const formattedBalance = useMemo( + () => formatBalanceWithoutSuffix(Number(totalBalance), totalBalancePreferredUnit, true), + [totalBalance, totalBalancePreferredUnit], + ); + + const toolTipActions = useMemo(() => { + let viewIn; + + if (totalBalancePreferredUnit === BitcoinUnit.SATS) { + viewIn = { + ...CommonToolTipActions.ViewInFiat, + text: loc.formatString(loc.total_balance_view.view_in_fiat, { currency: preferredFiatCurrency.endPointKey }), + }; + } else if (totalBalancePreferredUnit === BitcoinUnit.LOCAL_CURRENCY) { + viewIn = CommonToolTipActions.ViewInBitcoin; + } else if (totalBalancePreferredUnit === BitcoinUnit.BTC) { + viewIn = CommonToolTipActions.ViewInSats; + } else { + viewIn = CommonToolTipActions.ViewInBitcoin; + } + + return [viewIn, CommonToolTipActions.CopyAmount, CommonToolTipActions.HideBalance]; + }, [preferredFiatCurrency.endPointKey, totalBalancePreferredUnit]); + + const onPressMenuItem = useMemo( + () => async (id: string) => { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + switch (id) { + case CommonToolTipActions.ViewInFiat.id: + case CommonToolTipActions.ViewInBitcoin.id: + case CommonToolTipActions.ViewInSats.id: + switch (totalBalancePreferredUnit) { + case BitcoinUnit.BTC: + await setTotalBalancePreferredUnitStorage(BitcoinUnit.SATS); + break; + case BitcoinUnit.SATS: + await setTotalBalancePreferredUnitStorage(BitcoinUnit.LOCAL_CURRENCY); + break; + case BitcoinUnit.LOCAL_CURRENCY: + await setTotalBalancePreferredUnitStorage(BitcoinUnit.BTC); + break; + default: + break; + } + break; + case CommonToolTipActions.HideBalance.id: + setIsTotalBalanceEnabledStorage(false); + break; + case CommonToolTipActions.CopyAmount.id: + Clipboard.setString(formattedBalance.toString()); + break; + default: + break; + } + }, + [totalBalancePreferredUnit, setIsTotalBalanceEnabledStorage, formattedBalance, setTotalBalancePreferredUnitStorage], + ); + + return ( + (wallets.length > 1 && ( + + + {loc.wallets.total_balance} + onPressMenuItem(CommonToolTipActions.ViewInBitcoin.id)}> + + {formattedBalance}{' '} + {totalBalancePreferredUnit !== BitcoinUnit.LOCAL_CURRENCY && ( + {totalBalancePreferredUnit} + )} + + + + + )) || + null + ); +}; + +const styles = StyleSheet.create({ + container: { + flexDirection: 'column', + alignItems: 'flex-start', + padding: 16, + }, + label: { + fontSize: 14, + marginBottom: 4, + color: '#9BA0A9', + }, + balance: { + fontSize: 32, + fontWeight: 'bold', + color: '#1D2B53', + }, + currency: { + fontSize: 18, + fontWeight: 'bold', + color: '#1D2B53', + }, +}); + +export default TotalWalletsBalance; diff --git a/components/TransactionsNavigationHeader.tsx b/components/TransactionsNavigationHeader.tsx index e156af7f3..877607468 100644 --- a/components/TransactionsNavigationHeader.tsx +++ b/components/TransactionsNavigationHeader.tsx @@ -16,24 +16,13 @@ import ToolTipMenu from './TooltipMenu'; interface TransactionsNavigationHeaderProps { wallet: TWallet; onWalletUnitChange?: (wallet: any) => void; - navigation: { - navigate: (route: string, params?: any) => void; - goBack: () => void; - }; onManageFundsPressed?: (id?: string) => void; onWalletBalanceVisibilityChange?: (isShouldBeVisible: boolean) => void; - actionKeys: { - CopyToClipboard: 'copyToClipboard'; - WalletBalanceVisibility: 'walletBalanceVisibility'; - Refill: 'refill'; - RefillWithExternalWallet: 'qrcode'; - }; } const TransactionsNavigationHeader: React.FC = ({ wallet: initialWallet, onWalletUnitChange, - navigation, onManageFundsPressed, onWalletBalanceVisibilityChange, }) => { @@ -175,25 +164,24 @@ const TransactionsNavigationHeader: React.FC ]; }, [wallet.hideBalance]); + const imageSource = useMemo(() => { + switch (wallet.type) { + case LightningCustodianWallet.type: + return I18nManager.isRTL ? require('../img/lnd-shape-rtl.png') : require('../img/lnd-shape.png'); + case MultisigHDWallet.type: + return I18nManager.isRTL ? require('../img/vault-shape-rtl.png') : require('../img/vault-shape.png'); + default: + return I18nManager.isRTL ? require('../img/btc-shape-rtl.png') : require('../img/btc-shape.png'); + } + }, [wallet.type]); + return ( - { - switch (wallet.type) { - case LightningCustodianWallet.type: - return I18nManager.isRTL ? require('../img/lnd-shape-rtl.png') : require('../img/lnd-shape.png'); - case MultisigHDWallet.type: - return I18nManager.isRTL ? require('../img/vault-shape-rtl.png') : require('../img/vault-shape.png'); - default: - return I18nManager.isRTL ? require('../img/btc-shape-rtl.png') : require('../img/btc-shape.png'); - } - })()} - style={styles.chainIcon} - /> + {wallet.getLabel()} diff --git a/components/WalletsCarousel.tsx b/components/WalletsCarousel.tsx index 79b8a670c..d7f3606bc 100644 --- a/components/WalletsCarousel.tsx +++ b/components/WalletsCarousel.tsx @@ -237,7 +237,7 @@ export const WalletCarouselItem: React.FC = React.memo( > - + {renderHighlightedText && searchQuery ? renderHighlightedText(item.getLabel(), searchQuery) : item.getLabel()} diff --git a/components/WatchOnlyWarning.tsx b/components/WatchOnlyWarning.tsx index 46c57a53f..eafebc476 100644 --- a/components/WatchOnlyWarning.tsx +++ b/components/WatchOnlyWarning.tsx @@ -5,14 +5,14 @@ import loc from '../loc'; interface Props { handleDismiss: () => void; - isLoading?: boolean; + disabled?: boolean; } -const WatchOnlyWarning: React.FC = ({ handleDismiss, isLoading }) => { +const WatchOnlyWarning: React.FC = ({ handleDismiss, disabled }) => { return ( - + diff --git a/hooks/useKeyboard.ts b/hooks/useKeyboard.ts new file mode 100644 index 000000000..1baf9f42f --- /dev/null +++ b/hooks/useKeyboard.ts @@ -0,0 +1,54 @@ +import { useState, useEffect } from 'react'; +import { Keyboard, KeyboardEvent, Platform } from 'react-native'; + +interface KeyboardInfo { + isVisible: boolean; + height: number; +} + +interface UseKeyboardProps { + onKeyboardDidShow?: () => void; + onKeyboardDidHide?: () => void; +} + +export const useKeyboard = ({ onKeyboardDidShow, onKeyboardDidHide }: UseKeyboardProps = {}): KeyboardInfo => { + const [keyboardInfo, setKeyboardInfo] = useState({ + isVisible: false, + height: 0, + }); + + useEffect(() => { + const handleKeyboardDidShow = (event: KeyboardEvent) => { + setKeyboardInfo({ + isVisible: true, + height: event.endCoordinates.height, + }); + if (onKeyboardDidShow) { + onKeyboardDidShow(); + } + }; + + const handleKeyboardDidHide = () => { + setKeyboardInfo({ + isVisible: false, + height: 0, + }); + if (onKeyboardDidHide) { + onKeyboardDidHide(); + } + }; + + const showEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow'; + const hideEvent = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide'; + + const showSubscription = Keyboard.addListener(showEvent, handleKeyboardDidShow); + const hideSubscription = Keyboard.addListener(hideEvent, handleKeyboardDidHide); + + return () => { + showSubscription.remove(); + hideSubscription.remove(); + }; + }, [onKeyboardDidShow, onKeyboardDidHide]); + + return keyboardInfo; +}; diff --git a/ios/BlueWallet.xcodeproj/project.pbxproj b/ios/BlueWallet.xcodeproj/project.pbxproj index 21c4f7be5..fa8bf6ffa 100644 --- a/ios/BlueWallet.xcodeproj/project.pbxproj +++ b/ios/BlueWallet.xcodeproj/project.pbxproj @@ -1357,7 +1357,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1703136692; + CURRENT_PROJECT_VERSION = 1703136694; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = A7W54YZ4WU; @@ -1382,7 +1382,7 @@ "$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(inherited)", ); - MARKETING_VERSION = 7.0.2; + MARKETING_VERSION = 7.0.3; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -1417,7 +1417,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1703136692; + CURRENT_PROJECT_VERSION = 1703136694; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = A7W54YZ4WU; @@ -1437,7 +1437,7 @@ "$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(inherited)", ); - MARKETING_VERSION = 7.0.2; + MARKETING_VERSION = 7.0.3; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -1473,7 +1473,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1703136692; + CURRENT_PROJECT_VERSION = 1703136694; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = ""; @@ -1486,7 +1486,7 @@ "$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(inherited)", ); - MARKETING_VERSION = 7.0.2; + MARKETING_VERSION = 7.0.3; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES; @@ -1516,7 +1516,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1703136692; + CURRENT_PROJECT_VERSION = 1703136694; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = ""; @@ -1529,7 +1529,7 @@ "$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(inherited)", ); - MARKETING_VERSION = 7.0.2; + MARKETING_VERSION = 7.0.3; MTL_FAST_MATH = YES; PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES; PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.Stickers; @@ -1560,7 +1560,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1703136692; + CURRENT_PROJECT_VERSION = 1703136694; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = ""; @@ -1579,7 +1579,7 @@ "$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(inherited)", ); - MARKETING_VERSION = 7.0.2; + MARKETING_VERSION = 7.0.3; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES; @@ -1616,7 +1616,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Distribution"; CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1703136692; + CURRENT_PROJECT_VERSION = 1703136694; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = ""; @@ -1635,7 +1635,7 @@ "$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(inherited)", ); - MARKETING_VERSION = 7.0.2; + MARKETING_VERSION = 7.0.3; MTL_FAST_MATH = YES; PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES; PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.MarketWidget; @@ -1798,7 +1798,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1703136692; + CURRENT_PROJECT_VERSION = 1703136694; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = ""; @@ -1815,7 +1815,7 @@ "$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(inherited)", ); - MARKETING_VERSION = 7.0.2; + MARKETING_VERSION = 7.0.3; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES; @@ -1848,7 +1848,7 @@ "CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1703136692; + CURRENT_PROJECT_VERSION = 1703136694; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = ""; @@ -1865,7 +1865,7 @@ "$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(inherited)", ); - MARKETING_VERSION = 7.0.2; + MARKETING_VERSION = 7.0.3; MTL_FAST_MATH = YES; PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES; PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.watch.extension; @@ -1897,7 +1897,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1703136692; + CURRENT_PROJECT_VERSION = 1703136694; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = ""; @@ -1910,7 +1910,7 @@ "$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(inherited)", ); - MARKETING_VERSION = 7.0.2; + MARKETING_VERSION = 7.0.3; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES; @@ -1945,7 +1945,7 @@ "CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1703136692; + CURRENT_PROJECT_VERSION = 1703136694; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = ""; @@ -1958,7 +1958,7 @@ "$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(inherited)", ); - MARKETING_VERSION = 7.0.2; + MARKETING_VERSION = 7.0.3; MTL_FAST_MATH = YES; PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES; PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.watch; diff --git a/ios/Podfile.lock b/ios/Podfile.lock index c6ced25dd..6e747f398 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -969,8 +969,27 @@ PODS: - React - react-native-bw-file-access (1.0.0): - React-Core - - react-native-document-picker (9.3.0): + - react-native-document-picker (9.3.1): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Codegen - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - react-native-idle-timer (2.2.2): - React-Core - react-native-image-picker (7.1.2): @@ -1782,7 +1801,7 @@ SPEC CHECKSUMS: react-native-biometrics: 352e5a794bfffc46a0c86725ea7dc62deb085bdc react-native-blue-crypto: 23f1558ad3d38d7a2edb7e2f6ed1bc520ed93e56 react-native-bw-file-access: b232fd1d902521ca046f3fc5990ab1465e1878d7 - react-native-document-picker: 5b97e24a7f1a1e4a50a72c540a043f32d29a70a2 + react-native-document-picker: c4f197741c327270453aa9840932098e0064fd52 react-native-idle-timer: ee2053f2cd458f6fef1db7bebe5098ca281cce07 react-native-image-picker: c3afe5472ef870d98a4b28415fc0b928161ee5f7 react-native-ios-context-menu: e529171ba760a1af7f2ef0729f5a7f4d226171c5 diff --git a/loc/en.json b/loc/en.json index dd630b66d..290a6326e 100644 --- a/loc/en.json +++ b/loc/en.json @@ -380,6 +380,7 @@ "add_bitcoin": "Bitcoin", "add_bitcoin_explain": "Simple and powerful Bitcoin wallet", "add_create": "Create", + "total_balance": "Total Balance", "add_entropy": "Entropy", "add_entropy_bytes": "{bytes} bytes of entropy", "add_entropy_generated": "{gen} bytes of generated entropy", @@ -480,6 +481,13 @@ "xpub_title": "Wallet XPUB", "manage_wallets_search_placeholder": "Search wallets, memos" }, + "total_balance_view": { + "view_in_bitcoin": "View in Bitcoin", + "view_in_sats": "View in sats", + "view_in_fiat": "View in {currency}", + "title": "Total Balance", + "explanation": "View the total balance of all your wallets in the overview screen." + }, "multisig": { "multisig_vault": "Vault", "default_label": "Multisig Vault", diff --git a/loc/es_419.json b/loc/es_419.json index dc65922d2..29415b9f2 100644 --- a/loc/es_419.json +++ b/loc/es_419.json @@ -78,14 +78,10 @@ }, "plausibledeniability": { "create_fake_storage": "Crear almacenamiento encriptado", - "create_password": "Crear una contraseña", "create_password_explanation": "La contraseña del almacenamiento falso no debe coincidir con la contraseña de tu almacenamiento principal.", "help": "Bajo ciertas circunstancias, podrías verte obligado a revelar una contraseña. Para mantener tus monedas seguras, BlueWallet puede crear otro almacenamiento encriptado, con una contraseña diferente. Bajo presión puedes revelar esta contraseña a un tercero. Si se ingresa en BlueWallet, desbloqueará un nuevo almacenamiento \"falso\". Esto parecerá legítimo para un tercero, pero en secreto mantendrá tu almacenamiento principal con monedas seguras.", "help2": "El nuevo almacén será completamente funcional, y puedes almacenar cantidades mínimas para que sea mas creíble.", "password_should_not_match": "La contraseña está actualmente en uso. Intenta con una contraseña diferente.", - "passwords_do_not_match": "Las contraseñas no coinciden, intenta nuevamente", - "confirm_password": "Vuelve a escribir la contraseña", - "success": "Éxito", "title": "Negación plausible" }, "pleasebackup": { @@ -265,6 +261,10 @@ "encrypt_decrypt": "Descifrar Almacenamiento", "encrypt_decrypt_q": "¿Estás seguro de que deseas descifrar tu almacenamiento? Esto permitirá acceder a tus billeteras sin una contraseña.", "encrypt_enc_and_pass": "Encriptado y protegido con contraseña", + "encrypt_storage_explanation_headline": "Habilitar cifrado de almacenamiento", + "encrypt_storage_explanation_description_line1": "Habilitar el cifrado de almacenamiento agrega una capa adicional de protección a tu aplicación al proteger la forma en que se almacenan tus datos en tu dispositivo. Esto hace que sea más difícil para cualquier persona acceder a tu información sin permiso.", + "encrypt_storage_explanation_description_line2": "Sin embargo, es importante saber que este cifrado sólo protege el acceso a tus billeteras almacenadas en el llavero del dispositivo. No pone una contraseña ni ninguna protección adicional en las billeteras.", + "i_understand": "Entiendo", "encrypt_title": "Seguridad", "encrypt_tstorage": "Almacenamiento", "encrypt_use": "Usar {type}", @@ -293,8 +293,7 @@ "notifications": "Notificaciones", "open_link_in_explorer": "Abrir enlace en el explorador", "password": "Contraseña", - "password_explain": "Crea la contraseña que usarás para desencriptar el almacenamiento", - "passwords_do_not_match": "Las contraseñas no coinciden.", + "password_explain": "Ingresa la contraseña que usarás para desbloquear tu almacenamiento.", "plausible_deniability": "Negación Plausible", "privacy": "Privacidad", "privacy_read_clipboard": "Leer portapapeles", @@ -306,7 +305,6 @@ "privacy_do_not_track_explanation": "La información de rendimiento y confiabilidad no se enviará para su análisis.", "push_notifications": "Notificaciones Push", "rate": "Tasa", - "confirm_password": "Ingresa la contraseña nuevamente", "selfTest": "Auto-Test", "save": "Guardar", "saved": "Guardado", @@ -382,6 +380,7 @@ "add_bitcoin": "Bitcoin", "add_bitcoin_explain": "Billetera Bitcoin simple y potente", "add_create": "Crear", + "total_balance": "Balance Total", "add_entropy": "Entropía ", "add_entropy_bytes": "{bytes} bytes de entropía", "add_entropy_generated": "{gen} bytes de entropía generada", @@ -482,6 +481,13 @@ "xpub_title": "XPUB de la billetera", "manage_wallets_search_placeholder": "Buscar billeteras, notas" }, + "total_balance_view": { + "view_in_bitcoin": "Ver en Bitcoin", + "view_in_sats": "Ver en sats", + "view_in_fiat": "Ver en {currency}", + "title": "Balance Total", + "explanation": "Ve el saldo total de todas tus billeteras en la pantalla de descripción general." + }, "multisig": { "multisig_vault": "Bóveda", "default_label": "Bóveda Multifirma", diff --git a/loc/index.ts b/loc/index.ts index a4b0ffc0c..3d4b5f465 100644 --- a/loc/index.ts +++ b/loc/index.ts @@ -338,17 +338,14 @@ export function formatBalanceWithoutSuffix(balance = 0, toUnit: string, withForm if (toUnit === undefined) { return balance; } - if (balance !== 0) { - if (toUnit === BitcoinUnit.BTC) { - const value = new BigNumber(balance).dividedBy(100000000).toFixed(8); - return removeTrailingZeros(value); - } else if (toUnit === BitcoinUnit.SATS) { - return withFormatting ? new Intl.NumberFormat().format(balance).toString() : String(balance); - } else { - return satoshiToLocalCurrency(balance); - } + if (toUnit === BitcoinUnit.BTC) { + const value = new BigNumber(balance).dividedBy(100000000).toFixed(8); + return removeTrailingZeros(value); + } else if (toUnit === BitcoinUnit.SATS) { + return withFormatting ? new Intl.NumberFormat().format(balance).toString() : String(balance); + } else { + return satoshiToLocalCurrency(balance); } - return balance.toString(); } /** diff --git a/navigation/DetailViewScreensStack.tsx b/navigation/DetailViewScreensStack.tsx index 8cf2fcc4d..a28d63ac0 100644 --- a/navigation/DetailViewScreensStack.tsx +++ b/navigation/DetailViewScreensStack.tsx @@ -25,7 +25,6 @@ import WalletAddresses from '../screen/wallets/WalletAddresses'; import WalletDetails from '../screen/wallets/details'; import GenerateWord from '../screen/wallets/generateWord'; import SelectWallet from '../screen/wallets/SelectWallet'; -import WalletTransactions from '../screen/wallets/transactions'; import WalletsList from '../screen/wallets/WalletsList'; import { NavigationDefaultOptions, NavigationFormModalOptions, StatusBarLightOptions, DetailViewStack } from './index'; // Importing the navigator import AddWalletStack from './AddWalletStack'; @@ -64,16 +63,15 @@ import SettingsButton from '../components/icons/SettingsButton'; import ExportMultisigCoordinationSetupStack from './ExportMultisigCoordinationSetupStack'; import ManageWallets from '../screen/wallets/ManageWallets'; import getWalletTransactionsOptions from './helpers/getWalletTransactionsOptions'; -import { RouteProp } from '@react-navigation/native'; -import { DetailViewStackParamList } from './DetailViewStackParamList'; - -type walletTransactionsRouteProp = RouteProp; - -const walletTransactionsOptions = ({ route }: { route: walletTransactionsRouteProp }) => getWalletTransactionsOptions({ route }); +import { useSettings } from '../hooks/context/useSettings'; +import { useStorage } from '../hooks/context/useStorage'; +import WalletTransactions from '../screen/wallets/WalletTransactions'; const DetailViewStackScreensStack = () => { const theme = useTheme(); const navigation = useExtendedNavigation(); + const { wallets } = useStorage(); + const { isTotalBalanceEnabled } = useSettings(); const SaveButton = useMemo(() => , []); const DetailButton = useMemo(() => , []); @@ -94,11 +92,12 @@ const DetailViewStackScreensStack = () => { ); const useWalletListScreenOptions = useMemo(() => { + const displayTitle = !isTotalBalanceEnabled || wallets.length <= 1; return { - title: loc.wallets.wallets, + title: displayTitle ? loc.wallets.wallets : '', navigationBarColor: theme.colors.navigationBarColor, headerShown: !isDesktop, - headerLargeTitle: true, + headerLargeTitle: displayTitle, headerShadowVisible: false, headerLargeTitleShadowVisible: false, headerStyle: { @@ -106,7 +105,7 @@ const DetailViewStackScreensStack = () => { }, headerRight: () => RightBarButtons, }; - }, [RightBarButtons, theme.colors.customHeader, theme.colors.navigationBarColor]); + }, [RightBarButtons, isTotalBalanceEnabled, theme.colors.customHeader, theme.colors.navigationBarColor, wallets.length]); const walletListScreenOptions = useWalletListScreenOptions; @@ -116,7 +115,7 @@ const DetailViewStackScreensStack = () => { screenOptions={{ headerShadowVisible: false, animationTypeForReplace: 'push' }} > - + ; + +interface GetTransactionStatusOptionsParams { + route: TransactionStatusRouteProp; + navigation: any; + theme: Theme; +} + +const getTransactionStatusOptions = ({ route, navigation, theme }: GetTransactionStatusOptionsParams): NativeStackNavigationOptions => { + const { hash, walletID } = route.params; + + const navigateToTransactionDetails = () => { + navigation.navigate('TransactionDetails', { hash, walletID }); + }; + + return { + ...navigationStyle({ + title: '', + headerStyle: { + backgroundColor: theme.colors.customHeader, + }, + headerBackTitleStyle: { fontSize: 0 }, + headerBackTitleVisible: true, + statusBarStyle: 'auto', + })(theme), + headerRight: () => ( + + ), + }; +}; + +export default getTransactionStatusOptions; diff --git a/navigation/helpers/getWalletTransactionsOptions.tsx b/navigation/helpers/getWalletTransactionsOptions.tsx index ff90ebf2e..a249f21f0 100644 --- a/navigation/helpers/getWalletTransactionsOptions.tsx +++ b/navigation/helpers/getWalletTransactionsOptions.tsx @@ -2,16 +2,14 @@ import React from 'react'; import { TouchableOpacity, StyleSheet } from 'react-native'; import { Icon } from '@rneui/themed'; import WalletGradient from '../../class/wallet-gradient'; -import { RouteProp } from '@react-navigation/native'; import { NativeStackNavigationOptions } from '@react-navigation/native-stack'; import { DetailViewStackParamList } from '../DetailViewStackParamList'; import { navigationRef } from '../../NavigationService'; +import { RouteProp } from '@react-navigation/native'; -interface GetWalletTransactionsOptionsParams { - route: RouteProp; -} +export type WalletTransactionsRouteProps = RouteProp; -const getWalletTransactionsOptions = ({ route }: GetWalletTransactionsOptionsParams): NativeStackNavigationOptions => { +const getWalletTransactionsOptions = ({ route }: { route: WalletTransactionsRouteProps }): NativeStackNavigationOptions => { const { isLoading, walletID, walletType } = route.params; const onPress = () => { @@ -19,6 +17,7 @@ const getWalletTransactionsOptions = ({ route }: GetWalletTransactionsOptionsPar walletID, }); }; + const RightButton = ( diff --git a/package-lock.json b/package-lock.json index 05234c4b7..2681bf5b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "bluewallet", - "version": "7.0.2", + "version": "7.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bluewallet", - "version": "7.0.2", + "version": "7.0.3", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -14,7 +14,7 @@ "@bugsnag/react-native": "7.25.0", "@bugsnag/source-maps": "2.3.3", "@keystonehq/bc-ur-registry": "0.7.0", - "@lodev09/react-native-true-sheet": "github:BlueWallet/react-native-true-sheet#730a84b0261ef2dd2e7e9adadba7f260c7f76726", + "@lodev09/react-native-true-sheet": "github:BlueWallet/react-native-true-sheet#839f2966cee77c0ad99d09609dadb61a338e7f54", "@ngraveio/bc-ur": "1.1.13", "@noble/secp256k1": "1.6.3", "@react-native-async-storage/async-storage": "1.24.0", @@ -68,7 +68,7 @@ "react-native-crypto": "2.2.0", "react-native-default-preference": "1.4.4", "react-native-device-info": "11.1.0", - "react-native-document-picker": "9.3.0", + "react-native-document-picker": "9.3.1", "react-native-draggable-flatlist": "github:BlueWallet/react-native-draggable-flatlist#v4.0.1", "react-native-fs": "2.20.0", "react-native-gesture-handler": "2.18.1", @@ -3122,9 +3122,9 @@ } }, "node_modules/@lodev09/react-native-true-sheet": { - "version": "0.12.4", - "resolved": "git+ssh://git@github.com/BlueWallet/react-native-true-sheet.git#730a84b0261ef2dd2e7e9adadba7f260c7f76726", - "integrity": "sha512-Ll28G5GC/3rI1VH7bAfaMsfkr/ezTcJJcgJa6rnVGf2yoHaLyL2aiBYFAFOLm3VMruTFLLx5NT3XRcnkenF69A==", + "version": "0.13.0", + "resolved": "git+ssh://git@github.com/BlueWallet/react-native-true-sheet.git#839f2966cee77c0ad99d09609dadb61a338e7f54", + "integrity": "sha512-hMajWAQPrk4XjmgGD7k1uqCbouw0SCaYH5arj+To8EBeZ7r9kbr7umBQBvUZGnnicGpvwvbtFZh+fda97Hfp1A==", "license": "MIT", "workspaces": [ "example", @@ -12991,7 +12991,9 @@ } }, "node_modules/react-native-document-picker": { - "version": "9.3.0", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/react-native-document-picker/-/react-native-document-picker-9.3.1.tgz", + "integrity": "sha512-Vcofv9wfB0j67zawFjfq9WQPMMzXxOZL9kBmvWDpjVuEcVK73ndRmlXHlkeFl5ZHVsv4Zb6oZYhqm9u5omJOPA==", "license": "MIT", "dependencies": { "invariant": "^2.2.4" diff --git a/package.json b/package.json index 225d918be..0ed6fe3e1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bluewallet", - "version": "7.0.2", + "version": "7.0.3", "license": "MIT", "repository": { "type": "git", @@ -78,7 +78,7 @@ "@bugsnag/react-native": "7.25.0", "@bugsnag/source-maps": "2.3.3", "@keystonehq/bc-ur-registry": "0.7.0", - "@lodev09/react-native-true-sheet": "github:BlueWallet/react-native-true-sheet#730a84b0261ef2dd2e7e9adadba7f260c7f76726", + "@lodev09/react-native-true-sheet": "github:BlueWallet/react-native-true-sheet#839f2966cee77c0ad99d09609dadb61a338e7f54", "@ngraveio/bc-ur": "1.1.13", "@noble/secp256k1": "1.6.3", "@react-native-async-storage/async-storage": "1.24.0", @@ -131,7 +131,7 @@ "react-native-crypto": "2.2.0", "react-native-default-preference": "1.4.4", "react-native-device-info": "11.1.0", - "react-native-document-picker": "9.3.0", + "react-native-document-picker": "9.3.1", "react-native-draggable-flatlist": "github:BlueWallet/react-native-draggable-flatlist#v4.0.1", "react-native-fs": "2.20.0", "react-native-gesture-handler": "2.18.1", diff --git a/screen/send/SendDetails.tsx b/screen/send/SendDetails.tsx index b9f3e18ca..01535fdab 100644 --- a/screen/send/SendDetails.tsx +++ b/screen/send/SendDetails.tsx @@ -54,6 +54,7 @@ import { ContactList } from '../../class/contact-list'; import { useStorage } from '../../hooks/context/useStorage'; import { Action } from '../../components/types'; import SelectFeeModal from '../../components/SelectFeeModal'; +import { useKeyboard } from '../../hooks/useKeyboard'; interface IPaymentDestinations { address: string; // btc address or payment code @@ -134,25 +135,16 @@ const SendDetails = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [colors, wallet, isTransactionReplaceable, balance, addresses, isEditable, isLoading]); - // keyboad effects - useEffect(() => { - const _keyboardDidShow = () => { + useKeyboard({ + onKeyboardDidShow: () => { setWalletSelectionOrCoinsSelectedHidden(true); setIsAmountToolbarVisibleForAndroid(true); - }; - - const _keyboardDidHide = () => { + }, + onKeyboardDidHide: () => { setWalletSelectionOrCoinsSelectedHidden(false); setIsAmountToolbarVisibleForAndroid(false); - }; - - const showSubscription = Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow', _keyboardDidShow); - const hideSubscription = Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide', _keyboardDidHide); - return () => { - showSubscription.remove(); - hideSubscription.remove(); - }; - }, []); + }, + }); useEffect(() => { // decode route params diff --git a/screen/settings/SettingsPrivacy.tsx b/screen/settings/SettingsPrivacy.tsx index 156f53118..ecb741868 100644 --- a/screen/settings/SettingsPrivacy.tsx +++ b/screen/settings/SettingsPrivacy.tsx @@ -4,7 +4,7 @@ import { openSettings } from 'react-native-permissions'; import A from '../../blue_modules/analytics'; import { BlueCard, BlueSpacing20, BlueSpacing40, BlueText } from '../../BlueComponents'; import { Header } from '../../components/Header'; -import ListItem from '../../components/ListItem'; +import ListItem, { PressableWrapper } from '../../components/ListItem'; import { useTheme } from '../../components/themes'; import { setBalanceDisplayAllowed } from '../../components/WidgetCommunication'; import loc from '../../loc'; @@ -18,11 +18,12 @@ enum SettingsPrivacySection { QuickActions, Widget, TemporaryScreenshots, + TotalBalance, } const SettingsPrivacy: React.FC = () => { const { colors } = useTheme(); - const { isStorageEncrypted } = useStorage(); + const { isStorageEncrypted, wallets } = useStorage(); const { isDoNotTrackEnabled, setDoNotTrackStorage, @@ -34,6 +35,8 @@ const SettingsPrivacy: React.FC = () => { setIsClipboardGetContentEnabledStorage, isQuickActionsEnabled, setIsQuickActionsEnabledStorage, + isTotalBalanceEnabled, + setIsTotalBalanceEnabledStorage, } = useSettings(); const [isLoading, setIsLoading] = useState(SettingsPrivacySection.All); @@ -90,6 +93,16 @@ const SettingsPrivacy: React.FC = () => { setIsLoading(SettingsPrivacySection.None); }; + const onTotalBalanceEnabledValueChange = async (value: boolean) => { + setIsLoading(SettingsPrivacySection.TotalBalance); + try { + setIsTotalBalanceEnabledStorage(value); + } catch (e) { + console.debug('onTotalBalanceEnabledValueChange catch', e); + } + setIsLoading(SettingsPrivacySection.None); + }; + const onTemporaryScreenshotsValueChange = (value: boolean) => { setIsLoading(SettingsPrivacySection.TemporaryScreenshots); setIsPrivacyBlurEnabledState(!value); @@ -139,7 +152,20 @@ const SettingsPrivacy: React.FC = () => { {storageIsEncrypted && {loc.settings.encrypted_feature_disabled}} - + + + {loc.total_balance_view.explanation} + + = memo(({ navigation }) => { const { wallets, selectedWalletID } = useStorage(); const { colors } = useTheme(); const isFocused = useIsFocused(); + const { isTotalBalanceEnabled } = useSettings(); const stylesHook = useMemo( () => @@ -143,6 +146,7 @@ const DrawerList: React.FC = memo(({ navigation }) => { showsVerticalScrollIndicator={false} >
+ {isTotalBalanceEnabled && } 22 + ? 22 + : PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26); + +type WalletTransactionsProps = NativeStackScreenProps; + +const WalletTransactions: React.FC = ({ route }) => { + const { wallets, saveToDisk, setSelectedWalletID, isElectrumDisabled, setReloadTransactionsMenuActionFunction } = useStorage(); + const { isBiometricUseCapableAndEnabled } = useBiometrics(); + const [isLoading, setIsLoading] = useState(false); + const { walletID } = route.params; + const { name } = useRoute(); + const wallet = wallets.find(w => w.getID() === walletID); + const [itemPriceUnit, setItemPriceUnit] = useState(wallet?.getPreferredBalanceUnit() ?? BitcoinUnit.BTC); + const [limit, setLimit] = useState(15); + const [pageSize] = useState(20); + const navigation = useExtendedNavigation(); + const { setOptions, navigate } = navigation; + const { colors } = useTheme(); + const walletActionButtonsRef = useRef(null); + + const stylesHook = StyleSheet.create({ + listHeaderText: { + color: colors.foregroundColor, + }, + list: { + backgroundColor: colors.background, + }, + }); + + useFocusEffect( + useCallback(() => { + setOptions(getWalletTransactionsOptions({ route })); + }, [route, setOptions]), + ); + + const getTransactions = useCallback( + (lmt = Infinity): Transaction[] => { + if (!wallet) return []; + const txs = wallet.getTransactions(); + txs.sort((a: { received: string }, b: { received: string }) => +new Date(b.received) - +new Date(a.received)); + return txs.slice(0, lmt); + }, + [wallet], + ); + + const loadMoreTransactions = useCallback(() => { + if (getTransactions(Infinity).length > limit) { + setLimit(prev => prev + pageSize); + } + }, [getTransactions, limit, pageSize]); + + const refreshTransactions = useCallback(async () => { + if (!wallet || isElectrumDisabled || isLoading) return; + setIsLoading(true); + let smthChanged = false; + try { + await BlueElectrum.waitTillConnected(); + if (wallet.allowBIP47() && wallet.isBIP47Enabled() && 'fetchBIP47SenderPaymentCodes' in wallet) { + await wallet.fetchBIP47SenderPaymentCodes(); + } + const oldBalance = wallet.getBalance(); + await wallet.fetchBalance(); + if (oldBalance !== wallet.getBalance()) smthChanged = true; + const oldTxLen = wallet.getTransactions().length; + await wallet.fetchTransactions(); + if ('fetchPendingTransactions' in wallet) { + await wallet.fetchPendingTransactions(); + } + if ('fetchUserInvoices' in wallet) { + await wallet.fetchUserInvoices(); + } + if (oldTxLen !== wallet.getTransactions().length) smthChanged = true; + } catch (err) { + presentAlert({ message: (err as Error).message, type: AlertType.Toast }); + } finally { + if (smthChanged) { + await saveToDisk(); + setLimit(prev => prev + pageSize); + } + setIsLoading(false); + } + }, [wallet, isElectrumDisabled, isLoading, saveToDisk, pageSize]); + + useEffect(() => { + if (wallet && wallet.getLastTxFetch() === 0) { + refreshTransactions(); + } + }, [wallet, refreshTransactions]); + + useEffect(() => { + if (wallet) { + setSelectedWalletID(wallet.getID()); + } + }, [wallet, setSelectedWalletID]); + + const isLightning = (): boolean => wallet?.chain === Chain.OFFCHAIN || false; + + const renderListFooterComponent = () => { + // if not all txs rendered - display indicator + return wallet && wallet.getTransactions().length > limit ? : ; + }; + + const renderListHeaderComponent = () => { + const style: any = {}; + if (!isDesktop) { + // we need this button for testing + style.opacity = 0; + style.height = 1; + style.width = 1; + } else if (isLoading) { + style.opacity = 0.5; + } else { + style.opacity = 1.0; + } + + return ( + + + {loc.transactions.list_title} + + + ); + }; + + const navigateToSendScreen = () => { + navigate('SendDetailsRoot', { + screen: 'SendDetails', + params: { + walletID: wallet?.getID(), + }, + }); + }; + + const onWalletSelect = async (selectedWallet: TWallet) => { + if (selectedWallet) { + navigate('WalletTransactions', { + walletType: wallet?.type, + walletID: wallet?.getID(), + key: `WalletTransactions-${wallet?.getID()}`, + }); + if (wallet?.type === LightningCustodianWallet.type) { + let toAddress; + if (wallet?.refill_addressess.length > 0) { + toAddress = wallet.refill_addressess[0]; + } else { + try { + await wallet?.fetchBtcAddress(); + toAddress = wallet?.refill_addressess[0]; + } catch (Err) { + return presentAlert({ message: (Err as Error).message, type: AlertType.Toast }); + } + } + navigate('SendDetailsRoot', { + screen: 'SendDetails', + params: { + memo: loc.lnd.refill_lnd_balance, + address: toAddress, + walletID: selectedWallet.getID(), + }, + }); + } + } + }; + + const navigateToViewEditCosigners = () => { + navigate('ViewEditMultisigCosignersRoot', { + screen: 'ViewEditMultisigCosigners', + params: { + walletID, + }, + }); + }; + + const onManageFundsPressed = (id?: string) => { + if (id === actionKeys.Refill) { + const availableWallets = wallets.filter(item => item.chain === Chain.ONCHAIN && item.allowSend()); + if (availableWallets.length === 0) { + presentAlert({ message: loc.lnd.refill_create }); + } else { + navigate('SelectWallet', { onWalletSelect, chainType: Chain.ONCHAIN }); + } + } else if (id === actionKeys.RefillWithExternalWallet) { + navigate('ReceiveDetailsRoot', { + screen: 'ReceiveDetails', + params: { + walletID, + }, + }); + } + }; + + const getItemLayout = (_: any, index: number) => ({ + length: 64, + offset: 64 * index, + index, + }); + + const renderItem = (item: { item: Transaction }) => ( + + ); + + const onBarCodeRead = useCallback( + (ret?: { data?: any }) => { + if (!isLoading) { + setIsLoading(true); + const params = { + walletID: wallet?.getID(), + uri: ret?.data ? ret.data : ret, + }; + if (wallet?.chain === Chain.ONCHAIN) { + navigate('SendDetailsRoot', { screen: 'SendDetails', params }); + } else { + navigate('ScanLndInvoiceRoot', { screen: 'ScanLndInvoice', params }); + } + setIsLoading(false); + } + }, + [wallet, navigate, isLoading], + ); + + const choosePhoto = () => { + fs.showImagePickerAndReadImage() + .then(data => { + if (data) { + onBarCodeRead({ data }); + } + }) + .catch(error => { + console.log(error); + triggerHapticFeedback(HapticFeedbackTypes.NotificationError); + presentAlert({ title: loc.errors.error, message: error.message }); + }); + }; + + const _keyExtractor = (_item: any, index: number) => index.toString(); + + const copyFromClipboard = async () => { + onBarCodeRead({ data: await BlueClipboard().getClipboardContent() }); + }; + + const sendButtonPress = () => { + if (wallet?.chain === Chain.OFFCHAIN) { + return navigate('ScanLndInvoiceRoot', { screen: 'ScanLndInvoice', params: { walletID: wallet.getID() } }); + } + + if (wallet?.type === WatchOnlyWallet.type && wallet.isHd() && !wallet.useWithHardwareWalletEnabled()) { + return Alert.alert( + loc.wallets.details_title, + loc.transactions.enable_offline_signing, + [ + { + text: loc._.ok, + onPress: async () => { + wallet.setUseWithHardwareWalletEnabled(true); + await saveToDisk(); + navigateToSendScreen(); + }, + style: 'default', + }, + { text: loc._.cancel, onPress: () => {}, style: 'cancel' }, + ], + { cancelable: false }, + ); + } + + navigateToSendScreen(); + }; + + const sendButtonLongPress = async () => { + const isClipboardEmpty = (await BlueClipboard().getClipboardContent()).trim().length === 0; + const options = [loc._.cancel, loc.wallets.list_long_choose, loc.wallets.list_long_scan]; + const cancelButtonIndex = 0; + + if (!isClipboardEmpty) { + options.push(loc.wallets.list_long_clipboard); + } + + ActionSheet.showActionSheetWithOptions( + { + title: loc.send.header, + options, + cancelButtonIndex, + anchor: findNodeHandle(walletActionButtonsRef.current) ?? undefined, + }, + async buttonIndex => { + switch (buttonIndex) { + case 0: + break; + case 1: { + choosePhoto(); + break; + } + case 2: { + const data = await scanQrHelper(name, true); + if (data) { + onBarCodeRead({ data }); + } + break; + } + case 3: + if (!isClipboardEmpty) { + copyFromClipboard(); + } + break; + } + }, + ); + }; + + useFocusEffect( + useCallback(() => { + const task = InteractionManager.runAfterInteractions(() => { + setReloadTransactionsMenuActionFunction(() => refreshTransactions); + }); + return () => { + task.cancel(); + setReloadTransactionsMenuActionFunction(() => {}); + }; + }, [setReloadTransactionsMenuActionFunction, refreshTransactions]), + ); + + const refreshProps = isDesktop || isElectrumDisabled ? {} : { refreshing: isLoading, onRefresh: refreshTransactions }; + + return ( + + {wallet && ( + { + setItemPriceUnit(passedWallet.getPreferredBalanceUnit()); + await saveToDisk(); + }} + onWalletBalanceVisibilityChange={async isShouldBeVisible => { + const isBiometricsEnabled = await isBiometricUseCapableAndEnabled(); + if (wallet?.hideBalance && isBiometricsEnabled) { + const unlocked = await unlockWithBiometrics(); + if (!unlocked) throw new Error('Biometrics failed'); + } + wallet!.hideBalance = isShouldBeVisible; + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + await saveToDisk(); + }} + onManageFundsPressed={id => { + if (wallet?.type === MultisigHDWallet.type) { + navigateToViewEditCosigners(); + } else if (wallet?.type === LightningCustodianWallet.type) { + if (wallet.getUserHasSavedExport()) { + if (!id) return; + onManageFundsPressed(id); + } else { + presentWalletExportReminder() + .then(async () => { + if (!id) return; + wallet!.setUserHasSavedExport(true); + await saveToDisk(); + onManageFundsPressed(id); + }) + .catch(() => { + navigate('WalletExportRoot', { + screen: 'WalletExport', + params: { + walletID: wallet!.getID(), + }, + }); + }); + } + } + }} + /> + )} + + {wallet?.type === WatchOnlyWallet.type && wallet.isWatchOnlyWarningVisible && ( + { + wallet.isWatchOnlyWarningVisible = false; + LayoutAnimation.configureNext(LayoutAnimation.Presets.linear); + saveToDisk(); + }} + /> + )} + + + {(isLightning() && loc.wallets.list_empty_txs1_lightning) || loc.wallets.list_empty_txs1} + + {isLightning() && {loc.wallets.list_empty_txs2_lightning}} + + } + {...refreshProps} + data={getTransactions(limit)} + extraData={wallet} + keyExtractor={_keyExtractor} + renderItem={renderItem} + initialNumToRender={10} + removeClippedSubviews + contentInset={{ top: 0, left: 0, bottom: 90, right: 0 }} + maxToRenderPerBatch={15} + windowSize={25} + /> + + + {wallet?.allowReceive() && ( + { + if (wallet.chain === Chain.OFFCHAIN) { + navigate('LNDCreateInvoiceRoot', { screen: 'LNDCreateInvoice', params: { walletID: wallet.getID() } }); + } else { + navigate('ReceiveDetailsRoot', { screen: 'ReceiveDetails', params: { walletID: wallet.getID() } }); + } + }} + icon={ + + + + } + /> + )} + {(wallet?.allowSend() || (wallet?.type === WatchOnlyWallet.type && wallet.isHd())) && ( + + + + } + /> + )} + + + ); +}; + +export default WalletTransactions; + +const styles = StyleSheet.create({ + flex: { flex: 1 }, + scrollViewContent: { flex: 1, justifyContent: 'center', paddingHorizontal: 16, paddingBottom: 40 }, + activityIndicator: { marginVertical: 20 }, + listHeaderTextRow: { flex: 1, margin: 16, flexDirection: 'row', justifyContent: 'space-between' }, + listHeaderText: { marginTop: 8, marginBottom: 8, fontWeight: 'bold', fontSize: 24 }, + list: { flex: 1 }, + emptyTxs: { fontSize: 18, color: '#9aa0aa', textAlign: 'center', marginVertical: 16 }, + emptyTxsLightning: { fontSize: 18, color: '#9aa0aa', textAlign: 'center', fontWeight: '600' }, + sendIcon: { transform: [{ rotate: I18nManager.isRTL ? '-225deg' : '225deg' }] }, + receiveIcon: { transform: [{ rotate: I18nManager.isRTL ? '45deg' : '-45deg' }] }, +}); diff --git a/screen/wallets/WalletsList.tsx b/screen/wallets/WalletsList.tsx index 218cdc3aa..5473dffc8 100644 --- a/screen/wallets/WalletsList.tsx +++ b/screen/wallets/WalletsList.tsx @@ -21,6 +21,8 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList'; import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; import { useStorage } from '../../hooks/context/useStorage'; +import TotalWalletsBalance from '../../components/TotalWalletsBalance'; +import { useSettings } from '../../hooks/context/useSettings'; const WalletsListSections = { CAROUSEL: 'CAROUSEL', TRANSACTIONS: 'TRANSACTIONS' }; @@ -105,6 +107,7 @@ const WalletsList: React.FC = () => { isElectrumDisabled, setReloadTransactionsMenuActionFunction, } = useStorage(); + const { isTotalBalanceEnabled } = useSettings(); const { width } = useWindowDimensions(); const { colors, scanImage } = useTheme(); const { navigate } = useExtendedNavigation(); @@ -252,18 +255,20 @@ const WalletsList: React.FC = () => { const renderWalletsCarousel = useCallback(() => { return ( - + <> + + ); }, [handleClick, handleLongPress, isFocused, onSnapToItem, wallets]); @@ -286,11 +291,15 @@ const WalletsList: React.FC = () => { switch (section.section.key) { case WalletsListSections.TRANSACTIONS: return renderListHeaderComponent(); + case WalletsListSections.CAROUSEL: { + return !isLargeScreen && isTotalBalanceEnabled ? : null; + } + default: return null; } }, - [renderListHeaderComponent], + [isLargeScreen, isTotalBalanceEnabled, renderListHeaderComponent], ); const renderSectionFooter = useCallback( diff --git a/screen/wallets/details.js b/screen/wallets/details.js index 06ddc6f6e..3274d6438 100644 --- a/screen/wallets/details.js +++ b/screen/wallets/details.js @@ -215,10 +215,10 @@ const WalletDetails = () => { externalAddresses = wallet.getAllExternalAddresses(); } catch (_) {} Notifications.unsubscribe(externalAddresses, [], []); - popToTop(); deleteWallet(wallet); saveToDisk(true); triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess); + popToTop(); }; const presentWalletHasBalanceAlert = useCallback(async () => { diff --git a/screen/wallets/transactions.js b/screen/wallets/transactions.js deleted file mode 100644 index 1a555ae1d..000000000 --- a/screen/wallets/transactions.js +++ /dev/null @@ -1,653 +0,0 @@ -import { useFocusEffect, useRoute } from '@react-navigation/native'; -import PropTypes from 'prop-types'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { - ActivityIndicator, - Alert, - Dimensions, - findNodeHandle, - FlatList, - I18nManager, - InteractionManager, - LayoutAnimation, - PixelRatio, - ScrollView, - StyleSheet, - Text, - View, -} from 'react-native'; -import { Icon } from '@rneui/themed'; - -import * as BlueElectrum from '../../blue_modules/BlueElectrum'; -import BlueClipboard from '../../blue_modules/clipboard'; -import { isDesktop } from '../../blue_modules/environment'; -import * as fs from '../../blue_modules/fs'; -import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback'; -import { LightningCustodianWallet, MultisigHDWallet, WatchOnlyWallet } from '../../class'; -import WalletGradient from '../../class/wallet-gradient'; -import presentAlert, { AlertType } from '../../components/Alert'; -import { FButton, FContainer } from '../../components/FloatButtons'; -import { useTheme } from '../../components/themes'; -import { TransactionListItem } from '../../components/TransactionListItem'; -import TransactionsNavigationHeader, { actionKeys } from '../../components/TransactionsNavigationHeader'; -import { presentWalletExportReminder } from '../../helpers/presentWalletExportReminder'; -import { scanQrHelper } from '../../helpers/scan-qr'; -import { unlockWithBiometrics, useBiometrics } from '../../hooks/useBiometrics'; -import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; -import loc from '../../loc'; -import { Chain } from '../../models/bitcoinUnits'; -import ActionSheet from '../ActionSheet'; -import { useStorage } from '../../hooks/context/useStorage'; -import { WalletTransactionsStatus } from '../../components/Context/StorageProvider'; -import WatchOnlyWarning from '../../components/WatchOnlyWarning'; - -const buttonFontSize = - PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26) > 22 - ? 22 - : PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26); - -const WalletTransactions = ({ navigation }) => { - const { - wallets, - saveToDisk, - setSelectedWalletID, - walletTransactionUpdateStatus, - isElectrumDisabled, - setReloadTransactionsMenuActionFunction, - } = useStorage(); - const { isBiometricUseCapableAndEnabled } = useBiometrics(); - const [isLoading, setIsLoading] = useState(false); - const { walletID } = useRoute().params; - const { name } = useRoute(); - const wallet = wallets.find(w => w.getID() === walletID); - const [itemPriceUnit, setItemPriceUnit] = useState(wallet.getPreferredBalanceUnit()); - const [dataSource, setDataSource] = useState(wallet.getTransactions(15)); - const [isRefreshing, setIsRefreshing] = useState(false); // a simple flag to know that wallet was being updated once - const [timeElapsed, setTimeElapsed] = useState(0); - const [limit, setLimit] = useState(15); - const [pageSize, setPageSize] = useState(20); - const { setParams, setOptions, navigate } = useExtendedNavigation(); - const { colors } = useTheme(); - const walletActionButtonsRef = useRef(); - - const stylesHook = StyleSheet.create({ - listHeaderText: { - color: colors.foregroundColor, - }, - list: { - backgroundColor: colors.background, - }, - }); - - /** - * Simple wrapper for `wallet.getTransactions()`, where `wallet` is current wallet. - * Sorts. Provides limiting. - * - * @param lmt {Integer} How many txs return, starting from the earliest. Default: all of them. - * @returns {Array} - */ - const getTransactionsSliced = (lmt = Infinity) => { - let txs = wallet.getTransactions(); - for (const tx of txs) { - tx.sort_ts = +new Date(tx.received); - } - txs = txs.sort(function (a, b) { - return b.sort_ts - a.sort_ts; - }); - return txs.slice(0, lmt); - }; - - useEffect(() => { - const interval = setInterval(() => setTimeElapsed(prev => prev + 1), 60000); - return () => { - clearInterval(interval); - }; - }, []); - - useEffect(() => { - if (walletTransactionUpdateStatus === walletID) { - // wallet is being refreshed, drawing the 'Updating...' header: - setOptions({ headerTitle: loc.transactions.updating }); - setIsRefreshing(true); - } else { - setOptions({ headerTitle: '' }); - } - - if (isRefreshing && walletTransactionUpdateStatus === WalletTransactionsStatus.NONE) { - // if we are here this means that wallet was being updated (`walletTransactionUpdateStatus` was set, and - // `isRefreshing` flag was set) and we displayed "Updating..." message, - // and when it ended `walletTransactionUpdateStatus` became false (flag `isRefreshing` stayed). - // chances are that txs list changed for the wallet, so we need to re-render: - console.log('re-rendering transactions'); - setDataSource([...getTransactionsSliced(limit)]); - setIsRefreshing(false); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [walletTransactionUpdateStatus]); - - useEffect(() => { - setIsLoading(true); - setLimit(15); - setPageSize(20); - setTimeElapsed(0); - setItemPriceUnit(wallet.getPreferredBalanceUnit()); - setIsLoading(false); - setSelectedWalletID(wallet.getID()); - setDataSource([...getTransactionsSliced(limit)]); - setOptions({ - headerBackTitle: wallet.getLabel(), - headerBackTitleVisible: true, - headerStyle: { - backgroundColor: WalletGradient.headerColorFor(wallet.type), - borderBottomWidth: 0, - elevation: 0, - // shadowRadius: 0, - shadowOffset: { height: 0, width: 0 }, - }, - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [wallet]); - - useEffect(() => { - const newWallet = wallets.find(w => w.getID() === walletID); - if (newWallet) { - setParams({ walletID, isLoading: false }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [walletID]); - - // refresh transactions if it never hasn't been done. It could be a fresh imported wallet - useEffect(() => { - if (wallet.getLastTxFetch() === 0) { - refreshTransactions(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - // if description of transaction has been changed we want to show new one - useFocusEffect( - useCallback(() => { - setTimeElapsed(prev => prev + 1); - }, []), - ); - - const isLightning = () => { - const w = wallet; - if (w && w.chain === Chain.OFFCHAIN) { - return true; - } - - return false; - }; - - /** - * Forcefully fetches TXs and balance for wallet - */ - const refreshTransactions = async () => { - if (isElectrumDisabled) return setIsLoading(false); - if (isLoading) return; - setIsLoading(true); - let noErr = true; - let smthChanged = false; - try { - // await BlueElectrum.ping(); - await BlueElectrum.waitTillConnected(); - if (wallet.allowBIP47() && wallet.isBIP47Enabled()) { - const pcStart = +new Date(); - await wallet.fetchBIP47SenderPaymentCodes(); - const pcEnd = +new Date(); - console.log(wallet.getLabel(), 'fetch payment codes took', (pcEnd - pcStart) / 1000, 'sec'); - } - const balanceStart = +new Date(); - const oldBalance = wallet.getBalance(); - await wallet.fetchBalance(); - if (oldBalance !== wallet.getBalance()) smthChanged = true; - const balanceEnd = +new Date(); - console.log(wallet.getLabel(), 'fetch balance took', (balanceEnd - balanceStart) / 1000, 'sec'); - const start = +new Date(); - const oldTxLen = wallet.getTransactions().length; - let immatureTxsConfs = ''; // a silly way to keep track if anything changed in immature transactions - for (const tx of wallet.getTransactions()) { - if (tx.confirmations < 7) immatureTxsConfs += tx.txid + ':' + tx.confirmations + ';'; - } - await wallet.fetchTransactions(); - if (wallet.fetchPendingTransactions) { - await wallet.fetchPendingTransactions(); - } - if (wallet.fetchUserInvoices) { - await wallet.fetchUserInvoices(); - } - if (oldTxLen !== wallet.getTransactions().length) smthChanged = true; - let unconfirmedTxsConfs2 = ''; // a silly way to keep track if anything changed in immature transactions - for (const tx of wallet.getTransactions()) { - if (tx.confirmations < 7) unconfirmedTxsConfs2 += tx.txid + ':' + tx.confirmations + ';'; - } - if (unconfirmedTxsConfs2 !== immatureTxsConfs) { - smthChanged = true; - } - const end = +new Date(); - console.log(wallet.getLabel(), 'fetch tx took', (end - start) / 1000, 'sec'); - } catch (err) { - noErr = false; - presentAlert({ message: err.message, type: AlertType.Toast }); - setIsLoading(false); - setTimeElapsed(prev => prev + 1); - } - if (noErr && smthChanged) { - console.log('saving to disk'); - await saveToDisk(); // caching - setDataSource([...getTransactionsSliced(limit)]); - } - setIsLoading(false); - setTimeElapsed(prev => prev + 1); - }; - - const _keyExtractor = (_item, index) => index.toString(); - - const renderListFooterComponent = () => { - // if not all txs rendered - display indicator - return (wallet.getTransactions().length > limit && ) || ; - }; - - const renderListHeaderComponent = () => { - const style = {}; - if (!isDesktop) { - // we need this button for testing - style.opacity = 0; - style.height = 1; - style.width = 1; - } else if (isLoading) { - style.opacity = 0.5; - } else { - style.opacity = 1.0; - } - - return ( - - - {loc.transactions.list_title} - - - ); - }; - const onWalletSelect = async selectedWallet => { - if (selectedWallet) { - navigate('WalletTransactions', { - walletType: wallet.type, - walletID: wallet.getID(), - key: `WalletTransactions-${wallet.getID()}`, - }); - /** @type {LightningCustodianWallet} */ - let toAddress = false; - if (wallet.refill_addressess.length > 0) { - toAddress = wallet.refill_addressess[0]; - } else { - try { - await wallet.fetchBtcAddress(); - toAddress = wallet.refill_addressess[0]; - } catch (Err) { - return presentAlert({ message: Err.message, type: AlertType.Toast }); - } - } - navigate('SendDetailsRoot', { - screen: 'SendDetails', - params: { - memo: loc.lnd.refill_lnd_balance, - address: toAddress, - walletID: selectedWallet.getID(), - }, - }); - } - }; - const navigateToSendScreen = () => { - navigate('SendDetailsRoot', { - screen: 'SendDetails', - params: { - walletID: wallet.getID(), - }, - }); - }; - - const renderItem = item => ( - - ); - - const onBarCodeRead = ret => { - if (!isLoading) { - setIsLoading(true); - const params = { - walletID: wallet.getID(), - uri: ret.data ? ret.data : ret, - }; - if (wallet.chain === Chain.ONCHAIN) { - navigate('SendDetailsRoot', { screen: 'SendDetails', params }); - } else { - navigate('ScanLndInvoiceRoot', { screen: 'ScanLndInvoice', params }); - } - } - setIsLoading(false); - }; - - const choosePhoto = () => { - fs.showImagePickerAndReadImage() - .then(onBarCodeRead) - .catch(error => { - console.log(error); - triggerHapticFeedback(HapticFeedbackTypes.NotificationError); - presentAlert({ title: loc.errors.error, message: error.message }); - }); - }; - - const copyFromClipboard = async () => { - onBarCodeRead({ data: await BlueClipboard().getClipboardContent() }); - }; - - const sendButtonPress = () => { - if (wallet.chain === Chain.OFFCHAIN) { - return navigate('ScanLndInvoiceRoot', { screen: 'ScanLndInvoice', params: { walletID: wallet.getID() } }); - } - - if (wallet.type === WatchOnlyWallet.type && wallet.isHd() && !wallet.useWithHardwareWalletEnabled()) { - return Alert.alert( - loc.wallets.details_title, - loc.transactions.enable_offline_signing, - [ - { - text: loc._.ok, - onPress: async () => { - wallet.setUseWithHardwareWalletEnabled(true); - await saveToDisk(); - navigateToSendScreen(); - }, - style: 'default', - }, - - { text: loc._.cancel, onPress: () => {}, style: 'cancel' }, - ], - { cancelable: false }, - ); - } - - navigateToSendScreen(); - }; - - const sendButtonLongPress = async () => { - const isClipboardEmpty = (await BlueClipboard().getClipboardContent()).trim().length === 0; - const options = [loc._.cancel, loc.wallets.list_long_choose, loc.wallets.list_long_scan]; - const cancelButtonIndex = 0; - - if (!isClipboardEmpty) { - options.push(loc.wallets.list_long_clipboard); - } - - ActionSheet.showActionSheetWithOptions( - { - title: loc.send.header, - options, - cancelButtonIndex, - anchor: findNodeHandle(walletActionButtonsRef.current), - }, - async buttonIndex => { - switch (buttonIndex) { - case 0: - break; - case 1: - choosePhoto(); - break; - case 2: - scanQrHelper(name, true).then(data => onBarCodeRead(data)); - break; - case 3: - if (!isClipboardEmpty) { - copyFromClipboard(); - } - break; - } - }, - ); - }; - - const navigateToViewEditCosigners = () => { - navigate('ViewEditMultisigCosignersRoot', { - screen: 'ViewEditMultisigCosigners', - params: { - walletID, - }, - }); - }; - - const onManageFundsPressed = ({ id }) => { - if (id === actionKeys.Refill) { - const availableWallets = [...wallets.filter(item => item.chain === Chain.ONCHAIN && item.allowSend())]; - if (availableWallets.length === 0) { - presentAlert({ message: loc.lnd.refill_create }); - } else { - navigate('SelectWallet', { onWalletSelect, chainType: Chain.ONCHAIN }); - } - } else if (id === actionKeys.RefillWithExternalWallet) { - navigate('ReceiveDetailsRoot', { - screen: 'ReceiveDetails', - params: { - walletID, - }, - }); - } - }; - - useEffect(() => { - setOptions({ statusBarStyle: 'light', barTintColor: WalletGradient.headerColorFor(wallet.type) }); - }, [setOptions, wallet.type]); - - const getItemLayout = (_, index) => ({ - length: 64, - offset: 64 * index, - index, - }); - - useFocusEffect( - useCallback(() => { - const task = InteractionManager.runAfterInteractions(() => { - setReloadTransactionsMenuActionFunction(() => refreshTransactions); - }); - return () => { - task.cancel(); - setReloadTransactionsMenuActionFunction(() => {}); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []), - ); - - // Optimized for Mac option doesn't like RN Refresh component. Menu Elements now handles it for macOS - const refreshProps = isDesktop || isElectrumDisabled ? {} : { refreshing: isLoading, onRefresh: refreshTransactions }; - - return ( - - - InteractionManager.runAfterInteractions(async () => { - setItemPriceUnit(passedWallet.getPreferredBalanceUnit()); - saveToDisk(); - }) - } - onWalletBalanceVisibilityChange={async isShouldBeVisible => { - const isBiometricsEnabled = await isBiometricUseCapableAndEnabled(); - - if (wallet.hideBalance && isBiometricsEnabled) { - const unlocked = await unlockWithBiometrics(); - if (!unlocked) { - throw new Error('Biometrics failed'); - } - } - - wallet.hideBalance = isShouldBeVisible; - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); - await saveToDisk(); - }} - onManageFundsPressed={id => { - if (wallet.type === MultisigHDWallet.type) { - navigateToViewEditCosigners(); - } else if (wallet.type === LightningCustodianWallet.type) { - if (wallet.getUserHasSavedExport()) { - onManageFundsPressed({ id }); - } else { - presentWalletExportReminder() - .then(async () => { - wallet.setUserHasSavedExport(true); - await saveToDisk(); - onManageFundsPressed({ id }); - }) - .catch(() => { - navigate('WalletExportRoot', { - screen: 'WalletExport', - params: { - walletID: wallet.getID(), - }, - }); - }); - } - } - }} - /> - - {wallet.type === WatchOnlyWallet.type && wallet.isWatchOnlyWarningVisible && ( - { - wallet.isWatchOnlyWarningVisible = false; - LayoutAnimation.configureNext(LayoutAnimation.Presets.linear); - saveToDisk(); - }} - /> - )} - { - // pagination in works. in this block we will add more txs to FlatList - // so as user scrolls closer to bottom it will render mode transactions - - if (getTransactionsSliced(Infinity).length < limit) { - // all list rendered. nop - return; - } - - setDataSource(getTransactionsSliced(limit + pageSize)); - setLimit(prev => prev + pageSize); - setPageSize(prev => prev * 2); - }} - ListFooterComponent={renderListFooterComponent} - ListEmptyComponent={ - - - {(isLightning() && loc.wallets.list_empty_txs1_lightning) || loc.wallets.list_empty_txs1} - - {isLightning() && {loc.wallets.list_empty_txs2_lightning}} - - } - {...refreshProps} - data={dataSource} - extraData={[timeElapsed, dataSource, wallets]} - keyExtractor={_keyExtractor} - renderItem={renderItem} - initialNumToRender={10} - removeClippedSubviews - contentInset={{ top: 0, left: 0, bottom: 90, right: 0 }} - maxToRenderPerBatch={15} - windowSize={25} - /> - - - - {wallet.allowReceive() && ( - { - if (wallet.chain === Chain.OFFCHAIN) { - navigate('LNDCreateInvoiceRoot', { screen: 'LNDCreateInvoice', params: { walletID: wallet.getID() } }); - } else { - navigate('ReceiveDetailsRoot', { screen: 'ReceiveDetails', params: { walletID: wallet.getID() } }); - } - }} - icon={ - - - - } - /> - )} - {(wallet.allowSend() || (wallet.type === WatchOnlyWallet.type && wallet.isHd())) && ( - - - - } - /> - )} - - - ); -}; - -export default WalletTransactions; - -WalletTransactions.propTypes = { - navigation: PropTypes.shape(), -}; - -const styles = StyleSheet.create({ - flex: { - flex: 1, - }, - scrollViewContent: { - flex: 1, - justifyContent: 'center', - paddingHorizontal: 16, - paddingBottom: 40, - }, - activityIndicator: { - marginVertical: 20, - }, - listHeaderTextRow: { - flex: 1, - margin: 16, - flexDirection: 'row', - justifyContent: 'space-between', - }, - listHeaderText: { - marginTop: 8, - marginBottom: 8, - fontWeight: 'bold', - fontSize: 24, - }, - list: { - flex: 1, - }, - emptyTxs: { - fontSize: 18, - color: '#9aa0aa', - textAlign: 'center', - marginVertical: 16, - }, - emptyTxsLightning: { - fontSize: 18, - color: '#9aa0aa', - textAlign: 'center', - fontWeight: '600', - }, - sendIcon: { - transform: [{ rotate: I18nManager.isRTL ? '-225deg' : '225deg' }], - }, - receiveIcon: { - transform: [{ rotate: I18nManager.isRTL ? '45deg' : '-45deg' }], - }, -}); diff --git a/typings/CommonToolTipActions.ts b/typings/CommonToolTipActions.ts index d1f816ddc..2f0976e89 100644 --- a/typings/CommonToolTipActions.ts +++ b/typings/CommonToolTipActions.ts @@ -7,6 +7,10 @@ const keys = { OpenInBlockExplorer: 'open_in_blockExplorer', CopyAmount: 'copyAmount', CopyNote: 'copyNote', + HideBalance: 'hideBalance', + ViewInBitcoin: 'viewInBitcoin', + ViewInSats: 'viewInSats', + ViewInFiat: 'viewInFiat', }; const icons = { @@ -25,6 +29,12 @@ const icons = { Note: { iconValue: 'note.text', }, + ViewInBitcoin: { + iconValue: 'bitcoinsign.circle', + }, + ViewInFiat: { + iconValue: 'coloncurrencysign.circle', + }, }; export const CommonToolTipActions = { @@ -58,4 +68,25 @@ export const CommonToolTipActions = { text: loc.transactions.details_copy_note, icon: icons.Clipboard, }, + HideBalance: { + id: keys.HideBalance, + text: loc.transactions.details_balance_hide, + icon: icons.EyeSlash, + }, + ViewInFiat: { + id: keys.ViewInFiat, + text: loc.total_balance_view.view_in_fiat, + icon: icons.ViewInFiat, + }, + + ViewInSats: { + id: keys.ViewInSats, + text: loc.total_balance_view.view_in_sats, + icon: icons.ViewInBitcoin, + }, + ViewInBitcoin: { + id: keys.ViewInBitcoin, + text: loc.total_balance_view.view_in_bitcoin, + icon: icons.ViewInBitcoin, + }, };