mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2025-02-22 23:08:07 +01:00
Merge branch 'master' into man
This commit is contained in:
commit
1dac884b35
27 changed files with 1047 additions and 784 deletions
|
@ -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"
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
139
components/TotalWalletsBalance.tsx
Normal file
139
components/TotalWalletsBalance.tsx
Normal 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;
|
|
@ -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()}
|
||||
|
|
|
@ -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()}
|
||||
|
|
|
@ -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
54
hooks/useKeyboard.ts
Normal 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;
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
17
loc/index.ts
17
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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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}
|
||||
|
|
46
navigation/helpers/getTransactionStatusOptions.tsx
Normal file
46
navigation/helpers/getTransactionStatusOptions.tsx
Normal 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;
|
|
@ -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
18
package-lock.json
generated
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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]}
|
||||
|
|
504
screen/wallets/WalletTransactions.tsx
Normal file
504
screen/wallets/WalletTransactions.tsx
Normal 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' }] },
|
||||
});
|
|
@ -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(
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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' }],
|
||||
},
|
||||
});
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue