feat: redesigned wallet details (#8301)

* feat: redesign transaction detail screen with unified layout and Lottie pending animation

* ADD: decode OP_RETURN payload as UTF-8 text in transaction detail

Co-authored-by: Cursor <cursoragent@cursor.com>

* REF: transaction detail redesign (themes, pending icon, loc)

Co-authored-by: Cursor <cursoragent@cursor.com>

* REF: remove deprecated TransactionDetails, TransactionStatus and getTransactionStatusOptions

Co-authored-by: Cursor <cursoragent@cursor.com>

* FIX: resolve lint errors (unused vars, styles, loc keys, no-bitwise, inline styles)

Co-authored-by: Cursor <cursoragent@cursor.com>

* FIX: remove redundant !tx check in transaction detail guard

Co-authored-by: Cursor <cursoragent@cursor.com>

* FIX: show transaction not available when tx not found after load

Co-authored-by: Cursor <cursoragent@cursor.com>

* FIX: remove unused transaction prop type from TransactionDetail

Co-authored-by: Cursor <cursoragent@cursor.com>

* TST: update UTXO note E2E to use new transaction detail note prompt UI

Co-authored-by: Cursor <cursoragent@cursor.com>

* simplify changes on the PR for review

* remove unused loc

* remove unchanged colors

* better offline support for tx details

* remove unused key loc

* fix code review issues

* fix balance

* fix tests

* REF: address PR #8289 review feedback

* redesigned wallets details

* fix lint

* fix lint

* fix bip84 test

* fix test

* fix tests

* fix tests

* fix: truncation and sendTo logic display

* fix: new arch fixes

* fix: lint

* fix: crash on status update

* fix: lint and tests

* fix: tests

* fix: tests

* fix: tests

* fix: tests

* fix: tests

* Potential fix for pull request finding 'Identical operands'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>

* fix: tests

* fix: tests

* fix: tests

* fix: tests

* fix: tests

* fix style

* fix merge master

* Merge branch 'wallet-details' of https://github.com/BlueWallet/BlueWallet into wallet-details

* fix loc

* fix loc

* fix style

* improve coin control from wallet details

* fix: e2e

* fix: WalletDetails

* fix: flat

* fix: e2e

* fix: e2e

* Potential fix for pull request finding 'Unused variable, import, function or class'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>

* fix: remove notifications dialogs

* fix: second button title

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Ivan Vershigora <ivan.vershigora@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Co-authored-by: Overtorment <overtorment@gmail.com>
This commit is contained in:
Nuno 2026-05-21 09:12:49 +02:00 committed by GitHub
parent c1c13e9e58
commit 9e907566f0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 710 additions and 319 deletions

View File

@ -197,12 +197,13 @@ export class WatchOnlyWallet extends LegacyWallet {
async fetchUtxo() {
if (this._hdWalletInstance) return this._hdWalletInstance.fetchUtxo();
throw new Error('Not initialized');
// Single-address watch-only uses LegacyWallet UTXO + derivation from txs (no HD instance).
return super.fetchUtxo();
}
getUtxo(...args: Parameters<THDWalletForWatchOnly['getUtxo']>) {
if (this._hdWalletInstance) return this._hdWalletInstance.getUtxo(...args);
throw new Error('Not initialized');
return super.getUtxo(...args);
}
combinePsbt(...args: Parameters<THDWalletForWatchOnly['combinePsbt']>) {

View File

@ -11,10 +11,12 @@ interface ListItemProps {
noFeedback?: boolean;
bottomDivider?: boolean;
testID?: string;
switchTestID?: string;
onPress?: () => void;
disabled?: boolean;
switch?: SwitchProps;
title: string;
titleStyle?: StyleProp<TextStyle>;
subtitle?: string | React.ReactNode;
subtitleNumberOfLines?: number;
rightTitle?: string;
@ -33,10 +35,12 @@ const ListItem: React.FC<ListItemProps> = React.memo(
noFeedback = false,
bottomDivider = true,
testID,
switchTestID,
onPress,
disabled,
switch: switchProps,
title,
titleStyle,
subtitle,
subtitleNumberOfLines,
rightTitle,
@ -83,6 +87,7 @@ const ListItem: React.FC<ListItemProps> = React.memo(
const memoizedSwitchProps = useMemo(() => {
return switchProps ? { ...switchProps } : undefined;
}, [switchProps]);
const resolvedSwitchTestID = switchTestID ?? memoizedSwitchProps?.testID;
const enableFeedback = !noFeedback && !!onPress && !disabled;
const renderContent = () => (
@ -94,7 +99,7 @@ const ListItem: React.FC<ListItemProps> = React.memo(
</View>
)}
<View style={styles.content}>
<Text style={stylesHook.title} numberOfLines={0} accessibilityRole="text">
<Text style={[stylesHook.title, titleStyle]} numberOfLines={0} accessibilityRole="text">
{title}
</Text>
{subtitle ? (
@ -124,7 +129,14 @@ const ListItem: React.FC<ListItemProps> = React.memo(
<Icon name={isRtl ? 'angle-left' : 'angle-right'} type="font-awesome" color={colors.alternativeTextColor} size={18} />
) : null}
{switchProps ? (
<Switch {...memoizedSwitchProps} accessibilityLabel={title} style={styles.margin16} accessible accessibilityRole="switch" />
<Switch
{...memoizedSwitchProps}
testID={resolvedSwitchTestID}
accessibilityLabel={title}
style={styles.margin16}
accessible
accessibilityRole="switch"
/>
) : null}
{checkmark ? (
<View style={styles.checkmarkContainer}>

View File

@ -11,6 +11,7 @@ type SecondButtonProps = {
disabled?: boolean;
icon?: IconButtonProps;
title: string;
textColor?: string;
onPress?: () => void;
loading?: boolean;
testID?: string;
@ -19,7 +20,7 @@ type SecondButtonProps = {
export const SecondButton = forwardRef<React.ElementRef<typeof TouchableOpacity>, SecondButtonProps>((props, ref) => {
const { colors } = useTheme();
let backgroundColor = props.backgroundColor ? props.backgroundColor : colors.buttonGrayBackgroundColor;
let fontColor = colors.secondButtonTextColor;
let fontColor = props.textColor ?? colors.secondButtonTextColor;
if (props.disabled === true) {
backgroundColor = colors.buttonDisabledBackgroundColor;
fontColor = colors.buttonDisabledTextColor;

View File

@ -66,10 +66,13 @@ const getHandleCloseAction = (
const navigationStyle = (
{
closeButtonPosition,
closeButtonIfFirstInStack,
onCloseButtonPressed,
...opts
}: NativeStackNavigationOptions & {
closeButtonPosition?: CloseButtonPosition;
/** When set, show this close control only if this screen is the first route in the stack (e.g. Coin Control opened from wallet details). */
closeButtonIfFirstInStack?: CloseButtonPosition;
onCloseButtonPressed?: (deps: { navigation: any; route: any }) => void;
},
formatter?: OptionsFormatter,
@ -80,7 +83,10 @@ const navigationStyle = (
const isModal = route.params?.presentation === 'modal' || route.params?.presentation === 'transparentModal';
const isFormSheet = route.params?.presentation === 'formSheet';
const closeButton = getCloseButtonPosition(closeButtonPosition, isFirstRouteInStack, isModal);
const closeButton =
closeButtonIfFirstInStack && isFirstRouteInStack
? closeButtonIfFirstInStack
: getCloseButtonPosition(closeButtonPosition, isFirstRouteInStack, isModal);
const handleClose = getHandleCloseAction(onCloseButtonPressed, navigation, route);
let headerRight;

View File

@ -376,7 +376,6 @@
"rbf_title": "Speed Up (RBF)",
"status_bump": "Speed Up",
"status_cancel": "Cancel",
"transactions_count": "Transactions Count",
"txid": "Transaction ID",
"updating": "Updating...",
"watchOnlyWarningTitle": "Security warning",
@ -438,12 +437,15 @@
"details_delete_wallet": "Delete Wallet",
"details_derivation_path": "derivation path",
"details_display": "Display in Home Screen",
"details_edit": "edit",
"details_export_backup": "Export/Backup",
"details_export_history": "Export History to CSV",
"details_master_fingerprint": "Master Fingerprint",
"details_multisig_type": "multisig",
"details_options": "Options",
"details_show_xpub": "Show Wallet XPUB",
"details_show_addresses": "Show addresses",
"details_stats_coins": "Coins",
"details_title": "Wallet",
"wallets": "Wallets",
"swipe_balance_hide": "Hide",

View File

@ -272,7 +272,11 @@ const DetailViewStackScreensStack = () => {
name="WalletDetails"
component={WalletDetails}
options={navigationStyle({
headerTitle: loc.wallets.details_title,
headerTitle: '',
statusBarStyle: 'auto',
headerStyle: {
backgroundColor: theme.colors.background,
},
})(theme)}
/>
<DetailViewStack.Screen

View File

@ -115,7 +115,14 @@ const SendDetailsStack = () => {
closeButtonPosition: CloseButtonPosition.Right,
})(theme)}
/>
<Stack.Screen name="CoinControl" component={CoinControlComponent} options={navigationStyle({ title: loc.cc.header })(theme)} />
<Stack.Screen
name="CoinControl"
component={CoinControlComponent}
options={navigationStyle({
title: loc.cc.header,
closeButtonIfFirstInStack: CloseButtonPosition.Left,
})(theme)}
/>
<Stack.Screen
name="PaymentCodeList"
component={PaymentCodesListComponent}

View File

@ -0,0 +1,39 @@
import { CommonActions, StackActions } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { Utxo } from '../class/wallets/types';
import { BitcoinUnit } from '../models/bitcoinUnits';
import { SendDetailsStackParamList } from './SendDetailsStackParamList';
/**
* After choosing UTXO(s) in Coin Control, open Send with those coins.
* Uses popTo when SendDetails is already in the stack (normal send coin control).
* Resets the send stack when Coin Control was opened as the first screen (e.g. from wallet details).
*/
export function goFromCoinControlToSendDetails(
navigation: NativeStackNavigationProp<SendDetailsStackParamList>,
walletID: string,
utxos: Utxo[],
): void {
const state = navigation.getState();
const hasSendDetails = state.routes.some(r => r.name === 'SendDetails');
const params = {
walletID,
utxos,
isEditable: true as const,
feeUnit: BitcoinUnit.BTC,
amountUnit: BitcoinUnit.BTC,
};
if (hasSendDetails) {
navigation.dispatch(StackActions.popTo('SendDetails', params, { merge: true }));
} else {
navigation.dispatch(
CommonActions.reset({
index: 0,
routes: [{ name: 'SendDetails', params }],
}),
);
}
}

View File

@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { RouteProp, StackActions, useFocusEffect, useRoute } from '@react-navigation/native';
import { RouteProp, useFocusEffect, useRoute } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import Avatar from '../../components/Avatar';
import Badge from '../../components/Badge';
@ -17,6 +17,7 @@ import { useStorage } from '../../hooks/context/useStorage';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import loc, { formatBalance } from '../../loc';
import { BitcoinUnit } from '../../models/bitcoinUnits';
import { goFromCoinControlToSendDetails } from '../../navigation/goFromCoinControlToSendDetails';
import { SendDetailsStackParamList } from '../../navigation/SendDetailsStackParamList';
import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
@ -260,9 +261,8 @@ const CoinControl: React.FC = () => {
const handleChoose = (item: Utxo) => navigation.navigate('CoinControlOutput', { walletID, utxo: item });
const handleUseCoin = async (u: Utxo[]) => {
const popToAction = StackActions.popTo('SendDetails', { walletID, utxos: u }, { merge: true });
navigation.dispatch(popToAction);
const handleUseCoin = (u: Utxo[]) => {
goFromCoinControlToSendDetails(navigation, walletID, u);
};
const handleMassFreeze = () => {

View File

@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { RouteProp, StackActions, useRoute } from '@react-navigation/native';
import { RouteProp, useRoute } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { StyleSheet, Text, TextInput, View } from 'react-native';
import debounce from '../../blue_modules/debounce';
@ -10,6 +10,7 @@ import Button from '../../components/Button';
import { useTheme } from '../../components/themes';
import loc, { formatBalance } from '../../loc';
import { BitcoinUnit } from '../../models/bitcoinUnits';
import { goFromCoinControlToSendDetails } from '../../navigation/goFromCoinControlToSendDetails';
import { SendDetailsStackParamList } from '../../navigation/SendDetailsStackParamList';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import { useStorage } from '../../hooks/context/useStorage';
@ -81,8 +82,7 @@ const CoinControlOutputSheet: React.FC = () => {
debouncedSaveMemo.current.cancel();
wallet.setUTXOMetadata(utxo.txid, utxo.vout, { memo });
await saveToDisk();
const popToAction = StackActions.popTo('SendDetails', { walletID, utxos: [utxo] }, { merge: true });
navigation.dispatch(popToAction);
goFromCoinControlToSendDetails(navigation, walletID, [utxo]);
}, [memo, navigation, saveToDisk, utxo, wallet, walletID]);
const applyChangesAndClose = useCallback(async () => {

View File

@ -93,6 +93,8 @@ const SendDetails = () => {
const routeParams = route.params;
const scrollView = useRef<FlatList<any>>(null);
const scrollIndex = useRef(0);
/** Used so we only clear coin-selection (utxos) when the user switches wallet, not on first mount (e.g. Send opened from wallet details with pre-selected UTXOs). */
const prevWalletIdForCoinResetRef = useRef<string | null>(null);
const { colors } = useTheme();
// state
@ -274,17 +276,20 @@ const SendDetails = () => {
useEffect(() => {
if (!wallet) return;
// reset other values
setChangeAddress(null);
const prevId = prevWalletIdForCoinResetRef.current;
const currentId = wallet.getID();
const walletActuallyChanged = prevId !== null && prevId !== currentId;
setParams({
utxos: null,
...(walletActuallyChanged ? { utxos: null } : {}),
isTransactionReplaceable: wallet.type === HDSegwitBech32Wallet.type && !routeParams.isTransactionReplaceable ? true : undefined,
});
// update wallet UTXO
prevWalletIdForCoinResetRef.current = currentId;
wallet
.fetchUtxo()
.then(() => {
// we need to re-calculate fees
setDumb(v => !v);
})
.catch(e => console.log('fetchUtxo error', e));

View File

@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { ActivityIndicator, StyleSheet, Switch, Text, TextInput, TouchableOpacity, View } from 'react-native';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { writeFileAndExport } from '../../blue_modules/fs';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import { uint8ArrayToHex } from '../../blue_modules/uint8array-extras';
@ -16,7 +16,7 @@ import { WatchOnlyWallet } from '../../class/wallets/watch-only-wallet';
import { AbstractHDElectrumWallet } from '../../class/wallets/abstract-hd-electrum-wallet';
import { LightningCustodianWallet } from '../../class/wallets/lightning-custodian-wallet';
import presentAlert from '../../components/Alert';
import Button from '../../components/Button';
import CopyTextToClipboard from '../../components/CopyTextToClipboard';
import ListItem from '../../components/ListItem';
import { SecondButton } from '../../components/SecondButton';
import { useTheme } from '../../components/themes';
@ -29,16 +29,27 @@ import { useStorage } from '../../hooks/context/useStorage';
import { useFocusEffect, useRoute, RouteProp, usePreventRemove, useLocale } from '@react-navigation/native';
import { LightningTransaction, Transaction, TWallet } from '../../class/wallets/types';
import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList';
import HeaderMenuButton from '../../components/HeaderMenuButton';
import ToolTipMenu from '../../components/TooltipMenu';
import { Action } from '../../components/types';
import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
import SafeAreaScrollView from '../../components/SafeAreaScrollView';
import { BlueSpacing10, BlueSpacing20 } from '../../components/BlueSpacing';
import { BlueSpacing20 } from '../../components/BlueSpacing';
import { BlueLoading } from '../../components/BlueLoading';
import Icon from '../../components/Icon';
type RouteProps = RouteProp<DetailViewStackParamList, 'WalletDetails'>;
function getCoinControlStats(w: TWallet): { hasCoinControl: boolean; utxoCount: number | null } {
if (typeof w.getUtxo !== 'function') return { hasCoinControl: false, utxoCount: null };
try {
return { hasCoinControl: true, utxoCount: w.getUtxo().length };
} catch {
return { hasCoinControl: false, utxoCount: null };
}
}
const WalletDetails: React.FC = () => {
const { saveToDisk, wallets, txMetadata, handleWalletDeletion } = useStorage();
const { saveToDisk, wallets, txMetadata, handleWalletDeletion, sleep } = useStorage();
const { isBiometricUseCapableAndEnabled } = useBiometrics();
const { walletID } = useRoute<RouteProps>().params;
const { direction } = useLocale();
@ -60,11 +71,42 @@ const WalletDetails: React.FC = () => {
);
const { setOptions, navigate, navigateToWalletsList } = useExtendedNavigation();
const { colors } = useTheme();
const [walletName, setWalletName] = useState<string>(wallet.getLabel());
const [masterFingerprint, setMasterFingerprint] = useState<string | undefined>();
const [arkAddress, setArkAddress] = useState<string>('');
const walletTransactionsLength = useMemo<number>(() => wallet.getTransactions().length, [wallet]);
const [coinControlStats, setCoinControlStats] = useState(() => getCoinControlStats(wallet));
useEffect(() => {
const w = walletRef.current;
if (w) setCoinControlStats(getCoinControlStats(w));
}, [walletID]);
useFocusEffect(
useCallback(() => {
let cancelled = false;
const w = walletRef.current;
if (!w || typeof w.getUtxo !== 'function') return;
const refresh = async () => {
if (typeof w.fetchUtxo === 'function') {
try {
await Promise.race([w.fetchUtxo(), sleep(12000)]);
} catch {
// Same pattern as CoinControl: timeout or network errors; still re-read getUtxo() below.
}
}
if (!cancelled) setCoinControlStats(getCoinControlStats(w));
};
refresh().catch(() => {});
return () => {
cancelled = true;
};
}, [sleep]),
);
const { hasCoinControl, utxoCount } = coinControlStats;
const derivationPath = useMemo<string | null>(() => {
try {
// @ts-expect-error: Need to fix later
@ -79,6 +121,7 @@ const WalletDetails: React.FC = () => {
}
}, [wallet]);
const [isMasterFingerPrintVisible, setIsMasterFingerPrintVisible] = useState<boolean>(false);
const [isAdvancedExpanded, setIsAdvancedExpanded] = useState<boolean>(false);
// Fetch ark address when wallet is a LightningArkWallet
useEffect(() => {
@ -86,7 +129,6 @@ const WalletDetails: React.FC = () => {
if (wallet.type === LightningArkWallet.type && wallet.getArkAddress) {
try {
const address = await wallet.getArkAddress();
console.log('ark address:', address);
setArkAddress(address);
} catch (error: any) {
setArkAddress(error.message);
@ -215,19 +257,17 @@ const WalletDetails: React.FC = () => {
const toolTipOnPressMenuItem = useCallback(
async (id: string) => {
if (id === CommonToolTipActions.Delete.id) {
handleDeleteButtonTapped();
} else if (id === CommonToolTipActions.Share.id) {
if (id === CommonToolTipActions.Share.id) {
await writeFileAndExport(fileName, exportHistoryContent(), true);
} else if (id === CommonToolTipActions.SaveFile.id) {
await writeFileAndExport(fileName, exportHistoryContent(), false);
}
},
[exportHistoryContent, fileName, handleDeleteButtonTapped],
[exportHistoryContent, fileName],
);
const toolTipActions = useMemo(() => {
const actions: Action[] = [
const transactionsBoxMenuActions = useMemo(
(): Action[] => [
{
id: loc.wallets.details_export_history,
text: loc.wallets.details_export_history,
@ -235,22 +275,15 @@ const WalletDetails: React.FC = () => {
hidden: walletTransactionsLength === 0,
subactions: [CommonToolTipActions.Share, CommonToolTipActions.SaveFile],
},
CommonToolTipActions.Delete,
];
return actions;
}, [walletTransactionsLength]);
const HeaderRight = useMemo(
() => <HeaderMenuButton disabled={isLoading} onPressMenuItem={toolTipOnPressMenuItem} actions={toolTipActions} />,
[toolTipOnPressMenuItem, toolTipActions, isLoading],
],
[walletTransactionsLength],
);
useEffect(() => {
setOptions({
headerRight: () => HeaderRight,
headerRight: undefined,
});
}, [HeaderRight, setOptions]);
}, [setOptions]);
useEffect(() => {
setIsContactsVisible(wallet.allowBIP47 && wallet.allowBIP47() && isBIP47Enabled);
@ -277,20 +310,72 @@ const WalletDetails: React.FC = () => {
const stylesHook = StyleSheet.create({
textLabel1: {
color: colors.feeText,
color: colors.alternativeTextColor,
writingDirection: direction,
},
textLabel2: {
color: colors.feeText,
color: colors.alternativeTextColor,
writingDirection: direction,
},
textValue: {
color: colors.outputValue,
},
input: {
borderColor: colors.formBorder,
borderBottomColor: colors.formBorder,
backgroundColor: colors.inputBackgroundColor,
walletNameText: {
color: colors.outputValue,
writingDirection: direction,
},
nameRow: {
flexDirection: direction === 'rtl' ? 'row-reverse' : 'row',
alignItems: 'center',
},
editButton: {
backgroundColor: colors.lightButton,
marginLeft: direction === 'rtl' ? 0 : 12,
marginRight: direction === 'rtl' ? 12 : 0,
},
editButtonText: {
color: colors.buttonTextColor,
},
optionsSectionHeader: {
borderColor: colors.cardBorderColor,
},
detailsCard: {
borderColor: colors.cardBorderColor,
},
sectionTitle: {
backgroundColor: colors.cardSectionHeaderBackground,
},
sectionTitleText: {
color: colors.foregroundColor,
},
optionsContent: {
backgroundColor: colors.cardSectionBackground,
borderBottomLeftRadius: 12,
borderBottomRightRadius: 12,
overflow: 'hidden',
},
advancedContent: {
backgroundColor: colors.cardSectionBackground,
borderBottomLeftRadius: 12,
borderBottomRightRadius: 12,
overflow: 'hidden',
},
advancedListItemTitle: {
color: colors.feeText,
writingDirection: direction,
},
advancedListItemRightTitle: {
color: colors.foregroundColor,
},
statsBox: {
backgroundColor: colors.cardSectionHeaderBackground,
},
statsBoxNumber: {
color: colors.foregroundColor,
},
listItemContainerBorder: {
backgroundColor: 'transparent',
borderBottomColor: colors.cardBorderColor,
},
});
@ -399,26 +484,20 @@ const WalletDetails: React.FC = () => {
}
};
const walletNameTextInputOnBlur = useCallback(async () => {
const trimmedWalletName = walletName.trim();
if (trimmedWalletName.length === 0) {
const walletLabel = wallet.getLabel();
setWalletName(walletLabel);
} else if (wallet.getLabel() !== trimmedWalletName) {
// Only save if the name has changed
wallet.setLabel(trimmedWalletName);
try {
console.warn('saving wallet name:', trimmedWalletName);
await saveToDisk();
} catch (error) {
console.error((error as Error).message);
}
const handleEditWalletName = useCallback(async () => {
try {
const newName = await prompt(loc.wallets.add_wallet_name, '', true, 'plain-text', false, undefined, wallet.getLabel());
const trimmed = newName.trim();
if (trimmed.length === 0) return;
if (wallet.getLabel() === trimmed) return;
wallet.setLabel(trimmed);
await saveToDisk();
} catch (_) {
// User cancelled
}
}, [wallet, walletName, saveToDisk]);
}, [wallet, saveToDisk]);
usePreventRemove(false, () => {
walletNameTextInputOnBlur();
});
usePreventRemove(false, () => {});
const onViewMasterFingerPrintPress = () => {
setIsMasterFingerPrintVisible(true);
@ -432,79 +511,27 @@ const WalletDetails: React.FC = () => {
) : (
<>
<BlueCard style={styles.address}>
{(() => {
if (
[LegacyWallet.type, SegwitBech32Wallet.type, SegwitP2SHWallet.type].includes(wallet.type) ||
(wallet.type === WatchOnlyWallet.type && !wallet.isHd())
) {
return (
<>
<Text style={[styles.textLabel1, stylesHook.textLabel1]}>{loc.wallets.details_address.toLowerCase()}</Text>
<Text style={[styles.textValue, stylesHook.textValue]} selectable>
{(() => {
// gracefully handling faulty wallets, so at least user has an option to delete the wallet
try {
return wallet.getAddress ? wallet.getAddress() : '';
} catch (error: any) {
return error.message;
}
})()}
</Text>
</>
);
}
})()}
<Text style={[styles.textLabel2, stylesHook.textLabel2]}>{loc.wallets.add_wallet_name.toLowerCase()}</Text>
<View style={[styles.input, stylesHook.input]}>
<TextInput
value={walletName}
onChangeText={(text: string) => {
setWalletName(text);
}}
onChange={event => {
const text = event.nativeEvent.text;
setWalletName(text);
}}
onBlur={walletNameTextInputOnBlur}
<Text style={[styles.textLabel2, stylesHook.textLabel2]}>{loc.wallets.add_wallet_name}</Text>
<View style={[styles.nameRow, stylesHook.nameRow]}>
<Text
style={[styles.nameValue, stylesHook.walletNameText]}
numberOfLines={1}
placeholderTextColor="#81868e"
style={[styles.inputText, { writingDirection: direction }]}
editable={!isLoading}
underlineColorAndroid="transparent"
testID="WalletNameInput"
/>
ellipsizeMode="tail"
testID="WalletNameDisplay"
>
{wallet.getLabel()}
</Text>
<TouchableOpacity
style={[styles.editButton, stylesHook.editButton]}
onPress={handleEditWalletName}
disabled={isLoading}
accessibilityRole="button"
testID="WalletNameEditButton"
activeOpacity={0.7}
>
<BlueText style={[styles.editButtonText, stylesHook.editButtonText]}>{loc.wallets.details_edit}</BlueText>
</TouchableOpacity>
</View>
<BlueSpacing20 />
<Text style={[styles.textLabel1, stylesHook.textLabel1]}>{loc.wallets.details_type.toLowerCase()}</Text>
<Text style={[styles.textValue, stylesHook.textValue]} selectable>
{wallet.typeReadable}
</Text>
{wallet.type === LightningArkWallet.type && (
<>
<Text style={[styles.textLabel1, stylesHook.textLabel1]}>Ark {loc.wallets.details_address.toLowerCase()}</Text>
<Text style={[styles.textValue, stylesHook.textValue]} selectable>
{arkAddress}
</Text>
</>
)}
{wallet.type === MultisigHDWallet.type && (
<>
<Text style={[styles.textLabel2, stylesHook.textLabel2]}>{loc.wallets.details_multisig_type}</Text>
<BlueText>
{`${wallet.getM()} / ${wallet.getN()} (${
wallet.isNativeSegwit() ? 'native segwit' : wallet.isWrappedSegwit() ? 'wrapped segwit' : 'legacy'
})`}
</BlueText>
</>
)}
{wallet.type === MultisigHDWallet.type && (
<>
<Text style={[styles.textLabel2, stylesHook.textLabel2]}>{loc.multisig.how_many_signatures_can_bluewallet_make}</Text>
<BlueText>{wallet.howManySignaturesCanWeMake()}</BlueText>
</>
)}
{wallet.type === LightningCustodianWallet.type && (
<>
<Text style={[styles.textLabel1, stylesHook.textLabel1]}>{loc.wallets.details_connected_to.toLowerCase()}</Text>
@ -518,74 +545,143 @@ const WalletDetails: React.FC = () => {
<BlueText>{wallet.getIdentityPubkey()}</BlueText>
</>
)}
<BlueSpacing20 />
<>
<Text onPress={exportInternals} style={[styles.textLabel2, stylesHook.textLabel2]}>
{loc.transactions.list_title.toLowerCase()}
</Text>
<View style={styles.hardware}>
<BlueText>{loc.wallets.details_display}</BlueText>
<Switch
value={hideTransactionsInWalletsList}
onValueChange={async (value: boolean) => {
if (wallet.setHideTransactionsInWalletsList) {
wallet.setHideTransactionsInWalletsList(!value);
triggerHapticFeedback(HapticFeedbackTypes.ImpactLight);
setHideTransactionsInWalletsList(!wallet.getHideTransactionsInWalletsList());
}
</BlueCard>
{/* Address (watch-address wallets only) */}
{([LegacyWallet.type, SegwitBech32Wallet.type, SegwitP2SHWallet.type].includes(wallet.type) ||
(wallet.type === WatchOnlyWallet.type && !wallet.isHd())) && (
<View style={[styles.detailsCard, stylesHook.detailsCard]}>
<View style={stylesHook.optionsContent}>
<Text style={[styles.textLabel2, stylesHook.textLabel2, styles.optionsSubheader]}>{loc.wallets.details_address}</Text>
<Text style={[styles.textValue, stylesHook.textValue, styles.addressSectionContent]} selectable>
{(() => {
try {
await saveToDisk();
} catch (error: any) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
console.error(error.message);
return wallet.getAddress ? wallet.getAddress() : '';
} catch (error: unknown) {
return (error as Error).message;
}
}}
})()}
</Text>
</View>
</View>
)}
{/* Stats */}
<View style={[styles.detailsCard, stylesHook.detailsCard]}>
<View style={styles.statsRow}>
<View style={[styles.statsBox, stylesHook.statsBox]}>
<View style={styles.statsBoxTitleRow}>
<Text onPress={purgeTransactions} style={[styles.textLabel2, stylesHook.textLabel2]} testID="PurgeBackdoorButton">
{loc.transactions.list_title}
</Text>
{walletTransactionsLength > 0 && (
<ToolTipMenu
isButton
shouldOpenOnLongPress={false}
onPressMenuItem={toolTipOnPressMenuItem}
actions={transactionsBoxMenuActions}
>
<Icon name="more-horiz" type="material" size={20} color={colors.alternativeTextColor} />
</ToolTipMenu>
)}
</View>
<BlueText style={[styles.statsBoxNumber, stylesHook.statsBoxNumber]}>{wallet.getTransactions().length}</BlueText>
</View>
{hasCoinControl && utxoCount !== null && utxoCount > 0 ? (
<TouchableOpacity
style={[styles.statsBox, stylesHook.statsBox]}
onPress={() => navigate('SendDetailsRoot', { screen: 'CoinControl', params: { walletID } })}
activeOpacity={0.8}
testID="CoinsStatsBox"
>
<View style={styles.statsBoxTitleRow}>
<Text style={[styles.textLabel2, stylesHook.textLabel2]}>{loc.wallets.details_stats_coins}</Text>
<View style={styles.statsBoxTitleRowSpacer} />
</View>
<BlueText style={[styles.statsBoxNumber, stylesHook.statsBoxNumber]}>{utxoCount}</BlueText>
</TouchableOpacity>
) : (
<View style={[styles.statsBox, stylesHook.statsBox]} testID="CoinsStatsBox">
<View style={styles.statsBoxTitleRow}>
<Text style={[styles.textLabel2, stylesHook.textLabel2]}>{loc.wallets.details_stats_coins}</Text>
<View style={styles.statsBoxTitleRowSpacer} />
</View>
<BlueText style={[styles.statsBoxNumber, stylesHook.statsBoxNumber]}>
{hasCoinControl && utxoCount !== null ? utxoCount : '—'}
</BlueText>
</View>
)}
</View>
</View>
{/* Ark Address (Ark wallets only) */}
{wallet.type === LightningArkWallet.type && (
<View style={[styles.detailsCard, stylesHook.detailsCard]}>
<View style={stylesHook.optionsContent}>
<Text style={[styles.textLabel2, stylesHook.textLabel2, styles.optionsSubheader]}>
{`Ark ${loc.wallets.details_address}`}
</Text>
<CopyTextToClipboard
text={arkAddress}
style={[styles.textValue, stylesHook.textValue, styles.addressSectionContent]}
selectable
/>
</View>
</>
<>
<Text onPress={purgeTransactions} style={[styles.textLabel2, stylesHook.textLabel2]} testID="PurgeBackdoorButton">
{loc.transactions.transactions_count.toLowerCase()}
</Text>
<BlueText>{wallet.getTransactions().length}</BlueText>
</>
</View>
)}
{wallet.allowBIP47 && wallet.allowBIP47() ? (
<>
<Text style={[styles.textLabel2, stylesHook.textLabel2]}>{loc.bip47.payment_code}</Text>
<View style={styles.hardware}>
<BlueText>{loc.bip47.purpose}</BlueText>
<Switch
value={isBIP47Enabled}
onValueChange={async (value: boolean) => {
setIsBIP47Enabled(value);
if (wallet.switchBIP47) {
wallet.switchBIP47(value);
triggerHapticFeedback(HapticFeedbackTypes.ImpactLight);
}
try {
await saveToDisk();
} catch (error: unknown) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
console.error((error as Error).message);
}
}}
testID="BIP47Switch"
{/* Show addresses & Contacts */}
{(wallet instanceof AbstractHDElectrumWallet ||
(wallet.type === WatchOnlyWallet.type && wallet.isHd && wallet.isHd()) ||
isContactsVisible) && (
<View style={[styles.detailsCard, stylesHook.detailsCard]}>
<View style={stylesHook.optionsContent}>
{(wallet instanceof AbstractHDElectrumWallet ||
(wallet.type === WatchOnlyWallet.type && wallet.isHd && wallet.isHd())) && (
<ListItem
containerStyle={stylesHook.listItemContainerBorder}
onPress={navigateToAddresses}
title={loc.wallets.details_show_addresses}
chevron
bottomDivider={!!isContactsVisible}
/>
</View>
</>
) : null}
)}
{isContactsVisible ? (
<ListItem
containerStyle={stylesHook.listItemContainerBorder}
onPress={navigateToContacts}
title={loc.bip47.contacts}
chevron
bottomDivider={false}
/>
) : null}
</View>
</View>
)}
<View>
{/* Options container — header full width (single row so section background spans the card) */}
<View style={[styles.detailsCard, stylesHook.detailsCard]}>
<View
style={[
styles.sectionTitle,
stylesHook.sectionTitle,
styles.sectionTitleRowContainer,
styles.optionsSectionHeader,
stylesHook.optionsSectionHeader,
]}
>
<BlueText style={[styles.sectionTitleText, stylesHook.sectionTitleText]}>{loc.wallets.details_options}</BlueText>
</View>
<View style={stylesHook.optionsContent}>
{wallet.type === WatchOnlyWallet.type && wallet.isHd && wallet.isHd() && (
<>
<BlueSpacing10 />
<Text style={[styles.textLabel2, stylesHook.textLabel2]}>{loc.wallets.details_advanced.toLowerCase()}</Text>
<View style={styles.hardware}>
<BlueText>{loc.wallets.details_use_with_hardware_wallet}</BlueText>
<Switch
value={walletUseWithHardwareWallet}
onValueChange={async (value: boolean) => {
<Text style={[styles.textLabel2, stylesHook.textLabel2, styles.optionsSubheader]}>{loc.wallets.details_advanced}</Text>
<ListItem
containerStyle={styles.listItemContainerBorderLight}
title={loc.wallets.details_use_with_hardware_wallet}
switch={{
value: walletUseWithHardwareWallet,
onValueChange: async (value: boolean) => {
setWalletUseWithHardwareWallet(value);
if (wallet.setUseWithHardwareWalletEnabled) {
wallet.setUseWithHardwareWalletEnabled(value);
@ -597,80 +693,204 @@ const WalletDetails: React.FC = () => {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
console.error((error as Error).message);
}
}}
/>
</View>
</>
)}
<View style={styles.row}>
{wallet.allowMasterFingerprint && wallet.allowMasterFingerprint() && (
<View style={styles.marginRight16}>
<Text style={[styles.textLabel2, stylesHook.textLabel2]}>{loc.wallets.details_master_fingerprint.toLowerCase()}</Text>
{isMasterFingerPrintVisible ? (
<BlueText selectable>{masterFingerprint ?? <ActivityIndicator />}</BlueText>
) : (
<TouchableOpacity onPress={onViewMasterFingerPrintPress}>
<BlueText>{loc.multisig.view}</BlueText>
</TouchableOpacity>
)}
</View>
)}
{derivationPath && (
<View>
<Text style={[styles.textLabel2, stylesHook.textLabel2]}>{loc.wallets.details_derivation_path}</Text>
<BlueText selectable testID="DerivationPath">
{derivationPath}
</BlueText>
</View>
)}
</View>
</View>
</BlueCard>
{(wallet instanceof AbstractHDElectrumWallet || (wallet.type === WatchOnlyWallet.type && wallet.isHd && wallet.isHd())) && (
<ListItem onPress={navigateToAddresses} title={loc.wallets.details_show_addresses} chevron />
)}
{isContactsVisible ? <ListItem onPress={navigateToContacts} title={loc.bip47.contacts} chevron /> : null}
<BlueCard style={styles.address}>
<View>
<BlueSpacing20 />
<Button onPress={navigateToWalletExport} testID="WalletExport" title={loc.wallets.details_export_backup} />
{wallet.type === MultisigHDWallet.type && (
<>
<BlueSpacing20 />
<SecondButton
onPress={navigateToMultisigCoordinationSetup}
testID="MultisigCoordinationSetup"
title={loc.multisig.export_coordination_setup.replace(/^\w/, (c: string) => c.toUpperCase())}
},
}}
bottomDivider
/>
</>
)}
{wallet.type === MultisigHDWallet.type && (
<Text onPress={exportInternals} style={[styles.textLabel2, stylesHook.textLabel2, styles.optionsSubheader]}>
{loc.transactions.list_title}
</Text>
<ListItem
containerStyle={stylesHook.listItemContainerBorder}
title={loc.wallets.details_display}
switch={{
value: hideTransactionsInWalletsList,
onValueChange: async (value: boolean) => {
if (wallet.setHideTransactionsInWalletsList) {
wallet.setHideTransactionsInWalletsList(!value);
triggerHapticFeedback(HapticFeedbackTypes.ImpactLight);
setHideTransactionsInWalletsList(!wallet.getHideTransactionsInWalletsList());
}
try {
await saveToDisk();
} catch (error: any) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
console.error(error.message);
}
},
}}
bottomDivider
/>
{wallet.allowBIP47 && wallet.allowBIP47() && (
<>
<BlueSpacing20 />
<SecondButton
onPress={navigateToViewEditCosigners}
testID="ViewEditCosigners"
title={loc.multisig.view_edit_cosigners}
<Text style={[styles.textLabel2, stylesHook.textLabel2, styles.optionsSubheader]}>{loc.bip47.payment_code}</Text>
<ListItem
containerStyle={stylesHook.listItemContainerBorder}
title={loc.bip47.purpose}
switch={{
value: isBIP47Enabled,
onValueChange: async (value: boolean) => {
setIsBIP47Enabled(value);
if (wallet.switchBIP47) {
wallet.switchBIP47(value);
triggerHapticFeedback(HapticFeedbackTypes.ImpactLight);
}
try {
await saveToDisk();
} catch (error: unknown) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
console.error((error as Error).message);
}
},
}}
switchTestID="BIP47Switch"
bottomDivider
/>
</>
)}
{wallet.allowXpub && wallet.allowXpub() && (
<>
<BlueSpacing20 />
<SecondButton onPress={navigateToXPub} testID="XpubButton" title={loc.wallets.details_show_xpub} />
</>
<ListItem
containerStyle={stylesHook.listItemContainerBorder}
onPress={navigateToXPub}
title={loc.wallets.details_show_xpub}
chevron
testID="XpubButton"
bottomDivider
/>
)}
{wallet.allowSignVerifyMessage && wallet.allowSignVerifyMessage() && (
<ListItem
containerStyle={stylesHook.listItemContainerBorder}
onPress={navigateToSignVerify}
title={loc.addresses.sign_title}
chevron
testID="SignVerify"
bottomDivider={!!(wallet.type === MultisigHDWallet.type)}
/>
)}
{wallet.type === MultisigHDWallet.type && (
<>
<BlueSpacing20 />
<SecondButton onPress={navigateToSignVerify} testID="SignVerify" title={loc.addresses.sign_title} />
<ListItem
containerStyle={stylesHook.listItemContainerBorder}
onPress={navigateToMultisigCoordinationSetup}
title={loc.multisig.export_coordination_setup.replace(/^\w/, (c: string) => c.toUpperCase())}
chevron
testID="MultisigCoordinationSetup"
bottomDivider
/>
<ListItem
containerStyle={stylesHook.listItemContainerBorder}
onPress={navigateToViewEditCosigners}
title={loc.multisig.view_edit_cosigners}
chevron
testID="ViewEditCosigners"
bottomDivider={false}
/>
</>
)}
</View>
</View>
{/* Advanced */}
<View style={[styles.detailsCard, stylesHook.detailsCard]}>
<TouchableOpacity
onPress={() => setIsAdvancedExpanded(prev => !prev)}
style={[styles.sectionTitle, stylesHook.sectionTitle, styles.sectionTitleRowContainer]}
activeOpacity={0.85}
>
<BlueText style={[styles.sectionTitleText, stylesHook.sectionTitleText]}>{loc.wallets.details_advanced}</BlueText>
<Icon
name={isAdvancedExpanded ? 'chevron-up' : 'chevron-down'}
type="font-awesome"
size={16}
color={colors.alternativeTextColor}
/>
</TouchableOpacity>
{isAdvancedExpanded && (
<View style={[styles.advancedContent, stylesHook.advancedContent]}>
<ListItem
containerStyle={stylesHook.listItemContainerBorder}
title={loc.wallets.details_type}
titleStyle={stylesHook.advancedListItemTitle}
rightTitle={wallet.typeReadable}
rightTitleStyle={stylesHook.advancedListItemRightTitle}
bottomDivider={
!!(
wallet.type === MultisigHDWallet.type ||
derivationPath ||
(wallet.allowMasterFingerprint && wallet.allowMasterFingerprint())
)
}
/>
{wallet.type === MultisigHDWallet.type && (
<>
<ListItem
containerStyle={stylesHook.listItemContainerBorder}
title={loc.wallets.details_multisig_type}
titleStyle={stylesHook.advancedListItemTitle}
rightTitle={`${wallet.getM()} / ${wallet.getN()} (${
wallet.isNativeSegwit() ? 'native segwit' : wallet.isWrappedSegwit() ? 'wrapped segwit' : 'legacy'
})`}
rightTitleStyle={stylesHook.advancedListItemRightTitle}
bottomDivider={!!(derivationPath || (wallet.allowMasterFingerprint && wallet.allowMasterFingerprint()))}
/>
<ListItem
containerStyle={stylesHook.listItemContainerBorder}
title={loc.multisig.how_many_signatures_can_bluewallet_make}
titleStyle={stylesHook.advancedListItemTitle}
rightTitle={String(wallet.howManySignaturesCanWeMake())}
rightTitleStyle={stylesHook.advancedListItemRightTitle}
bottomDivider={!!(derivationPath || (wallet.allowMasterFingerprint && wallet.allowMasterFingerprint()))}
/>
</>
)}
{wallet.allowMasterFingerprint && wallet.allowMasterFingerprint() && (
<ListItem
containerStyle={stylesHook.listItemContainerBorder}
onPress={isMasterFingerPrintVisible ? undefined : onViewMasterFingerPrintPress}
title={loc.wallets.details_master_fingerprint}
titleStyle={stylesHook.advancedListItemTitle}
rightTitle={
isMasterFingerPrintVisible ? (masterFingerprint ?? loc.wallets.import_derivation_loading) : loc.multisig.view
}
rightTitleStyle={stylesHook.advancedListItemRightTitle}
bottomDivider={!!derivationPath}
/>
)}
{derivationPath && (
<ListItem
containerStyle={stylesHook.listItemContainerBorder}
title={loc.wallets.details_derivation_path}
titleStyle={stylesHook.advancedListItemTitle}
rightTitle={derivationPath}
rightTitleStyle={stylesHook.advancedListItemRightTitle}
bottomDivider={false}
testID="DerivationPath"
/>
)}
</View>
)}
</View>
<BlueCard style={styles.address}>
<View>
<SecondButton
onPress={navigateToWalletExport}
testID="WalletExport"
title={loc.wallets.details_export_backup}
backgroundColor={colors.mainColor}
textColor={colors.buttonTextColor}
/>
<BlueSpacing20 />
<BlueSpacing20 />
<SecondButton
onPress={handleDeleteButtonTapped}
testID="DeleteWallet"
title={loc.wallets.details_delete_wallet}
backgroundColor={colors.redBG}
textColor={colors.redText}
/>
</View>
</BlueCard>
</>
@ -685,6 +905,11 @@ const styles = StyleSheet.create({
alignItems: 'center',
flex: 1,
},
addressSectionContent: {
paddingTop: 4,
paddingBottom: 16,
paddingHorizontal: 12,
},
textLabel1: {
fontWeight: '500',
fontSize: 14,
@ -693,37 +918,101 @@ const styles = StyleSheet.create({
textLabel2: {
fontWeight: '500',
fontSize: 14,
marginVertical: 16,
marginVertical: 8,
},
textValue: {
fontWeight: '500',
fontSize: 14,
},
input: {
flexDirection: 'row',
borderWidth: 1,
borderBottomWidth: 0.5,
minHeight: 44,
height: 44,
alignItems: 'center',
borderRadius: 4,
nameRow: {
marginBottom: 32,
},
inputText: {
nameValue: {
flex: 1,
marginHorizontal: 8,
minHeight: 33,
color: '#81868e',
fontWeight: '500',
fontSize: 18,
},
hardware: {
flexDirection: 'row',
editButton: {
paddingVertical: 4,
paddingHorizontal: 12,
borderRadius: 6,
minWidth: 50,
alignItems: 'center',
justifyContent: 'space-between',
justifyContent: 'center',
},
row: {
editButtonText: {
fontSize: 15,
fontWeight: '500',
lineHeight: 20,
},
detailsCard: {
marginHorizontal: 16,
marginBottom: 40,
padding: 0,
borderRadius: 12,
overflow: 'hidden',
},
sectionTitle: {
paddingVertical: 16,
paddingHorizontal: 16,
borderTopLeftRadius: 12,
borderTopRightRadius: 12,
},
sectionTitleRowContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
marginRight16: {
marginRight: 16,
sectionTitleText: {
fontSize: 17,
fontWeight: '600',
},
optionsSubheader: {
paddingTop: 8,
paddingHorizontal: 12,
},
statsRow: {
flexDirection: 'row',
gap: 12,
},
statsBox: {
flex: 1,
padding: 16,
borderRadius: 12,
},
statsBoxTitleRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
statsBoxTitleRowSpacer: {
width: 20,
height: 20,
},
optionsSectionHeader: {
width: '100%',
alignSelf: 'stretch',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 0,
minHeight: 44,
borderTopLeftRadius: 12,
borderTopRightRadius: 12,
overflow: 'hidden',
},
statsBoxNumber: {
fontSize: 32,
fontWeight: '700',
},
advancedContent: {
marginTop: 0,
paddingTop: 0,
paddingBottom: 0,
},
listItemContainerBorderLight: {
backgroundColor: 'transparent',
borderBottomColor: 'rgba(255, 255, 255, 0.1)',
},
});

View File

@ -120,17 +120,15 @@ describe('BlueWallet UI Tests - no wallets', () => {
// network -> electrum server
// change electrum server to electrum.blockstream.info and revert it back
// skip this test on iOS. HeaderMenuButton tap triggers a keyboard open for some reason.
if (device.getPlatform() === 'andoid') {
if (device.getPlatform() === 'android') {
await element(by.id('ElectrumSettings')).tap();
await waitFor(element(by.id('HostInput')))
.toBeVisible()
.whileElement(by.id('ElectrumSettingsScrollView'))
.scroll(500, 'down'); // in case emu screen is small and it doesnt fit
await element(by.id('HostInput')).replaceText('electrum.blockstream.info\n');
await element(by.id('HostInput')).tapReturnKey();
await waitForKeyboardToClose();
await element(by.id('PortInput')).replaceText('50001\n');
await element(by.id('PortInput')).tapReturnKey();
await waitForKeyboardToClose();
await waitFor(element(by.id('Save')))
.toBeVisible()
@ -263,12 +261,6 @@ describe('BlueWallet UI Tests - no wallets', () => {
await tapAndTapAgainIfElementIsNotVisible('cr34t3d', 'ReceiveButton');
await element(by.id('ReceiveButton')).tap();
await element(by.text('Yes, I have.')).tap();
try {
// in case emulator has no google services and doesnt support pushes
// we just dont show this popup
await element(by.text(`No, and do not ask me again.`)).tap();
await element(by.text(`No, and do not ask me again.`)).tap(); // sometimes the first click doesnt work (detox issue, not app's)
} catch (_) {}
await waitForId('BitcoinAddressQRCode');
await waitForId('CopyTextToClipboard');
await element(by.id('SetCustomAmountButton')).tap();
@ -584,18 +576,20 @@ describe('BlueWallet UI Tests - no wallets', () => {
await element(by.id('Multisig Vault')).tap(); // go inside the wallet
await waitForId('ReceiveButton');
await element(by.id('ReceiveButton')).tap();
try {
// in case emulator has no google services and doesnt support pushes
// we just dont show this popup
await element(by.text(`No, and do not ask me again.`)).tap();
await element(by.text(`No, and do not ask me again.`)).tap(); // sometimes the first click doesnt work (detox issue, not app's)
} catch (_) {}
await waitForLabel('bc1qmf06nt4jhvzz4387ak8fecs42k6jqygr2unumetfc7xkdup7ah9s8phlup');
await goBack();
await element(by.id('WalletDetails')).tap();
await waitForText('2 / 2 (native segwit)');
await waitFor(element(by.text('Advanced')))
.toBeVisible()
.whileElement(by.id('WalletDetailsScroll'))
.scroll(150, 'down');
await element(by.text('Advanced')).tap();
await waitFor(element(by.text('2 / 2 (native segwit)')))
.toBeVisible()
.whileElement(by.id('WalletDetailsScroll'))
.scroll(100, 'down');
process.env.CI && require('fs').writeFileSync(lockFile, '1');
});
@ -776,7 +770,15 @@ describe('BlueWallet UI Tests - no wallets', () => {
// go to wallet and check derivation path
await element(by.id('Imported HD Legacy (BIP44 P2PKH)')).tap();
await element(by.id('WalletDetails')).tap();
await expect(element(by.id('DerivationPath'))).toHaveText("m/44'/0'/0'");
await waitFor(element(by.text('Advanced')))
.toBeVisible()
.whileElement(by.id('WalletDetailsScroll'))
.scroll(150, 'down');
await element(by.text('Advanced')).tap();
await waitFor(element(by.text("m/44'/0'/0'")))
.toBeVisible()
.whileElement(by.id('WalletDetailsScroll'))
.scroll(100, 'down');
process.env.CI && require('fs').writeFileSync(lockFile, '1');
});
@ -938,14 +940,23 @@ describe('BlueWallet UI Tests - no wallets', () => {
// verify wallet details
await element(by.id('WalletDetails')).tap();
await waitForText('2 / 3 (native segwit)');
await waitFor(element(by.text('Advanced')))
.toBeVisible()
.whileElement(by.id('WalletDetailsScroll'))
.scroll(150, 'down');
await element(by.text('Advanced')).tap();
await waitFor(element(by.text('2 / 3 (native segwit)')))
.toBeVisible()
.whileElement(by.id('WalletDetailsScroll'))
.scroll(100, 'down');
// test Export Coordination Setup, it has animated qrcode, that uses setInterval, so we need to disable synchronization
await waitFor(element(by.id('MultisigCoordinationSetup')))
.toBeVisible()
.whileElement(by.id('WalletDetailsScroll'))
.scroll(150, 'down');
await element(by.id('MultisigCoordinationSetup')).tap();
// Tap high in the row: full-width list hitboxes near the bottom edge can extend into the system nav bar on Android.
await element(by.id('MultisigCoordinationSetup')).tap({ x: 120, y: 18 });
await device.disableSynchronization();
await waitForId('ExportMultisigCoordinationSetupView');
await element(by.id('NavigationCloseButton')).atIndex(0).tap();
@ -959,7 +970,11 @@ describe('BlueWallet UI Tests - no wallets', () => {
const vaultReceiveAddress = await extractTextFromElementById('AddressValue');
assert.ok(vaultReceiveAddress && vaultReceiveAddress.length > 20);
await goBack();
await waitForId('ReceiveButton');
await element(by.id('WalletDetails')).tap();
await waitFor(element(by.id('WalletDetailsScroll')))
.toBeVisible()
.withTimeout(20000);
console.log('vaultReceiveAddress', vaultReceiveAddress);
@ -968,6 +983,8 @@ describe('BlueWallet UI Tests - no wallets', () => {
.toBeVisible()
.whileElement(by.id('WalletDetailsScroll'))
.scroll(100, 'down');
// Extra scroll moves the full-width row up so its hitbox no longer overlaps the Android system nav bar.
await element(by.id('WalletDetailsScroll')).scroll(200, 'down');
await element(by.id('ViewEditCosigners')).tap();
await waitForText('Vault Key 1');
await expect(element(by.text('Vault Key 2'))).toBeVisible();
@ -1008,11 +1025,17 @@ describe('BlueWallet UI Tests - no wallets', () => {
// go back to manage keys, restore seed for cosigner 3, and save
await goBack();
await waitForId('ReceiveButton');
await element(by.id('WalletDetails')).tap();
await waitFor(element(by.id('WalletDetailsScroll')))
.toBeVisible()
.withTimeout(20000);
await waitFor(element(by.id('ViewEditCosigners')))
.toBeVisible()
.whileElement(by.id('WalletDetailsScroll'))
.scroll(100, 'down');
// Extra scroll moves the full-width row up so its hitbox no longer overlaps the Android system nav bar.
await element(by.id('WalletDetailsScroll')).scroll(200, 'down');
await element(by.id('ViewEditCosigners')).tap();
await waitFor(element(by.id('VaultCosignerImportMnemonics3')))
.toBeVisible()
@ -1096,7 +1119,15 @@ describe('BlueWallet UI Tests - no wallets', () => {
await element(by.id('Multisig Vault')).tap();
await waitForId('ReceiveButton');
await element(by.id('WalletDetails')).tap();
await waitForText('2 / 2 (wrapped segwit)');
await waitFor(element(by.text('Advanced')))
.toBeVisible()
.whileElement(by.id('WalletDetailsScroll'))
.scroll(150, 'down');
await element(by.text('Advanced')).tap();
await waitFor(element(by.text('2 / 2 (wrapped segwit)')))
.toBeVisible()
.whileElement(by.id('WalletDetailsScroll'))
.scroll(100, 'down');
process.env.CI && require('fs').writeFileSync(lockFile, '1');
});

View File

@ -394,12 +394,16 @@ describe('BlueWallet UI Tests - import BIP84 wallet', () => {
// switch on BIP47 slider if its not switched
if (!(await getSwitchValue('BIP47Switch'))) {
await expect(element(by.text('Contacts'))).not.toBeVisible();
// Scroll down so Options section (BIP47 switch) is on screen
await element(by.id('WalletDetailsScroll')).swipe('up', 'fast', 1);
await element(by.id('WalletDetailsScroll')).swipe('up', 'fast', 1);
await waitFor(element(by.id('BIP47Switch')))
.toExist()
.withTimeout(5000);
await element(by.id('BIP47Switch')).tap();
await waitFor(element(by.text('Contacts')))
.toBeVisible()
.whileElement(by.id('WalletDetailsScroll'))
.scroll(500, 'down');
await expect(element(by.text('Contacts'))).toBeVisible();
.toExist()
.withTimeout(10000);
await goBack();
} else {
await goBack();
@ -408,11 +412,6 @@ describe('BlueWallet UI Tests - import BIP84 wallet', () => {
// go to receive screen and check that payment code is there
await waitForId('ReceiveButton');
await element(by.id('ReceiveButton')).tap();
try {
await element(by.text('ASK ME LATER.')).tap();
} catch (_) {}
await element(by.text('Payment Code')).tap();
await element(by.id('ReceiveDetailsScrollView')).swipe('up', 'fast', 1); // in case emu screen is small and it doesnt fit
await sleep(200);
@ -562,9 +561,10 @@ describe('BlueWallet UI Tests - import BIP84 wallet', () => {
// let's test wallet details screens
await element(by.id('WalletDetails')).tap();
// rename test
await element(by.id('WalletNameInput')).replaceText('testname');
await element(by.id('WalletNameInput')).typeText('\n'); // newline is what triggers saving the wallet
// rename test: tap edit, enter new name in prompt, tap OK
await element(by.id('WalletNameEditButton')).tap();
await typeTextIntoAlertInput('testname');
await element(by.text('OK')).tap();
await waitForKeyboardToClose();
await goBack();
await waitForText('testname');
@ -572,8 +572,9 @@ describe('BlueWallet UI Tests - import BIP84 wallet', () => {
await element(by.id('WalletDetails')).tap();
// rename back
await element(by.id('WalletNameInput')).replaceText('Imported HD SegWit (BIP84 Bech32 Native)');
await element(by.id('WalletNameInput')).typeText('\n'); // newline is what triggers saving the wallet
await element(by.id('WalletNameEditButton')).tap();
await typeTextIntoAlertInput('Imported HD SegWit (BIP84 Bech32 Native)');
await element(by.text('OK')).tap();
await waitForKeyboardToClose();
await goBack();
await waitForText('Imported HD SegWit (BIP84 Bech32 Native)');

View File

@ -51,12 +51,6 @@ describe('BlueWallet UI Tests - import Watch-only wallet (zpub)', () => {
} catch (_) {}
await element(by.id('ReceiveButton')).tap();
try {
// in case emulator has no google services and doesnt support pushes
// we just dont show this popup
await element(by.text(`No, and do not ask me again.`)).tap();
await element(by.text(`No, and do not ask me again.`)).tap(); // sometimes the first click doesnt work (detox issue, not app's)
} catch (_) {}
await expect(element(by.id('BitcoinAddressQRCode'))).toBeVisible();
await expect(element(by.label('bc1qgrhr5xc5774maph97d73ydrjlqqmg2v6jjlr29'))).toBeVisible();
await element(by.id('SetCustomAmountButton')).tap();

View File

@ -166,8 +166,7 @@ export async function helperDeleteWallet(label, remainingBalanceSat = false) {
await element(by.id('WalletDetails')).tap();
await element(by.id('WalletDetailsScroll')).swipe('up', 'fast', 1);
await sleep(200);
await element(by.id('HeaderMenuButton')).tap();
await element(by.text('Delete')).tap();
await element(by.id('DeleteWallet')).tap();
await waitForText('Yes, delete');
await element(by.text('Yes, delete')).tap();
if (remainingBalanceSat) {