Merge branch 'master' into man

This commit is contained in:
Marcos Rodriguez Velez 2024-08-25 13:26:35 -04:00
commit 1dac884b35
27 changed files with 1047 additions and 784 deletions

View file

@ -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"

View file

@ -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'
}

View file

@ -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<boolean> => {
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<BitcoinUnit> => {
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<void>;
isQuickActionsEnabled: boolean;
setIsQuickActionsEnabledStorage: (value: boolean) => Promise<void>;
isTotalBalanceEnabled: boolean;
setIsTotalBalanceEnabledStorage: (value: boolean) => Promise<void>;
totalBalancePreferredUnit: BitcoinUnit;
setTotalBalancePreferredUnitStorage: (unit: BitcoinUnit) => Promise<void>;
}
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<SettingsContextType>(defaultSettingsContext);
@ -81,6 +137,9 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil
const [isClipboardGetContentEnabled, setIsClipboardGetContentEnabled] = useState<boolean>(false);
// Quick Actions
const [isQuickActionsEnabled, setIsQuickActionsEnabled] = useState<boolean>(true);
// Total Balance
const [isTotalBalanceEnabled, setIsTotalBalanceEnabled] = useState<boolean>(true);
const [totalBalancePreferredUnit, setTotalBalancePreferredUnitState] = useState<BitcoinUnit>(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,
],
);

View file

@ -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
<TouchableOpacity
accessibilityLabel={text}
accessibilityRole="button"
testID={testID}
style={[bStyles.root, bStylesHook.root, style, additionalStyles]}
{...props}
>

View file

@ -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 && (
<ToolTipMenu actions={toolTipActions} onPressMenuItem={onPressMenuItem}>
<View style={styles.container}>
<Text style={styles.label}>{loc.wallets.total_balance}</Text>
<TouchableOpacity onPress={() => onPressMenuItem(CommonToolTipActions.ViewInBitcoin.id)}>
<Text style={[styles.balance, styleHooks.balance]}>
{formattedBalance}{' '}
{totalBalancePreferredUnit !== BitcoinUnit.LOCAL_CURRENCY && (
<Text style={[styles.currency, styleHooks.currency]}>{totalBalancePreferredUnit}</Text>
)}
</Text>
</TouchableOpacity>
</View>
</ToolTipMenu>
)) ||
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;

View file

@ -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<TransactionsNavigationHeaderProps> = ({
wallet: initialWallet,
onWalletUnitChange,
navigation,
onManageFundsPressed,
onWalletBalanceVisibilityChange,
}) => {
@ -175,25 +164,24 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
];
}, [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 (
<LinearGradient
colors={WalletGradient.gradientsFor(wallet.type)}
style={styles.lineaderGradient}
{...WalletGradient.linearGradientProps(wallet.type)}
>
<Image
source={(() => {
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}
/>
<Image source={imageSource} defaultSource={imageSource} style={styles.chainIcon} />
<Text testID="WalletLabel" numberOfLines={1} style={styles.walletLabel} selectable>
{wallet.getLabel()}

View file

@ -237,7 +237,7 @@ export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
>
<View style={[iStyles.shadowContainer, { backgroundColor: colors.background, shadowColor: colors.shadowColor }]}>
<LinearGradient colors={WalletGradient.gradientsFor(item.type)} style={iStyles.grad}>
<Image source={image} style={iStyles.image} />
<Image defaultSource={image} source={image} style={iStyles.image} />
<Text style={iStyles.br} />
<Text numberOfLines={1} style={[iStyles.label, { color: colors.inverseForegroundColor }]}>
{renderHighlightedText && searchQuery ? renderHighlightedText(item.getLabel(), searchQuery) : item.getLabel()}

View file

@ -5,14 +5,14 @@ import loc from '../loc';
interface Props {
handleDismiss: () => void;
isLoading?: boolean;
disabled?: boolean;
}
const WatchOnlyWarning: React.FC<Props> = ({ handleDismiss, isLoading }) => {
const WatchOnlyWarning: React.FC<Props> = ({ handleDismiss, disabled }) => {
return (
<View style={styles.container}>
<View style={styles.content}>
<TouchableOpacity style={styles.dismissButton} onPress={handleDismiss} disabled={isLoading}>
<TouchableOpacity style={styles.dismissButton} onPress={handleDismiss} disabled={disabled}>
<Icon name="close" color="white" size={20} />
</TouchableOpacity>
<Icon name="warning" color="#FFFF" />

54
hooks/useKeyboard.ts Normal file
View file

@ -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<KeyboardInfo>({
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;
};

View file

@ -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;

View file

@ -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

View file

@ -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",

View file

@ -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",

View file

@ -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();
}
/**

View file

@ -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<DetailViewStackParamList, 'WalletTransactions'>;
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(() => <HeaderRightButton testID="SaveButton" disabled={true} title={loc.wallets.details_save} />, []);
const DetailButton = useMemo(() => <HeaderRightButton testID="DetailButton" disabled={true} title={loc.send.create_details} />, []);
@ -94,11 +92,12 @@ const DetailViewStackScreensStack = () => {
);
const useWalletListScreenOptions = useMemo<NativeStackNavigationOptions>(() => {
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' }}
>
<DetailViewStack.Screen name="WalletsList" component={WalletsList} options={navigationStyle(walletListScreenOptions)(theme)} />
<DetailViewStack.Screen name="WalletTransactions" component={WalletTransactions} options={walletTransactionsOptions} />
<DetailViewStack.Screen name="WalletTransactions" component={WalletTransactions} options={getWalletTransactionsOptions} />
<DetailViewStack.Screen
name="WalletDetails"
component={WalletDetails}

View file

@ -0,0 +1,46 @@
import { RouteProp } from '@react-navigation/native';
import { NativeStackNavigationOptions } from '@react-navigation/native-stack';
import HeaderRightButton from '../../components/HeaderRightButton';
import loc from '../../loc';
import { DetailViewStackParamList } from '../DetailViewStackParamList';
import navigationStyle from '../../components/navigationStyle';
import { Theme } from '../../components/themes';
import React from 'react';
type TransactionStatusRouteProp = RouteProp<DetailViewStackParamList, 'TransactionStatus'>;
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: () => (
<HeaderRightButton
testID="TransactionDetailsButton"
disabled={false}
title={loc.send.create_details}
onPress={navigateToTransactionDetails}
/>
),
};
};
export default getTransactionStatusOptions;

View file

@ -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<DetailViewStackParamList, 'WalletTransactions'>;
}
export type WalletTransactionsRouteProps = RouteProp<DetailViewStackParamList, 'WalletTransactions'>;
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 = (
<TouchableOpacity accessibilityRole="button" testID="WalletDetails" disabled={isLoading} style={styles.walletDetails} onPress={onPress}>
<Icon name="more-horiz" type="material" size={22} color="#FFFFFF" />

18
package-lock.json generated
View file

@ -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"

View file

@ -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",

View file

@ -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

View file

@ -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<number>(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 = () => {
<BlueSpacing20 />
{storageIsEncrypted && <BlueText>{loc.settings.encrypted_feature_disabled}</BlueText>}
</BlueCard>
<ListItem
title={loc.total_balance_view.title}
Component={PressableWrapper}
switch={{
onValueChange: onTotalBalanceEnabledValueChange,
value: isTotalBalanceEnabled,
disabled: isLoading === SettingsPrivacySection.All || wallets.length < 2,
testID: 'TotalBalanceSwitch',
}}
/>
<BlueCard>
<BlueText>{loc.total_balance_view.explanation}</BlueText>
<BlueSpacing20 />
</BlueCard>
<ListItem
title={loc.settings.privacy_temporary_screenshots}
Component={TouchableWithoutFeedback}

View file

@ -10,6 +10,8 @@ import { useTheme } from '../../components/themes';
import WalletsCarousel from '../../components/WalletsCarousel';
import loc from '../../loc';
import { useStorage } from '../../hooks/context/useStorage';
import TotalWalletsBalance from '../../components/TotalWalletsBalance';
import { useSettings } from '../../hooks/context/useSettings';
enum WalletActionType {
SetWallets = 'SET_WALLETS',
@ -87,6 +89,7 @@ const DrawerList: React.FC<DrawerListProps> = 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<DrawerListProps> = memo(({ navigation }) => {
showsVerticalScrollIndicator={false}
>
<Header leftText={loc.wallets.list_title} onNewWalletPress={onNewWalletPress} isDrawerList />
{isTotalBalanceEnabled && <TotalWalletsBalance />}
<WalletsCarousel
data={state.wallets}
extraData={[state.wallets]}

View file

@ -0,0 +1,504 @@
import { useFocusEffect, useRoute } from '@react-navigation/native';
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 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 { scanQrHelper } from '../../helpers/scan-qr';
import { unlockWithBiometrics, useBiometrics } from '../../hooks/useBiometrics';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import loc from '../../loc';
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
import ActionSheet from '../ActionSheet';
import { useStorage } from '../../hooks/context/useStorage';
import WatchOnlyWarning from '../../components/WatchOnlyWarning';
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList';
import { Transaction, TWallet } from '../../class/wallets/types';
import getWalletTransactionsOptions from '../../navigation/helpers/getWalletTransactionsOptions';
import { presentWalletExportReminder } from '../../helpers/presentWalletExportReminder';
const buttonFontSize =
PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26) > 22
? 22
: PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26);
type WalletTransactionsProps = NativeStackScreenProps<DetailViewStackParamList, 'WalletTransactions'>;
const WalletTransactions: React.FC<WalletTransactionsProps> = ({ 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<BitcoinUnit>(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<View>(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 ? <ActivityIndicator style={styles.activityIndicator} /> : <View />;
};
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 (
<View style={styles.flex}>
<View style={styles.listHeaderTextRow}>
<Text style={[styles.listHeaderText, stylesHook.listHeaderText]}>{loc.transactions.list_title}</Text>
</View>
</View>
);
};
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 }) => (
<TransactionListItem item={item.item} itemPriceUnit={itemPriceUnit} walletID={walletID} />
);
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 (
<View style={styles.flex}>
{wallet && (
<TransactionsNavigationHeader
wallet={wallet}
onWalletUnitChange={async passedWallet => {
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(),
},
});
});
}
}
}}
/>
)}
<View style={[styles.list, stylesHook.list]}>
{wallet?.type === WatchOnlyWallet.type && wallet.isWatchOnlyWarningVisible && (
<WatchOnlyWarning
disabled={isLoading}
handleDismiss={() => {
wallet.isWatchOnlyWarningVisible = false;
LayoutAnimation.configureNext(LayoutAnimation.Presets.linear);
saveToDisk();
}}
/>
)}
<FlatList
getItemLayout={getItemLayout}
updateCellsBatchingPeriod={30}
ListHeaderComponent={renderListHeaderComponent}
onEndReachedThreshold={0.3}
onEndReached={loadMoreTransactions}
ListFooterComponent={renderListFooterComponent}
ListEmptyComponent={
<ScrollView style={styles.flex} contentContainerStyle={styles.scrollViewContent}>
<Text numberOfLines={0} style={styles.emptyTxs}>
{(isLightning() && loc.wallets.list_empty_txs1_lightning) || loc.wallets.list_empty_txs1}
</Text>
{isLightning() && <Text style={styles.emptyTxsLightning}>{loc.wallets.list_empty_txs2_lightning}</Text>}
</ScrollView>
}
{...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}
/>
</View>
<FContainer ref={walletActionButtonsRef}>
{wallet?.allowReceive() && (
<FButton
testID="ReceiveButton"
text={loc.receive.header}
onPress={() => {
if (wallet.chain === Chain.OFFCHAIN) {
navigate('LNDCreateInvoiceRoot', { screen: 'LNDCreateInvoice', params: { walletID: wallet.getID() } });
} else {
navigate('ReceiveDetailsRoot', { screen: 'ReceiveDetails', params: { walletID: wallet.getID() } });
}
}}
icon={
<View style={styles.receiveIcon}>
<Icon name="arrow-down" size={buttonFontSize} type="font-awesome" color={colors.buttonAlternativeTextColor} />
</View>
}
/>
)}
{(wallet?.allowSend() || (wallet?.type === WatchOnlyWallet.type && wallet.isHd())) && (
<FButton
onLongPress={sendButtonLongPress}
onPress={sendButtonPress}
text={loc.send.header}
testID="SendButton"
icon={
<View style={styles.sendIcon}>
<Icon name="arrow-down" size={buttonFontSize} type="font-awesome" color={colors.buttonAlternativeTextColor} />
</View>
}
/>
)}
</FContainer>
</View>
);
};
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' }] },
});

View file

@ -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<NavigationProps>();
@ -252,18 +255,20 @@ const WalletsList: React.FC = () => {
const renderWalletsCarousel = useCallback(() => {
return (
<WalletsCarousel
data={wallets}
extraData={[wallets]}
onPress={handleClick}
handleLongPress={handleLongPress}
onMomentumScrollEnd={onSnapToItem}
ref={walletsCarousel}
onNewWalletPress={handleClick}
testID="WalletsList"
horizontal
scrollEnabled={isFocused}
/>
<>
<WalletsCarousel
data={wallets}
extraData={[wallets]}
onPress={handleClick}
handleLongPress={handleLongPress}
onMomentumScrollEnd={onSnapToItem}
ref={walletsCarousel}
onNewWalletPress={handleClick}
testID="WalletsList"
horizontal
scrollEnabled={isFocused}
/>
</>
);
}, [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 ? <TotalWalletsBalance /> : null;
}
default:
return null;
}
},
[renderListHeaderComponent],
[isLargeScreen, isTotalBalanceEnabled, renderListHeaderComponent],
);
const renderSectionFooter = useCallback(

View file

@ -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 () => {

View file

@ -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 && <ActivityIndicator style={styles.activityIndicator} />) || <View />;
};
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 (
<View style={styles.flex}>
<View style={styles.listHeaderTextRow}>
<Text style={[styles.listHeaderText, stylesHook.listHeaderText]}>{loc.transactions.list_title}</Text>
</View>
</View>
);
};
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 => (
<TransactionListItem item={item.item} itemPriceUnit={itemPriceUnit} timeElapsed={timeElapsed} walletID={walletID} />
);
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 (
<View style={styles.flex}>
<TransactionsNavigationHeader
navigation={navigation}
wallet={wallet}
onWalletUnitChange={passedWallet =>
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(),
},
});
});
}
}
}}
/>
<View style={[styles.list, stylesHook.list]}>
{wallet.type === WatchOnlyWallet.type && wallet.isWatchOnlyWarningVisible && (
<WatchOnlyWarning
disabled={isLoading}
handleDismiss={() => {
wallet.isWatchOnlyWarningVisible = false;
LayoutAnimation.configureNext(LayoutAnimation.Presets.linear);
saveToDisk();
}}
/>
)}
<FlatList
getItemLayout={getItemLayout}
updateCellsBatchingPeriod={30}
ListHeaderComponent={renderListHeaderComponent}
onEndReachedThreshold={0.3}
onEndReached={async () => {
// 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={
<ScrollView style={styles.flex} contentContainerStyle={styles.scrollViewContent}>
<Text numberOfLines={0} style={styles.emptyTxs}>
{(isLightning() && loc.wallets.list_empty_txs1_lightning) || loc.wallets.list_empty_txs1}
</Text>
{isLightning() && <Text style={styles.emptyTxsLightning}>{loc.wallets.list_empty_txs2_lightning}</Text>}
</ScrollView>
}
{...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}
/>
</View>
<FContainer ref={walletActionButtonsRef}>
{wallet.allowReceive() && (
<FButton
testID="ReceiveButton"
text={loc.receive.header}
onPress={() => {
if (wallet.chain === Chain.OFFCHAIN) {
navigate('LNDCreateInvoiceRoot', { screen: 'LNDCreateInvoice', params: { walletID: wallet.getID() } });
} else {
navigate('ReceiveDetailsRoot', { screen: 'ReceiveDetails', params: { walletID: wallet.getID() } });
}
}}
icon={
<View style={styles.receiveIcon}>
<Icon name="arrow-down" size={buttonFontSize} type="font-awesome" color={colors.buttonAlternativeTextColor} />
</View>
}
/>
)}
{(wallet.allowSend() || (wallet.type === WatchOnlyWallet.type && wallet.isHd())) && (
<FButton
onLongPress={sendButtonLongPress}
onPress={sendButtonPress}
text={loc.send.header}
testID="SendButton"
icon={
<View style={styles.sendIcon}>
<Icon name="arrow-down" size={buttonFontSize} type="font-awesome" color={colors.buttonAlternativeTextColor} />
</View>
}
/>
)}
</FContainer>
</View>
);
};
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' }],
},
});

View file

@ -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,
},
};