Merge branch 'master' into wallrtd

This commit is contained in:
Marcos Rodriguez Velez 2024-10-11 00:12:24 -04:00
commit 90204a9c87
92 changed files with 1499 additions and 1073 deletions

View file

@ -34,7 +34,7 @@ jobs:
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: 15.4
xcode-version: 16.0
- name: Set up Ruby
uses: ruby/setup-ruby@v1
@ -127,14 +127,14 @@ jobs:
- name: Expected IPA file name
run: |
echo "IPA file name: BlueWallet.${{env.PROJECT_VERSION}}(${{env.NEW_BUILD_NUMBER}}).ipa"
echo "IPA file name: BlueWallet_${{env.PROJECT_VERSION}}(${{env.NEW_BUILD_NUMBER}}).ipa"
- name: Build App
run: bundle exec fastlane ios build_app_lane
- name: Upload IPA as Artifact
uses: actions/upload-artifact@v4
with:
name: BlueWallet.${{env.PROJECT_VERSION}}(${{env.NEW_BUILD_NUMBER}}).ipa
path: ./build/BlueWallet.${{env.PROJECT_VERSION}}(${{env.NEW_BUILD_NUMBER}}).ipa
name: BlueWallet_${{env.PROJECT_VERSION}}_${{env.NEW_BUILD_NUMBER}}.ipa
path: ./build/BlueWallet_${{env.PROJECT_VERSION}}_${{env.NEW_BUILD_NUMBER}}.ipa
testflight-upload:
needs: build
@ -167,7 +167,7 @@ jobs:
- name: Download IPA from Artifact
uses: actions/download-artifact@v4
with:
name: BlueWallet.${{ needs.build.outputs.project_version }}(${{ needs.build.outputs.new_build_number }}).ipa
name: BlueWallet_${{ needs.build.outputs.project_version }}_${{ needs.build.outputs.new_build_number }}.ipa
path: ./
- name: Create App Store Connect API Key JSON
run: echo '${{ secrets.APP_STORE_CONNECT_API_KEY_CONTENT }}' > ./appstore_api_key.json

View file

@ -1,7 +1,6 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import BigNumber from 'bignumber.js';
import * as bitcoin from 'bitcoinjs-lib';
import { Alert } from 'react-native';
import DefaultPreference from 'react-native-default-preference';
import RNFS from 'react-native-fs';
import Realm from 'realm';
@ -299,13 +298,13 @@ const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
);
return;
}
Alert.alert(
loc.errors.network,
loc.formatString(
presentAlert({
title: loc.errors.network,
message: loc.formatString(
usingPeer ? loc.settings.electrum_unable_to_connect : loc.settings.electrum_error_connect,
usingPeer ? { server: `${usingPeer.host}:${usingPeer.ssl ?? usingPeer.tcp}` } : {},
),
[
buttons: [
{
text: loc.wallets.list_tryagain,
onPress: () => {
@ -318,10 +317,10 @@ const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
{
text: loc.settings.electrum_reset,
onPress: () => {
Alert.alert(
loc.settings.electrum_reset,
loc.settings.electrum_reset_to_default,
[
presentAlert({
title: loc.settings.electrum_reset,
message: loc.settings.electrum_reset_to_default,
buttons: [
{
text: loc._.cancel,
style: 'cancel',
@ -340,16 +339,15 @@ const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
await DefaultPreference.clear(ELECTRUM_SSL_PORT);
await DefaultPreference.clear(ELECTRUM_TCP_PORT);
} catch (e) {
// Must be running on Android
console.log(e);
console.log(e); // Must be running on Android
}
presentAlert({ message: loc.settings.electrum_saved });
setTimeout(connectMain, 500);
},
},
],
{ cancelable: true },
);
options: { cancelable: true },
});
connectionAttempt = 0;
mainClient.close() && mainClient.close();
},
@ -364,8 +362,8 @@ const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
style: 'cancel',
},
],
{ cancelable: false },
);
options: { cancelable: false },
});
};
/**

View file

@ -31,8 +31,6 @@ function Notifications(props) {
return false;
};
Notifications.isNotificationsCapable = hasGmsSync() || hasHmsSync() || Platform.OS !== 'android';
/**
* Calls `configure`, which tries to obtain push token, save it, and registers all associated with
* notifications callbacks
@ -131,7 +129,7 @@ function Notifications(props) {
* @returns {Promise<boolean>} TRUE if permissions were obtained, FALSE otherwise
*/
Notifications.tryToObtainPermissions = async function (anchor) {
if (!Notifications.isNotificationsCapable) return false;
if (!isNotificationsCapable) return false;
if (await Notifications.getPushToken()) {
// we already have a token, no sense asking again, just configure pushes to register callbacks and we are done
if (!alreadyConfigured) configureNotifications(); // no await so it executes in background while we return TRUE and use token
@ -441,4 +439,6 @@ function Notifications(props) {
return null;
}
export const isNotificationsCapable = hasGmsSync() || hasHmsSync() || Platform.OS !== 'android';
export default Notifications;

View file

@ -72,7 +72,7 @@ const AddressInput = ({
const toolTipOnPress = useCallback(async () => {
await scanButtonTapped();
Keyboard.dismiss();
if (launchedBy) scanQrHelper(launchedBy).then(value => onBarScanned({ data: value }));
if (launchedBy) scanQrHelper(launchedBy, true).then(value => onBarScanned({ data: value }));
}, [launchedBy, onBarScanned, scanButtonTapped]);
const onMenuItemPressed = useCallback(

View file

@ -1,35 +1,92 @@
import { Alert as RNAlert, Platform, ToastAndroid } from 'react-native';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../blue_modules/hapticFeedback';
import loc from '../loc';
export enum AlertType {
Alert,
Toast,
}
const presentAlert = ({
title,
message,
type = AlertType.Alert,
hapticFeedback,
}: {
title?: string;
message: string;
type?: AlertType;
hapticFeedback?: HapticFeedbackTypes;
}) => {
if (hapticFeedback) {
triggerHapticFeedback(hapticFeedback);
}
if (Platform.OS !== 'android') {
type = AlertType.Alert;
}
switch (type) {
case AlertType.Toast:
ToastAndroid.show(message, ToastAndroid.LONG);
break;
default:
RNAlert.alert(title ?? message, title && message ? message : undefined);
break;
}
};
interface AlertButton {
text: string;
onPress?: () => void;
style?: 'default' | 'cancel' | 'destructive';
}
interface AlertOptions {
cancelable?: boolean;
}
const presentAlert = (() => {
let lastAlertParams: {
title?: string;
message: string;
type?: AlertType;
hapticFeedback?: HapticFeedbackTypes;
buttons?: AlertButton[];
options?: AlertOptions;
} | null = null;
const clearCache = () => {
lastAlertParams = null;
};
return ({
title,
message,
type = AlertType.Alert,
hapticFeedback,
buttons = [],
options = { cancelable: false },
}: {
title?: string;
message: string;
type?: AlertType;
hapticFeedback?: HapticFeedbackTypes;
buttons?: AlertButton[];
options?: AlertOptions;
}) => {
if (
lastAlertParams &&
lastAlertParams.title === title &&
lastAlertParams.message === message &&
lastAlertParams.type === type &&
lastAlertParams.hapticFeedback === hapticFeedback &&
JSON.stringify(lastAlertParams.buttons) === JSON.stringify(buttons) &&
JSON.stringify(lastAlertParams.options) === JSON.stringify(options)
) {
return; // Skip showing the alert if the content is the same as the last one
}
lastAlertParams = { title, message, type, hapticFeedback, buttons, options };
if (hapticFeedback) {
triggerHapticFeedback(hapticFeedback);
}
// Ensure that there's at least one button (required for both iOS and Android)
const wrappedButtons =
buttons.length > 0
? buttons
: [
{
text: loc._.ok,
onPress: () => {},
},
];
switch (type) {
case AlertType.Toast:
if (Platform.OS === 'android') {
ToastAndroid.show(message, ToastAndroid.LONG);
clearCache();
}
break;
default:
RNAlert.alert(title ?? message, title && message ? message : undefined, wrappedButtons, options);
break;
}
};
})();
export default presentAlert;

View file

@ -362,7 +362,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
marginVertical: 8,
margin: 16,
},
container: {
flexDirection: 'row',

View file

@ -14,6 +14,7 @@ import { useStorage } from '../../hooks/context/useStorage';
import { BitcoinUnit } from '../../models/bitcoinUnits';
import { TotalWalletsBalanceKey, TotalWalletsBalancePreferredUnit } from '../TotalWalletsBalance';
import { LayoutAnimation } from 'react-native';
import { BLOCK_EXPLORERS, getBlockExplorerUrl, saveBlockExplorer, BlockExplorer, normalizeUrl } from '../../models/blockExplorer';
// DefaultPreference and AsyncStorage get/set
@ -85,6 +86,8 @@ interface SettingsContextType {
setTotalBalancePreferredUnitStorage: (unit: BitcoinUnit) => Promise<void>;
isDrawerShouldHide: boolean;
setIsDrawerShouldHide: (value: boolean) => void;
selectedBlockExplorer: BlockExplorer;
setBlockExplorerStorage: (explorer: BlockExplorer) => Promise<boolean>;
}
const defaultSettingsContext: SettingsContextType = {
@ -112,6 +115,8 @@ const defaultSettingsContext: SettingsContextType = {
setTotalBalancePreferredUnitStorage: async (unit: BitcoinUnit) => {},
isDrawerShouldHide: false,
setIsDrawerShouldHide: () => {},
selectedBlockExplorer: BLOCK_EXPLORERS.default,
setBlockExplorerStorage: async (explorer: BlockExplorer) => false,
};
export const SettingsContext = createContext<SettingsContextType>(defaultSettingsContext);
@ -142,6 +147,8 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil
// Toggle Drawer (for screens like Manage Wallets or ScanQRCode)
const [isDrawerShouldHide, setIsDrawerShouldHide] = useState<boolean>(false);
const [selectedBlockExplorer, setSelectedBlockExplorer] = useState<BlockExplorer>(BLOCK_EXPLORERS.default);
const languageStorage = useAsyncStorage(STORAGE_KEY);
const { walletsInitialized } = useStorage();
@ -211,6 +218,18 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil
setTotalBalancePreferredUnitState(unit);
})
.catch(error => console.error('Error fetching total balance preferred unit:', error));
getBlockExplorerUrl()
.then(url => {
console.debug('SettingsContext blockExplorer:', url);
const predefinedExplorer = Object.values(BLOCK_EXPLORERS).find(explorer => normalizeUrl(explorer.url) === normalizeUrl(url));
if (predefinedExplorer) {
setSelectedBlockExplorer(predefinedExplorer);
} else {
setSelectedBlockExplorer({ key: 'custom', name: 'Custom', url });
}
})
.catch(error => console.error('Error fetching block explorer settings:', error));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@ -295,6 +314,13 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil
setTotalBalancePreferredUnitState(unit);
}, []);
const setBlockExplorerStorage = useCallback(async (explorer: BlockExplorer): Promise<boolean> => {
const success = await saveBlockExplorer(explorer.url);
if (success) {
setSelectedBlockExplorer(explorer);
}
return success;
}, []);
const value = useMemo(
() => ({
preferredFiatCurrency,
@ -321,6 +347,8 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil
setTotalBalancePreferredUnitStorage,
isDrawerShouldHide,
setIsDrawerShouldHide,
selectedBlockExplorer,
setBlockExplorerStorage,
}),
[
preferredFiatCurrency,
@ -347,6 +375,8 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil
setTotalBalancePreferredUnitStorage,
isDrawerShouldHide,
setIsDrawerShouldHide,
selectedBlockExplorer,
setBlockExplorerStorage,
],
);

View file

@ -59,28 +59,16 @@ export enum WalletTransactionsStatus {
NONE = 'NONE',
ALL = 'ALL',
}
// @ts-ignore defaut value does not match the type
// @ts-ignore default value does not match the type
export const StorageContext = createContext<StorageContextType>(undefined);
export const StorageProvider = ({ children }: { children: React.ReactNode }) => {
const txMetadata = useRef<TTXMetadata>(BlueApp.tx_metadata);
const counterpartyMetadata = useRef<TCounterpartyMetadata>(BlueApp.counterparty_metadata || {}); // init
const getTransactions = BlueApp.getTransactions;
const fetchWalletBalances = BlueApp.fetchWalletBalances;
const fetchWalletTransactions = BlueApp.fetchWalletTransactions;
const getBalance = BlueApp.getBalance;
const isStorageEncrypted = BlueApp.storageIsEncrypted;
const encryptStorage = BlueApp.encryptStorage;
const sleep = BlueApp.sleep;
const createFakeStorage = BlueApp.createFakeStorage;
const decryptStorage = BlueApp.decryptStorage;
const isPasswordInUse = BlueApp.isPasswordInUse;
const cachedPassword = BlueApp.cachedPassword;
const getItem = BlueApp.getItem;
const setItem = BlueApp.setItem;
const [wallets, setWallets] = useState<TWallet[]>([]);
const [selectedWalletID, setSelectedWalletID] = useState<undefined | string>();
const [selectedWalletID, setSelectedWalletID] = useState<string | undefined>();
const [walletTransactionUpdateStatus, setWalletTransactionUpdateStatus] = useState<WalletTransactionsStatus | string>(
WalletTransactionsStatus.NONE,
);
@ -89,6 +77,47 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
const [currentSharedCosigner, setCurrentSharedCosigner] = useState<string>('');
const [reloadTransactionsMenuActionFunction, setReloadTransactionsMenuActionFunction] = useState<() => void>(() => {});
const saveToDisk = useCallback(
async (force: boolean = false) => {
if (!force && BlueApp.getWallets().length === 0) {
console.debug('Not saving empty wallets array');
return;
}
await InteractionManager.runAfterInteractions(async () => {
BlueApp.tx_metadata = txMetadata.current;
BlueApp.counterparty_metadata = counterpartyMetadata.current;
await BlueApp.saveToDisk();
const w: TWallet[] = [...BlueApp.getWallets()];
setWallets(w);
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[txMetadata.current, counterpartyMetadata.current],
);
const addWallet = useCallback((wallet: TWallet) => {
BlueApp.wallets.push(wallet);
setWallets([...BlueApp.getWallets()]);
}, []);
const deleteWallet = useCallback((wallet: TWallet) => {
BlueApp.deleteWallet(wallet);
setWallets([...BlueApp.getWallets()]);
}, []);
const resetWallets = useCallback(() => {
setWallets(BlueApp.getWallets());
}, []);
const setWalletsWithNewOrder = useCallback(
(wlts: TWallet[]) => {
BlueApp.wallets = wlts;
saveToDisk();
},
[saveToDisk],
);
// Initialize wallets and connect to Electrum
useEffect(() => {
BlueElectrum.isDisabled().then(setIsElectrumDisabled);
if (walletsInitialized) {
@ -99,33 +128,6 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
}
}, [walletsInitialized]);
const saveToDisk = useCallback(async (force: boolean = false) => {
InteractionManager.runAfterInteractions(async () => {
if (BlueApp.getWallets().length === 0 && !force) {
console.log('not saving empty wallets array');
return;
}
BlueApp.tx_metadata = txMetadata.current;
BlueApp.counterparty_metadata = counterpartyMetadata.current;
await BlueApp.saveToDisk();
setWallets([...BlueApp.getWallets()]);
txMetadata.current = BlueApp.tx_metadata;
counterpartyMetadata.current = BlueApp.counterparty_metadata;
});
}, []);
const resetWallets = () => {
setWallets(BlueApp.getWallets());
};
const setWalletsWithNewOrder = useCallback(
(wlts: TWallet[]) => {
BlueApp.wallets = wlts;
saveToDisk();
},
[saveToDisk],
);
const refreshAllWalletTransactions = useCallback(
async (lastSnappedTo?: number, showUpdateStatusIndicator: boolean = true) => {
const TIMEOUT_DURATION = 30000;
@ -137,41 +139,37 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
);
const mainLogicPromise = new Promise<void>((resolve, reject) => {
try {
InteractionManager.runAfterInteractions(async () => {
let noErr = true;
try {
await BlueElectrum.waitTillConnected();
if (showUpdateStatusIndicator) {
setWalletTransactionUpdateStatus(WalletTransactionsStatus.ALL);
}
const paymentCodesStart = Date.now();
await BlueApp.fetchSenderPaymentCodes(lastSnappedTo);
const paymentCodesEnd = Date.now();
console.log('fetch payment codes took', (paymentCodesEnd - paymentCodesStart) / 1000, 'sec');
const balanceStart = +new Date();
await fetchWalletBalances(lastSnappedTo);
const balanceEnd = +new Date();
console.log('fetch balance took', (balanceEnd - balanceStart) / 1000, 'sec');
const start = +new Date();
await fetchWalletTransactions(lastSnappedTo);
const end = +new Date();
console.log('fetch tx took', (end - start) / 1000, 'sec');
} catch (err) {
noErr = false;
console.warn(err);
reject(err);
} finally {
setWalletTransactionUpdateStatus(WalletTransactionsStatus.NONE);
InteractionManager.runAfterInteractions(async () => {
let noErr = true;
try {
await BlueElectrum.waitTillConnected();
if (showUpdateStatusIndicator) {
setWalletTransactionUpdateStatus(WalletTransactionsStatus.ALL);
}
if (noErr) await saveToDisk(); // caching
resolve();
});
} catch (err) {
reject(err);
} finally {
setWalletTransactionUpdateStatus(WalletTransactionsStatus.NONE);
}
const paymentCodesStart = Date.now();
await BlueApp.fetchSenderPaymentCodes(lastSnappedTo);
const paymentCodesEnd = Date.now();
console.debug('fetch payment codes took', (paymentCodesEnd - paymentCodesStart) / 1000, 'sec');
const balanceStart = Date.now();
await BlueApp.fetchWalletBalances(lastSnappedTo);
const balanceEnd = Date.now();
console.debug('fetch balance took', (balanceEnd - balanceStart) / 1000, 'sec');
const start = Date.now();
await BlueApp.fetchWalletTransactions(lastSnappedTo);
const end = Date.now();
console.debug('fetch tx took', (end - start) / 1000, 'sec');
} catch (err) {
noErr = false;
console.error(err);
reject(err);
} finally {
setWalletTransactionUpdateStatus(WalletTransactionsStatus.NONE);
}
if (noErr) await saveToDisk();
resolve();
});
});
try {
@ -182,53 +180,43 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
setWalletTransactionUpdateStatus(WalletTransactionsStatus.NONE);
}
},
[fetchWalletBalances, fetchWalletTransactions, saveToDisk],
[saveToDisk],
);
const fetchAndSaveWalletTransactions = useCallback(
async (walletID: string) => {
InteractionManager.runAfterInteractions(async () => {
await InteractionManager.runAfterInteractions(async () => {
const index = wallets.findIndex(wallet => wallet.getID() === walletID);
let noErr = true;
try {
// 5sec debounce:
if (+new Date() - _lastTimeTriedToRefetchWallet[walletID] < 5000) {
console.log('re-fetch wallet happens too fast; NOP');
if (Date.now() - (_lastTimeTriedToRefetchWallet[walletID] || 0) < 5000) {
console.debug('Re-fetch wallet happens too fast; NOP');
return;
}
_lastTimeTriedToRefetchWallet[walletID] = +new Date();
_lastTimeTriedToRefetchWallet[walletID] = Date.now();
await BlueElectrum.waitTillConnected();
setWalletTransactionUpdateStatus(walletID);
const balanceStart = +new Date();
await fetchWalletBalances(index);
const balanceEnd = +new Date();
console.log('fetch balance took', (balanceEnd - balanceStart) / 1000, 'sec');
const start = +new Date();
await fetchWalletTransactions(index);
const end = +new Date();
console.log('fetch tx took', (end - start) / 1000, 'sec');
const balanceStart = Date.now();
await BlueApp.fetchWalletBalances(index);
const balanceEnd = Date.now();
console.debug('fetch balance took', (balanceEnd - balanceStart) / 1000, 'sec');
const start = Date.now();
await BlueApp.fetchWalletTransactions(index);
const end = Date.now();
console.debug('fetch tx took', (end - start) / 1000, 'sec');
} catch (err) {
noErr = false;
console.warn(err);
console.error(err);
} finally {
setWalletTransactionUpdateStatus(WalletTransactionsStatus.NONE);
}
if (noErr) await saveToDisk(); // caching
if (noErr) await saveToDisk();
});
},
[fetchWalletBalances, fetchWalletTransactions, saveToDisk, wallets],
[saveToDisk, wallets],
);
const addWallet = useCallback((wallet: TWallet) => {
BlueApp.wallets.push(wallet);
setWallets([...BlueApp.getWallets()]);
}, []);
const deleteWallet = useCallback((wallet: TWallet) => {
BlueApp.deleteWallet(wallet);
setWallets([...BlueApp.getWallets()]);
}, []);
const addAndSaveWallet = useCallback(
async (w: TWallet) => {
if (wallets.some(i => i.getID() === w.getID())) {
@ -247,13 +235,12 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
hapticFeedback: HapticFeedbackTypes.ImpactHeavy,
message: w.type === WatchOnlyWallet.type ? loc.wallets.import_success_watchonly : loc.wallets.import_success,
});
// @ts-ignore need to type notifications first
// @ts-ignore: Notifications type is not defined
Notifications.majorTomToGroundControl(w.getAllExternalAddresses(), [], []);
// start balance fetching at the background
await w.fetchBalance();
setWallets([...BlueApp.getWallets()]);
},
[addWallet, saveToDisk, wallets],
[wallets, addWallet, saveToDisk],
);
const value: StorageContextType = useMemo(
@ -263,7 +250,7 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
txMetadata: txMetadata.current,
counterpartyMetadata: counterpartyMetadata.current,
saveToDisk,
getTransactions,
getTransactions: BlueApp.getTransactions,
selectedWalletID,
setSelectedWalletID,
addWallet,
@ -271,24 +258,24 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
currentSharedCosigner,
setSharedCosigner: setCurrentSharedCosigner,
addAndSaveWallet,
setItem,
getItem,
fetchWalletBalances,
fetchWalletTransactions,
setItem: BlueApp.setItem,
getItem: BlueApp.getItem,
fetchWalletBalances: BlueApp.fetchWalletBalances,
fetchWalletTransactions: BlueApp.fetchWalletTransactions,
fetchAndSaveWalletTransactions,
isStorageEncrypted,
encryptStorage,
isStorageEncrypted: BlueApp.storageIsEncrypted,
encryptStorage: BlueApp.encryptStorage,
startAndDecrypt,
cachedPassword,
getBalance,
cachedPassword: BlueApp.cachedPassword,
getBalance: BlueApp.getBalance,
walletsInitialized,
setWalletsInitialized,
refreshAllWalletTransactions,
sleep,
createFakeStorage,
sleep: BlueApp.sleep,
createFakeStorage: BlueApp.createFakeStorage,
resetWallets,
decryptStorage,
isPasswordInUse,
decryptStorage: BlueApp.decryptStorage,
isPasswordInUse: BlueApp.isPasswordInUse,
walletTransactionUpdateStatus,
setWalletTransactionUpdateStatus,
isElectrumDisabled,
@ -300,30 +287,23 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
wallets,
setWalletsWithNewOrder,
saveToDisk,
getTransactions,
selectedWalletID,
setSelectedWalletID,
addWallet,
deleteWallet,
currentSharedCosigner,
addAndSaveWallet,
setItem,
getItem,
fetchWalletBalances,
fetchWalletTransactions,
fetchAndSaveWalletTransactions,
isStorageEncrypted,
encryptStorage,
cachedPassword,
getBalance,
walletsInitialized,
setWalletsInitialized,
refreshAllWalletTransactions,
sleep,
createFakeStorage,
decryptStorage,
isPasswordInUse,
resetWallets,
walletTransactionUpdateStatus,
setWalletTransactionUpdateStatus,
isElectrumDisabled,
setIsElectrumDisabled,
reloadTransactionsMenuActionFunction,
setReloadTransactionsMenuActionFunction,
],
);

View file

@ -0,0 +1,31 @@
import React from 'react';
import ToolTipMenu from './TooltipMenu';
import { useTheme } from './themes';
import { Icon } from '@rneui/themed';
import { Platform } from 'react-native';
import { Action } from './types';
interface HeaderMenuButtonProps {
onPressMenuItem: (id: string) => void;
actions: Action[];
disabled?: boolean;
}
const HeaderMenuButton: React.FC<HeaderMenuButtonProps> = ({ onPressMenuItem, actions, disabled }) => {
const { colors } = useTheme();
const styleProps = Platform.OS === 'android' ? { iconStyle: { transform: [{ rotate: '90deg' }] } } : {};
return (
<ToolTipMenu
testID="HeaderMenuButton"
disabled={disabled}
isButton
isMenuPrimaryAction
onPressMenuItem={onPressMenuItem}
actions={actions}
>
<Icon size={22} name="more-horiz" type="material" color={colors.foregroundColor} {...styleProps} />
</ToolTipMenu>
);
};
export default HeaderMenuButton;

View file

@ -98,6 +98,8 @@ const ListItem: React.FC<ListItemProps> = React.memo(
writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr',
color: colors.alternativeTextColor,
fontWeight: '400',
paddingVertical: switchProps ? 8 : 0,
lineHeight: 20,
fontSize: 14,
},
@ -130,7 +132,7 @@ const ListItem: React.FC<ListItemProps> = React.memo(
</RNElementsListItem.Title>
{subtitle && (
<RNElementsListItem.Subtitle
numberOfLines={subtitleNumberOfLines ?? 1}
numberOfLines={switchProps ? 0 : (subtitleNumberOfLines ?? 1)}
accessible={switchProps === undefined}
style={stylesHook.subtitle}
>
@ -152,7 +154,9 @@ const ListItem: React.FC<ListItemProps> = React.memo(
<>
{chevron && <RNElementsListItem.Chevron iconStyle={{ transform: [{ scaleX: I18nManager.isRTL ? -1 : 1 }] }} />}
{rightIcon && <Avatar icon={rightIcon} />}
{switchProps && <Switch {...memoizedSwitchProps} accessibilityLabel={title} accessible accessibilityRole="switch" />}
{switchProps && (
<Switch {...memoizedSwitchProps} accessibilityLabel={title} style={styles.margin16} accessible accessibilityRole="switch" />
)}
{checkmark && (
<RNElementsListItem.CheckBox
iconRight
@ -216,5 +220,8 @@ const styles = StyleSheet.create({
margin8: {
margin: 8,
},
margin16: {
marginLeft: 16,
},
width16: { width: 16 },
});

View file

@ -101,6 +101,8 @@ const QRCodeComponent: React.FC<QRCodeComponentProps> = ({
<View
style={styles.qrCodeContainer}
testID="BitcoinAddressQRCodeContainer"
accessibilityIgnoresInvertColors
importantForAccessibility="no-hide-descendants"
accessibilityRole="image"
accessibilityLabel={loc.receive.qrcode_for_the_address}
>

View file

@ -0,0 +1,89 @@
import React from 'react';
import { StyleSheet, TextInput, View, Switch } from 'react-native';
import { ListItem } from '@rneui/themed';
import { useTheme } from './themes';
import loc from '../loc';
interface SettingsBlockExplorerCustomUrlItemProps {
isCustomEnabled: boolean;
onSwitchToggle: (value: boolean) => void;
customUrl: string;
onCustomUrlChange: (url: string) => void;
onSubmitCustomUrl: () => void;
inputRef?: React.RefObject<TextInput>;
}
const SettingsBlockExplorerCustomUrlItem: React.FC<SettingsBlockExplorerCustomUrlItemProps> = ({
isCustomEnabled,
onSwitchToggle,
customUrl,
onCustomUrlChange,
onSubmitCustomUrl,
inputRef,
}) => {
const { colors } = useTheme();
return (
<>
<ListItem containerStyle={[styles.container, { backgroundColor: colors.background }]} bottomDivider>
<ListItem.Content>
<ListItem.Title style={[styles.title, { color: colors.text }]}>{loc.settings.block_explorer_preferred}</ListItem.Title>
</ListItem.Content>
<Switch
accessible
accessibilityRole="switch"
accessibilityState={{ checked: isCustomEnabled }}
onValueChange={onSwitchToggle}
value={isCustomEnabled}
/>
</ListItem>
{isCustomEnabled && (
<View style={[styles.uriContainer, { borderColor: colors.formBorder, backgroundColor: colors.inputBackgroundColor }]}>
<TextInput
ref={inputRef}
value={customUrl}
placeholder={loc._.enter_url}
onChangeText={onCustomUrlChange}
numberOfLines={1}
style={[styles.uriText, { color: colors.text }]}
placeholderTextColor={colors.placeholderTextColor}
textContentType="URL"
clearButtonMode="while-editing"
autoCapitalize="none"
autoCorrect={false}
underlineColorAndroid="transparent"
onSubmitEditing={onSubmitCustomUrl}
editable={isCustomEnabled}
/>
</View>
)}
</>
);
};
export default SettingsBlockExplorerCustomUrlItem;
const styles = StyleSheet.create({
container: {
minHeight: 60,
paddingVertical: 10,
},
title: {
fontSize: 16,
fontWeight: '500',
},
uriContainer: {
flexDirection: 'row',
borderWidth: 1,
borderRadius: 4,
marginHorizontal: 15,
marginVertical: 10,
paddingHorizontal: 10,
alignItems: 'center',
},
uriText: {
flex: 1,
minHeight: 36,
},
});

View file

@ -43,7 +43,7 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
const { navigate } = useExtendedNavigation<NavigationProps>();
const menuRef = useRef<ToolTipMenuProps>();
const { txMetadata, counterpartyMetadata, wallets } = useStorage();
const { language } = useSettings();
const { language, selectedBlockExplorer } = useSettings();
const containerStyle = useMemo(
() => ({
backgroundColor: 'transparent',
@ -253,16 +253,16 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
const handleOnCopyTransactionID = useCallback(() => Clipboard.setString(item.hash), [item.hash]);
const handleOnCopyNote = useCallback(() => Clipboard.setString(subtitle ?? ''), [subtitle]);
const handleOnViewOnBlockExplorer = useCallback(() => {
const url = `https://mempool.space/tx/${item.hash}`;
const url = `${selectedBlockExplorer}/tx/${item.hash}`;
Linking.canOpenURL(url).then(supported => {
if (supported) {
Linking.openURL(url);
}
});
}, [item.hash]);
}, [item.hash, selectedBlockExplorer]);
const handleCopyOpenInBlockExplorerPress = useCallback(() => {
Clipboard.setString(`https://mempool.space/tx/${item.hash}`);
}, [item.hash]);
Clipboard.setString(`${selectedBlockExplorer}/tx/${item.hash}`);
}, [item.hash, selectedBlockExplorer]);
const onToolTipPress = useCallback(
(id: any) => {

View file

@ -31,6 +31,7 @@ export const BlueDefaultTheme = {
outgoingForegroundColor: '#d0021b',
successColor: '#37c0a1',
failedColor: '#ff0000',
placeholderTextColor: '#81868e',
shadowColor: '#000000',
inverseForegroundColor: '#ffffff',
hdborderColor: '#68BBE1',

View file

@ -331,7 +331,7 @@ platform :ios do
xcargs: "GCC_PREPROCESSOR_DEFINITIONS='$(inherited) VERBOSE_LOGGING=1'",
output_directory: "./build", # Directory where the IPA file will be stored
output_name: "BlueWallet.#{ENV['PROJECT_VERSION']}(#{ENV['NEW_BUILD_NUMBER']}).ipa",
output_name: "BlueWallet_#{ENV['PROJECT_VERSION']}_#{ENV['NEW_BUILD_NUMBER']}.ipa",
buildlog_path: "./build_logs"
)
end
@ -343,10 +343,11 @@ platform :ios do
begin
UI.message("Uploading to TestFlight without processing wait...")
changelog = ENV["LATEST_COMMIT_MESSAGE"]
ipa_path = "./BlueWallet_#{ENV['PROJECT_VERSION']}_#{ENV['NEW_BUILD_NUMBER']}.ipa"
upload_to_testflight(
api_key_path: "./appstore_api_key.json",
ipa: "./BlueWallet.#{ENV['PROJECT_VERSION']}(#{ENV['NEW_BUILD_NUMBER']}).ipa",
ipa: ipa_path,
skip_waiting_for_build_processing: true, # Do not wait for processing
changelog: changelog
)

View file

@ -26,7 +26,7 @@ function scanQrHelper(
if (useMerge) {
const onBarScanned = function (data: any) {
setTimeout(() => resolve(data.data || data), 1);
navigationRef.navigate({ name: currentScreenName, params: {}, merge: true });
navigationRef.navigate({ name: currentScreenName, params: data, merge: true });
};
params = {

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 767 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

View file

@ -1,172 +1,100 @@
{
"images" : [
{
"filename" : "BlueWallet-20@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "BlueWallet-20@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"filename" : "BlueWallet-29@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "BlueWallet-29@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"filename" : "BlueWallet-40@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "BlueWallet-40@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"filename" : "BlueWallet-60@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"filename" : "BlueWallet-60@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"filename" : "BlueWallet-20.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"filename" : "BlueWallet-20@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "BlueWallet-29.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"filename" : "BlueWallet-29@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "BlueWallet-40.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"filename" : "BlueWallet-40@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "BlueWallet-76.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"filename" : "BlueWallet-76@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"filename" : "BlueWallet-83.5@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"filename" : "BlueWallet-1024.png",
"idiom" : "ios-marketing",
"scale" : "1x",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"filename" : "16.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"filename" : "32.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"filename" : "32-1.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"filename" : "64.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"filename" : "128.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"filename" : "256.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"filename" : "256-1.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"filename" : "512.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"filename" : "512-1.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "1024.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"filename" : "1024 1.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"filename" : "16pt@1x.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"filename" : "16pt@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"filename" : "32pt@1x.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"filename" : "32pt@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"filename" : "128pt@1x.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"filename" : "128pt@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"filename" : "256pt@1x.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"filename" : "256pt@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"filename" : "512pt@1x.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"filename" : "512pt@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
},
{
"filename" : "1024 2.png",
"idiom" : "universal",
"platform" : "watchos",
"size" : "1024x1024"
}
],
"info" : {

File diff suppressed because it is too large Load diff

View file

@ -10,6 +10,7 @@
"never": "Never",
"of": "{number} of {total}",
"ok": "OK",
"enter_url": "Enter URL",
"storage_is_encrypted": "Your storage is encrypted. Password is required to decrypt it.",
"yes": "Yes",
"no": "No",
@ -25,7 +26,8 @@
"pick_file": "Choose a file",
"enter_amount": "Enter amount",
"qr_custom_input_button": "Tap 10 times to enter custom input",
"unlock": "Unlock"
"unlock": "Unlock",
"suggested": "Suggested"
},
"azteco": {
"codeIs": "Your voucher code is",
@ -206,8 +208,10 @@
"performance_score": "Performance score: {num}",
"run_performance_test": "Test performance",
"about_selftest": "Run self-test",
"block_explorer_invalid_custom_url": "The URL provided is invalid. Please enter a valid URL starting with http:// or https://.",
"about_selftest_electrum_disabled": "Self-testing is not available with Electrum Offline Mode. Please disable offline mode and try again.",
"about_selftest_ok": "All internal tests have passed successfully. The wallet works well.",
"about_sm_github": "GitHub",
"about_sm_discord": "Discord Server",
"about_sm_telegram": "Telegram channel",
@ -259,6 +263,9 @@
"encrypt_storage_explanation_description_line1": "Enabling Storage Encryption adds an extra layer of protection to your app by securing the way your data is stored on your device. This makes it harder for anyone to access your information without permission.",
"encrypt_storage_explanation_description_line2": "However, it's important to know that this encryption only protects the access to your wallets stored on the device's keychain. It doesn't put a password or any extra protection on the wallets themselves.",
"i_understand": "I understand",
"block_explorer": "Block Explorer",
"block_explorer_preferred": "Use preferred block explorer",
"block_explorer_error_saving_custom": "Error saving preferred block explorer",
"encrypt_title": "Security",
"encrypt_tstorage": "Storage",
"encrypt_use": "Use {type}",

View file

@ -10,6 +10,7 @@
"never": "Nunca",
"of": "{number} de {total}",
"ok": "OK",
"enter_url": "Introducir URL",
"storage_is_encrypted": "Tu almacenamiento está encriptado. Se requiere contraseña para descifrarlo.",
"yes": "Sí",
"no": "No",
@ -25,7 +26,8 @@
"pick_file": "Escoge un archivo",
"enter_amount": "Ingresa la cantidad",
"qr_custom_input_button": "Pulsa 10 veces para ingresar una entrada personalizada",
"unlock": "Desbloquear"
"unlock": "Desbloquear",
"suggested": "Sugerido"
},
"azteco": {
"codeIs": "Tu código de cupón es",
@ -206,8 +208,10 @@
"performance_score": "Puntuación de rendimiento: {num}",
"run_performance_test": "Prueba de rendimiento",
"about_selftest": "Ejecutar auto-prueba",
"block_explorer_invalid_custom_url": "La URL proporcionada no es válida. Ingresa una URL válida que comience con http:// o https://.",
"about_selftest_electrum_disabled": "La autocomprobación no está disponible con el modo sin conexión de Electrum. Desactiva el modo sin conexión y vuelve a intentarlo. ",
"about_selftest_ok": "Todas las pruebas internas han pasado satisfactoriamente. La billetera funciona bien.",
"about_sm_github": "GitHub",
"about_sm_discord": "Servidor Discord",
"about_sm_telegram": "Chat de Telegram ",
@ -259,6 +263,9 @@
"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",
"block_explorer": "Explorador de bloques",
"block_explorer_preferred": "Utiliza el explorador de bloques preferido",
"block_explorer_error_saving_custom": "Error al guardar el explorador de bloques preferido",
"encrypt_title": "Seguridad",
"encrypt_tstorage": "Almacenamiento",
"encrypt_use": "Usar {type}",

79
models/blockExplorer.ts Normal file
View file

@ -0,0 +1,79 @@
// blockExplorer.ts
import DefaultPreference from 'react-native-default-preference';
export interface BlockExplorer {
key: string;
name: string;
url: string;
}
export const BLOCK_EXPLORERS: { [key: string]: BlockExplorer } = {
default: { key: 'default', name: 'Mempool.space', url: 'https://mempool.space' },
blockchair: { key: 'blockchair', name: 'Blockchair', url: 'https://blockchair.com/bitcoin' },
blockstream: { key: 'blockstream', name: 'Blockstream.info', url: 'https://blockstream.info' },
custom: { key: 'custom', name: 'Custom', url: '' }, // Custom URL will be handled separately
};
export const getBlockExplorersList = (): BlockExplorer[] => {
return Object.values(BLOCK_EXPLORERS);
};
export const normalizeUrl = (url: string): string => {
return url.replace(/\/+$/, '');
};
export const isValidUrl = (url: string): boolean => {
const pattern = /^(https?:\/\/)/;
return pattern.test(url);
};
export const findMatchingExplorerByDomain = (url: string): BlockExplorer | null => {
const domain = getDomain(url);
for (const explorer of Object.values(BLOCK_EXPLORERS)) {
if (getDomain(explorer.url) === domain) {
return explorer;
}
}
return null;
};
export const getDomain = (url: string): string => {
try {
const hostname = new URL(url).hostname;
return hostname.replace(/^www\./, '');
} catch {
return '';
}
};
const BLOCK_EXPLORER_STORAGE_KEY = 'blockExplorer';
export const saveBlockExplorer = async (url: string): Promise<boolean> => {
try {
await DefaultPreference.set(BLOCK_EXPLORER_STORAGE_KEY, url);
return true;
} catch (error) {
console.error('Error saving block explorer:', error);
return false;
}
};
export const removeBlockExplorer = async (): Promise<boolean> => {
try {
await DefaultPreference.clear(BLOCK_EXPLORER_STORAGE_KEY);
return true;
} catch (error) {
console.error('Error removing block explorer:', error);
return false;
}
};
export const getBlockExplorerUrl = async (): Promise<string> => {
try {
const url = await DefaultPreference.get(BLOCK_EXPLORER_STORAGE_KEY);
return url ?? BLOCK_EXPLORERS.default.url;
} catch (error) {
console.error('Error getting block explorer:', error);
return BLOCK_EXPLORERS.default.url;
}
};

View file

@ -21,9 +21,16 @@ import {
export type AddWalletStackParamList = {
AddWallet: undefined;
ImportWallet: undefined;
ImportWalletDiscovery: undefined;
ImportWalletDiscovery: {
importText: string;
askPassphrase: boolean;
searchAccounts: boolean;
};
ImportSpeed: undefined;
ImportCustomDerivationPath: undefined;
ImportCustomDerivationPath: {
importText: string;
password: string | undefined;
};
PleaseBackup: undefined;
PleaseBackupLNDHub: undefined;
ProvideEntropy: undefined;

View file

@ -31,6 +31,7 @@ import AddWalletStack from './AddWalletStack';
import AztecoRedeemStackRoot from './AztecoRedeemStack';
import {
AboutComponent,
BlockExplorerSettingsComponent,
CurrencyComponent,
DefaultViewComponent,
ElectrumSettingsComponent,
@ -304,6 +305,12 @@ const DetailViewStackScreensStack = () => {
component={NetworkSettingsComponent}
options={navigationStyle({ title: loc.settings.network })(theme)}
/>
<DetailViewStack.Screen
name="SettingsBlockExplorer"
component={BlockExplorerSettingsComponent}
options={navigationStyle({ title: loc.settings.block_explorer })(theme)}
/>
<DetailViewStack.Screen name="About" component={AboutComponent} options={navigationStyle({ title: loc.settings.about })(theme)} />
<DetailViewStack.Screen
name="DefaultView"

View file

@ -54,6 +54,7 @@ export type DetailViewStackParamList = {
About: undefined;
DefaultView: undefined;
ElectrumSettings: undefined;
SettingsBlockExplorer: undefined;
EncryptStorage: undefined;
Language: undefined;
LightningSettings: {

View file

@ -5,7 +5,7 @@ import { LazyLoadingIndicator } from './LazyLoadingIndicator';
// Define lazy imports
const WalletsAdd = lazy(() => import('../screen/wallets/Add'));
const ImportCustomDerivationPath = lazy(() => import('../screen/wallets/importCustomDerivationPath'));
const ImportWalletDiscovery = lazy(() => import('../screen/wallets/importDiscovery'));
const ImportWalletDiscovery = lazy(() => import('../screen/wallets/ImportWalletDiscovery'));
const ImportSpeed = lazy(() => import('../screen/wallets/importSpeed'));
const ImportWallet = lazy(() => import('../screen/wallets/import'));
const PleaseBackup = lazy(() => import('../screen/wallets/PleaseBackup'));

View file

@ -3,6 +3,7 @@ import React, { lazy, Suspense } from 'react';
import Currency from '../screen/settings/Currency';
import Language from '../screen/settings/Language';
import { LazyLoadingIndicator } from './LazyLoadingIndicator'; // Assume you have this component for loading indication
import SettingsBlockExplorer from '../screen/settings/SettingsBlockExplorer';
const Settings = lazy(() => import('../screen/settings/Settings'));
const GeneralSettings = lazy(() => import('../screen/settings/GeneralSettings'));
@ -46,6 +47,12 @@ export const NetworkSettingsComponent = () => (
</Suspense>
);
export const BlockExplorerSettingsComponent = () => (
<Suspense fallback={<LazyLoadingIndicator />}>
<SettingsBlockExplorer />
</Suspense>
);
export const AboutComponent = () => (
<Suspense fallback={<LazyLoadingIndicator />}>
<About />

View file

@ -35,7 +35,7 @@ export type SendDetailsStackParamList = {
};
PsbtWithHardwareWallet: {
memo?: string;
fromWallet: TWallet;
walletID: string;
launchedBy?: string;
psbt?: Psbt;
txhex?: string;
@ -79,18 +79,4 @@ export type SendDetailsStackParamList = {
PaymentCodeList: {
walletID: string;
};
ScanQRCodeRoot: {
screen: string;
params: {
isLoading?: boolean;
cameraStatusGranted?: boolean;
backdoorPressed?: boolean;
launchedBy?: string;
urTotal?: number;
urHave?: number;
backdoorText?: string;
showFileImportButton?: boolean;
onBarScanned: (data: string) => void;
};
};
};

174
package-lock.json generated
View file

@ -21,8 +21,8 @@
"@react-native-clipboard/clipboard": "1.14.2",
"@react-native-community/push-notification-ios": "1.11.0",
"@react-native-menu/menu": "https://github.com/BlueWallet/menu.git#8c6004b",
"@react-native/gradle-plugin": "^0.75.3",
"@react-native/metro-config": "0.75.3",
"@react-native/gradle-plugin": "^0.75.4",
"@react-native/metro-config": "0.75.4",
"@react-navigation/drawer": "6.7.2",
"@react-navigation/native": "6.1.18",
"@react-navigation/native-stack": "6.11.0",
@ -47,7 +47,7 @@
"coinselect": "3.1.13",
"crypto-js": "4.2.0",
"dayjs": "1.11.13",
"detox": "20.27.2",
"detox": "20.27.3",
"ecpair": "2.0.1",
"ecurve": "1.0.6",
"electrum-client": "github:BlueWallet/rn-electrum-client#1bfe3cc",
@ -61,7 +61,7 @@
"prop-types": "15.8.1",
"react": "18.3.1",
"react-localization": "github:BlueWallet/react-localization#ae7969a",
"react-native": "0.75.3",
"react-native": "0.75.4",
"react-native-biometrics": "3.0.1",
"react-native-blue-crypto": "github:BlueWallet/react-native-blue-crypto#3cb5442",
"react-native-camera-kit": "13.0.0",
@ -89,7 +89,7 @@
"react-native-quick-actions": "0.3.13",
"react-native-randombytes": "3.6.1",
"react-native-rate": "1.2.12",
"react-native-reanimated": "3.15.3",
"react-native-reanimated": "3.15.4",
"react-native-safe-area-context": "4.11.0",
"react-native-screens": "3.34.0",
"react-native-secure-key-store": "github:BlueWallet/react-native-secure-key-store#2076b4849e88aa0a78e08bfbb4ce3923e0925cbc",
@ -112,11 +112,11 @@
"@babel/core": "^7.20.0",
"@babel/runtime": "^7.20.0",
"@jest/reporters": "^27.5.1",
"@react-native/babel-preset": "^0.75.3",
"@react-native/eslint-config": "^0.75.3",
"@react-native/js-polyfills": "^0.75.3",
"@react-native/metro-babel-transformer": "^0.75.3",
"@react-native/typescript-config": "^0.75.3",
"@react-native/babel-preset": "^0.75.4",
"@react-native/eslint-config": "^0.75.4",
"@react-native/js-polyfills": "^0.75.4",
"@react-native/metro-babel-transformer": "^0.75.4",
"@react-native/typescript-config": "^0.75.4",
"@types/bip38": "^3.1.2",
"@types/bs58check": "^2.1.0",
"@types/create-hash": "^1.2.2",
@ -5610,30 +5610,30 @@
}
},
"node_modules/@react-native/assets-registry": {
"version": "0.75.3",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.75.3.tgz",
"integrity": "sha512-i7MaRbYR06WdpJWv3a0PQ2ScFBUeevwcJ0tVopnFwTg0tBWp3NFEMDIcU8lyXVy9Y59WmrP1V2ROaRDaPiESgg==",
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.75.4.tgz",
"integrity": "sha512-WX6/LNHwyjislSFM+h3qQjBiPaXXPJW5ZV4TdgNKb6QOPO0g1KGYRQj44cI2xSpZ3fcWrvQFZfQgSMbVK9Sg7A==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@react-native/babel-plugin-codegen": {
"version": "0.75.3",
"resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.75.3.tgz",
"integrity": "sha512-8JmXEKq+Efb9AffsV48l8gmKe/ZQ2PbBygZjHdIf8DNZZhO/z5mt27J4B43MWNdp5Ww1l59T0mEaf8l/uywQUg==",
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.75.4.tgz",
"integrity": "sha512-gu5ZRIdr7+ufi09DJROhfDtbF4biTnCDJqtqcmtsku4cXOXPHE36QbC/vAmKEZ0PMPURBI8lwF2wfaeHLn7gig==",
"license": "MIT",
"dependencies": {
"@react-native/codegen": "0.75.3"
"@react-native/codegen": "0.75.4"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@react-native/babel-preset": {
"version": "0.75.3",
"resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.75.3.tgz",
"integrity": "sha512-VZQkQEj36DKEGApXFYdVcFtqdglbnoVr7aOZpjffURSgPcIA9vWTm1b+OL4ayOaRZXTZKiDBNQCXvBX5E5AgQg==",
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.75.4.tgz",
"integrity": "sha512-UtyYCDJ3rZIeggyFEfh/q5t/FZ5a1h9F8EI37Nbrwyk/OKPH+1XS4PbHROHJzBARlJwOAfmT75+ovYUO0eakJA==",
"license": "MIT",
"dependencies": {
"@babel/core": "^7.20.0",
@ -5678,7 +5678,7 @@
"@babel/plugin-transform-typescript": "^7.5.0",
"@babel/plugin-transform-unicode-regex": "^7.0.0",
"@babel/template": "^7.0.0",
"@react-native/babel-plugin-codegen": "0.75.3",
"@react-native/babel-plugin-codegen": "0.75.4",
"babel-plugin-transform-flow-enums": "^0.0.2",
"react-refresh": "^0.14.0"
},
@ -5690,9 +5690,9 @@
}
},
"node_modules/@react-native/codegen": {
"version": "0.75.3",
"resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.75.3.tgz",
"integrity": "sha512-I0bz5jwOkiR7vnhYLGoV22RGmesErUg03tjsCiQgmsMpbyCYumudEtLNN5+DplHGK56bu8KyzBqKkWXGSKSCZQ==",
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.75.4.tgz",
"integrity": "sha512-0FplNAD/S5FUvm8YIn6uyarOcP4jdJPqWz17K4a/Gp2KSsG/JJKEskX3aj5wpePzVfNQl3WyvBJ0whODdCocIA==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.20.0",
@ -5712,15 +5712,15 @@
}
},
"node_modules/@react-native/community-cli-plugin": {
"version": "0.75.3",
"resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.75.3.tgz",
"integrity": "sha512-njsYm+jBWzfLcJcxavAY5QFzYTrmPtjbxq/64GSqwcQYzy9qAkI7LNTK/Wprq1I/4HOuHJO7Km+EddCXB+ByRQ==",
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.75.4.tgz",
"integrity": "sha512-k/hevYPjEpW0MNVVyb3v9PJosOP+FzenS7+oqYNLXdEmgTnGHrAtYX9ABrJJgzeJt7I6g8g+RDvm8PSE+tnM5w==",
"license": "MIT",
"dependencies": {
"@react-native-community/cli-server-api": "14.1.0",
"@react-native-community/cli-tools": "14.1.0",
"@react-native/dev-middleware": "0.75.3",
"@react-native/metro-babel-transformer": "0.75.3",
"@react-native/dev-middleware": "0.75.4",
"@react-native/metro-babel-transformer": "0.75.4",
"chalk": "^4.0.0",
"execa": "^5.1.1",
"metro": "^0.80.3",
@ -5804,22 +5804,22 @@
}
},
"node_modules/@react-native/debugger-frontend": {
"version": "0.75.3",
"resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.75.3.tgz",
"integrity": "sha512-99bLQsUwsxUMNR7Wa9eV2uyR38yfd6mOEqfN+JIm8/L9sKA926oh+CZkjDy1M8RmCB6spB5N9fVFVkrVdf2yFA==",
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.75.4.tgz",
"integrity": "sha512-QfGurR5hV6bhMPn/6VxS2RomYrPRFGwA03jJr+zKyWHnxDAu5jOqYVyKAktIIbhYe5sPp78QVl1ZYuhcnsRbEw==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=18"
}
},
"node_modules/@react-native/dev-middleware": {
"version": "0.75.3",
"resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.75.3.tgz",
"integrity": "sha512-h2/6+UGmeMWjnT43axy27jNqoDRsE1C1qpjRC3sYpD4g0bI0jSTkY1kAgj8uqGGXLnHXiHOtjLDGdbAgZrsPaA==",
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.75.4.tgz",
"integrity": "sha512-UhyBeQOG2wNcvrUGw3+IBrHBk/lIu7hHGmWt4j8W9Aqv9BwktHKkPyko+5A1yoUeO1O/VDnHWYqWeOejcA9wpQ==",
"license": "MIT",
"dependencies": {
"@isaacs/ttlcache": "^1.4.1",
"@react-native/debugger-frontend": "0.75.3",
"@react-native/debugger-frontend": "0.75.4",
"chrome-launcher": "^0.15.2",
"chromium-edge-launcher": "^0.2.0",
"connect": "^3.6.5",
@ -5876,15 +5876,15 @@
}
},
"node_modules/@react-native/eslint-config": {
"version": "0.75.3",
"resolved": "https://registry.npmjs.org/@react-native/eslint-config/-/eslint-config-0.75.3.tgz",
"integrity": "sha512-BUkgQ3+irVZFfikIX3JgQQut/LABcumMZD3lzWRpf1hpbsEsXmYLo3u9qpfyYsavnqVGtWg8bLZDDGG6lmQBhA==",
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/eslint-config/-/eslint-config-0.75.4.tgz",
"integrity": "sha512-3KBHYwp4HnBdaCFx9KDPvQY+sGrv5fHX2qDkXGKmN3uYBz+zfnMQXTiht6OuBbWULUF0y0o8m+uH1yYAn/V9mw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.20.0",
"@babel/eslint-parser": "^7.20.0",
"@react-native/eslint-plugin": "0.75.3",
"@react-native/eslint-plugin": "0.75.4",
"@typescript-eslint/eslint-plugin": "^7.1.1",
"@typescript-eslint/parser": "^7.1.1",
"eslint-config-prettier": "^8.5.0",
@ -6074,9 +6074,9 @@
}
},
"node_modules/@react-native/eslint-plugin": {
"version": "0.75.3",
"resolved": "https://registry.npmjs.org/@react-native/eslint-plugin/-/eslint-plugin-0.75.3.tgz",
"integrity": "sha512-p0BwuqflumZtUJ8VUt5YQPsYnxrwfHJZCTQ0N1X+4B8aoBNIKxtfzeIqJsSz/GvM86HGgGnd/nyxEo8IwkPo7Q==",
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/eslint-plugin/-/eslint-plugin-0.75.4.tgz",
"integrity": "sha512-1kEZzC8UKi3baHnH7tBVCNpF4aoAmT7g7hEa5/rtZ+Z7vcpaxeY6wjNYt3j02Z9n310yX0NKDJox30CqvzEvsg==",
"dev": true,
"license": "MIT",
"engines": {
@ -6084,31 +6084,31 @@
}
},
"node_modules/@react-native/gradle-plugin": {
"version": "0.75.3",
"resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.75.3.tgz",
"integrity": "sha512-mSfa/Mq/AsALuG/kvXz5ECrc6HdY5waMHal2sSfa8KA0Gt3JqYQVXF9Pdwd4yR5ClPZDI2HRa1tdE8GVlhMvPA==",
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.75.4.tgz",
"integrity": "sha512-kKTmw7cF7p1raT30DC0L6N+xiVXN7dlRy0J+hYPiCRRVHplwgvyS7pszjxfzwXmHFqOxwpxQVI3du8opsma1Mg==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@react-native/js-polyfills": {
"version": "0.75.3",
"resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.75.3.tgz",
"integrity": "sha512-+JVFJ351GSJT3V7LuXscMqfnpR/UxzsAjbBjfAHBR3kqTbVqrAmBccqPCA3NLzgb/RY8khLJklwMUVlWrn8iFg==",
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.75.4.tgz",
"integrity": "sha512-NF5ID5FjcVHBYk1LQ4JMRjPmxBWEo4yoqW1m6vGOQZPT8D5Qs9afgx3f7gQatxbn3ivMh0FVbLW0zBx6LyxEzA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@react-native/metro-babel-transformer": {
"version": "0.75.3",
"resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.75.3.tgz",
"integrity": "sha512-gDlEl6C2mwQPLxFOR+yla5MpJpDPNOFD6J5Hd9JM9+lOdUq6MNujh1Xn4ZMvglW7rfViq3nMjg4xPQeGUhDG+w==",
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.75.4.tgz",
"integrity": "sha512-O0WMW/K8Ny/MAAeRebqGEQhrbzcioxcPHZtos+EH2hWeBTEKHQV8fMYYxfYDabpr392qdhSBwg3LlXUD4U3PXQ==",
"license": "MIT",
"dependencies": {
"@babel/core": "^7.20.0",
"@react-native/babel-preset": "0.75.3",
"@react-native/babel-preset": "0.75.4",
"hermes-parser": "0.22.0",
"nullthrows": "^1.1.1"
},
@ -6120,13 +6120,13 @@
}
},
"node_modules/@react-native/metro-config": {
"version": "0.75.3",
"resolved": "https://registry.npmjs.org/@react-native/metro-config/-/metro-config-0.75.3.tgz",
"integrity": "sha512-BKvJGCzKMtGPfKU4v8+16xtv6iXPbnSeeD6ONvzW0Hm9amPxAP9GGweiUTwnOXUqt9XcfRhytZosiOmGa39Wqg==",
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/metro-config/-/metro-config-0.75.4.tgz",
"integrity": "sha512-gIIVlPUtZ1UKCxMJRtG88FoWS5REbI4YUmiyoM8eBUA85Zvk27b67iBX5Lkuxg8FGc7t9tjWQRpVGs2IK5uZpQ==",
"license": "MIT",
"dependencies": {
"@react-native/js-polyfills": "0.75.3",
"@react-native/metro-babel-transformer": "0.75.3",
"@react-native/js-polyfills": "0.75.4",
"@react-native/metro-babel-transformer": "0.75.4",
"metro-config": "^0.80.3",
"metro-runtime": "^0.80.3"
},
@ -6135,22 +6135,22 @@
}
},
"node_modules/@react-native/normalize-colors": {
"version": "0.75.3",
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.75.3.tgz",
"integrity": "sha512-3mhF8AJFfIN0E5bEs/DQ4U2LzMJYm+FPSwY5bJ1DZhrxW1PFAh24bAPrSd8PwS0iarQ7biLdr1lWf/8LFv8pDA==",
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.75.4.tgz",
"integrity": "sha512-90QrQDLg0/k9xqYesaKuIkayOSjD+FKa0hsHollbwT5h3kuGMY+lU7UZxnb8tU55Y1PKdvjYxqQsYWI/ql79zA==",
"license": "MIT"
},
"node_modules/@react-native/typescript-config": {
"version": "0.75.3",
"resolved": "https://registry.npmjs.org/@react-native/typescript-config/-/typescript-config-0.75.3.tgz",
"integrity": "sha512-yekwhjG9lDvib+3jYwagEXcyTJ4mEryDLCoQINWCwy+7GtMx2BrKweHc4Phcc1dqmK1U/XMzVUXF3X3hc+FVPA==",
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/typescript-config/-/typescript-config-0.75.4.tgz",
"integrity": "sha512-0849BqSIDGYltqMbniZg1MvDSFO5KMQsmIKpGzioTm3+SF73Ec1LihA1CtpJyZFonEclKXEIawRTNh2bxGgJJQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@react-native/virtualized-lists": {
"version": "0.75.3",
"resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.75.3.tgz",
"integrity": "sha512-cTLm7k7Y//SvV8UK8esrDHEw5OrwwSJ4Fqc3x52Imi6ROuhshfGIPFwhtn4pmAg9nWHzHwwqiJ+9hCSVnXXX+g==",
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.75.4.tgz",
"integrity": "sha512-iEauRiXjvWG/iOH8bV+9MfepCS+72cuL5rhkrenYZS0NUnDcNjF+wtaoS9+Gx5z1UJOfEXxSmyXRtQJZne8SnA==",
"license": "MIT",
"dependencies": {
"invariant": "^2.2.4",
@ -9772,9 +9772,9 @@
}
},
"node_modules/detox": {
"version": "20.27.2",
"resolved": "https://registry.npmjs.org/detox/-/detox-20.27.2.tgz",
"integrity": "sha512-cC6S40v7ix+uA5jYzG8eazSs7YtOWgc2aCwWLZIIzfE5Kvo0gfHgtqeRhrYWCMZaj/irKKs39h2B070oNQOIrA==",
"version": "20.27.3",
"resolved": "https://registry.npmjs.org/detox/-/detox-20.27.3.tgz",
"integrity": "sha512-bMAjL+6dQLF75DWbJb0gTp6/cOnpFqu5CU+VcE48ZIRLBQp5KD/ppk2zvx/B8k+CeQJjWFGs8exfpVuOBJUTpw==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@ -9784,7 +9784,7 @@
"caf": "^15.0.1",
"chalk": "^4.0.0",
"child-process-promise": "^2.2.0",
"detox-copilot": "^0.0.9",
"detox-copilot": "^0.0.14",
"execa": "^5.1.1",
"find-up": "^5.0.0",
"fs-extra": "^11.0.0",
@ -9831,9 +9831,9 @@
}
},
"node_modules/detox-copilot": {
"version": "0.0.9",
"resolved": "https://registry.npmjs.org/detox-copilot/-/detox-copilot-0.0.9.tgz",
"integrity": "sha512-Wk2fuisD8EH+349b0ysNWvZ7UEsThAChbYFlLqOR1jWkDaonEvgf6IOUlmxjvyTl9ENtl8ckd1U7k94yCBYwqw==",
"version": "0.0.14",
"resolved": "https://registry.npmjs.org/detox-copilot/-/detox-copilot-0.0.14.tgz",
"integrity": "sha512-XzruvuEVnX/sGkkDLDDLTGuwyoS0qLk+g5psDWgpLrnFd9w4QA0I65G5dwPzcK68lYaOGb0FThXv3fmD37/Mlg==",
"license": "MIT"
},
"node_modules/detox/node_modules/ansi-styles": {
@ -20467,22 +20467,22 @@
}
},
"node_modules/react-native": {
"version": "0.75.3",
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.75.3.tgz",
"integrity": "sha512-+Ne6u5H+tPo36sme19SCd1u2UID2uo0J/XzAJarxmrDj4Nsdi44eyUDKtQHmhgxjRGsuVJqAYrMK0abLSq8AHw==",
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.75.4.tgz",
"integrity": "sha512-Jehg4AMNIAXu9cn0/1jbTCoNg3tc+t6EekwucCalN8YoRmxGd/PY6osQTI/5fSAM40JQ4O8uv8Qg09Ycpb5sxQ==",
"license": "MIT",
"dependencies": {
"@jest/create-cache-key-function": "^29.6.3",
"@react-native-community/cli": "14.1.0",
"@react-native-community/cli-platform-android": "14.1.0",
"@react-native-community/cli-platform-ios": "14.1.0",
"@react-native/assets-registry": "0.75.3",
"@react-native/codegen": "0.75.3",
"@react-native/community-cli-plugin": "0.75.3",
"@react-native/gradle-plugin": "0.75.3",
"@react-native/js-polyfills": "0.75.3",
"@react-native/normalize-colors": "0.75.3",
"@react-native/virtualized-lists": "0.75.3",
"@react-native/assets-registry": "0.75.4",
"@react-native/codegen": "0.75.4",
"@react-native/community-cli-plugin": "0.75.4",
"@react-native/gradle-plugin": "0.75.4",
"@react-native/js-polyfills": "0.75.4",
"@react-native/normalize-colors": "0.75.4",
"@react-native/virtualized-lists": "0.75.4",
"abort-controller": "^3.0.0",
"anser": "^1.4.9",
"ansi-regex": "^5.0.0",
@ -20882,9 +20882,9 @@
}
},
"node_modules/react-native-reanimated": {
"version": "3.15.3",
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.15.3.tgz",
"integrity": "sha512-5QBk/7PZvZ98Adxm4MRyglwzsRzReTQIe4Hd2wbBBAZ68IC4OYKvsc8cPEjgx3/1mG8HgHFYhbcDe5U2RjeFqw==",
"version": "3.15.4",
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.15.4.tgz",
"integrity": "sha512-jcpHE+MnsvSbClhHgAFoken7SnaHrUJ5gVA8BUw8S1j6rkrw2VzRpht6cxn14NlqYx5ytjfG9IXJDOzq8tFvfw==",
"license": "MIT",
"dependencies": {
"@babel/plugin-transform-arrow-functions": "^7.0.0-0",

View file

@ -10,11 +10,11 @@
"@babel/core": "^7.20.0",
"@babel/runtime": "^7.20.0",
"@jest/reporters": "^27.5.1",
"@react-native/babel-preset": "^0.75.3",
"@react-native/eslint-config": "^0.75.3",
"@react-native/js-polyfills": "^0.75.3",
"@react-native/metro-babel-transformer": "^0.75.3",
"@react-native/typescript-config": "^0.75.3",
"@react-native/babel-preset": "^0.75.4",
"@react-native/eslint-config": "^0.75.4",
"@react-native/js-polyfills": "^0.75.4",
"@react-native/metro-babel-transformer": "^0.75.4",
"@react-native/typescript-config": "^0.75.4",
"@types/bip38": "^3.1.2",
"@types/bs58check": "^2.1.0",
"@types/create-hash": "^1.2.2",
@ -85,8 +85,8 @@
"@react-native-clipboard/clipboard": "1.14.2",
"@react-native-community/push-notification-ios": "1.11.0",
"@react-native-menu/menu": "https://github.com/BlueWallet/menu.git#8c6004b",
"@react-native/gradle-plugin": "^0.75.3",
"@react-native/metro-config": "0.75.3",
"@react-native/gradle-plugin": "^0.75.4",
"@react-native/metro-config": "0.75.4",
"@react-navigation/drawer": "6.7.2",
"@react-navigation/native": "6.1.18",
"@react-navigation/native-stack": "6.11.0",
@ -111,7 +111,7 @@
"coinselect": "3.1.13",
"crypto-js": "4.2.0",
"dayjs": "1.11.13",
"detox": "20.27.2",
"detox": "20.27.3",
"ecpair": "2.0.1",
"ecurve": "1.0.6",
"electrum-client": "github:BlueWallet/rn-electrum-client#1bfe3cc",
@ -125,7 +125,7 @@
"prop-types": "15.8.1",
"react": "18.3.1",
"react-localization": "github:BlueWallet/react-localization#ae7969a",
"react-native": "0.75.3",
"react-native": "0.75.4",
"react-native-biometrics": "3.0.1",
"react-native-blue-crypto": "github:BlueWallet/react-native-blue-crypto#3cb5442",
"react-native-camera-kit": "13.0.0",
@ -153,7 +153,7 @@
"react-native-quick-actions": "0.3.13",
"react-native-randombytes": "3.6.1",
"react-native-rate": "1.2.12",
"react-native-reanimated": "3.15.3",
"react-native-reanimated": "3.15.4",
"react-native-safe-area-context": "4.11.0",
"react-native-screens": "3.34.0",
"react-native-secure-key-store": "github:BlueWallet/react-native-secure-key-store#2076b4849e88aa0a78e08bfbb4ce3923e0925cbc",

View file

@ -25,7 +25,7 @@ import AmountInput from '../../components/AmountInput';
import Button from '../../components/Button';
import { useTheme } from '../../components/themes';
import { presentWalletExportReminder } from '../../helpers/presentWalletExportReminder';
import { requestCameraAuthorization } from '../../helpers/scan-qr';
import { scanQrHelper } from '../../helpers/scan-qr';
import loc, { formatBalance, formatBalancePlain, formatBalanceWithoutSuffix } from '../../loc';
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
import * as NavigationService from '../../NavigationService';
@ -325,16 +325,8 @@ const LNDCreateInvoice = () => {
};
const navigateToScanQRCode = () => {
requestCameraAuthorization().then(() => {
NavigationService.navigate('ScanQRCodeRoot', {
screen: 'ScanQRCode',
params: {
onBarScanned: processLnurl,
launchedBy: name,
},
});
Keyboard.dismiss();
});
scanQrHelper(name, true, processLnurl);
Keyboard.dismiss();
};
const renderScanClickable = () => {

View file

@ -35,9 +35,8 @@ import { SuccessView } from '../send/success';
import { useStorage } from '../../hooks/context/useStorage';
import { HandOffActivityType } from '../../components/types';
import SegmentedControl from '../../components/SegmentControl';
import ToolTipMenu from '../../components/TooltipMenu';
import { Icon } from '@rneui/themed';
import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
import HeaderMenuButton from '../../components/HeaderMenuButton';
const segmentControlValues = [loc.wallets.details_address, loc.bip47.payment_code];
@ -167,12 +166,9 @@ const ReceiveDetails = () => {
}, [onEnablePaymentsCodeSwitchValue]);
const HeaderRight = useMemo(
() => (
<ToolTipMenu isButton isMenuPrimaryAction onPressMenuItem={onPressMenuItem} actions={[toolTipActions]}>
<Icon size={22} name="more-horiz" type="material" color={colors.foregroundColor} />
</ToolTipMenu>
),
[colors.foregroundColor, onPressMenuItem, toolTipActions],
() => <HeaderMenuButton actions={toolTipActions} onPressMenuItem={onPressMenuItem} />,
[onPressMenuItem, toolTipActions],
);
const handleClose = useCallback(() => {

View file

@ -23,6 +23,7 @@ import { useTheme } from '../../components/themes';
import { scanQrHelper } from '../../helpers/scan-qr';
import loc from '../../loc';
import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList';
import { useSettings } from '../../hooks/context/useSettings';
const BROADCAST_RESULT = Object.freeze({
none: 'Input transaction hex',
@ -39,6 +40,7 @@ const Broadcast: React.FC = () => {
const [txHex, setTxHex] = useState<string | undefined>();
const { colors } = useTheme();
const [broadcastResult, setBroadcastResult] = useState<string>(BROADCAST_RESULT.none);
const { selectedBlockExplorer } = useSettings();
const stylesHooks = StyleSheet.create({
input: {
@ -158,13 +160,13 @@ const Broadcast: React.FC = () => {
<BlueSpacing20 />
</BlueCard>
)}
{BROADCAST_RESULT.success === broadcastResult && tx && <SuccessScreen tx={tx} />}
{BROADCAST_RESULT.success === broadcastResult && tx && <SuccessScreen tx={tx} url={`${selectedBlockExplorer}/tx/${tx}`} />}
</View>
</SafeArea>
);
};
const SuccessScreen: React.FC<{ tx: string }> = ({ tx }) => {
const SuccessScreen: React.FC<{ tx: string; url: string }> = ({ tx, url }) => {
if (!tx) {
return null;
}
@ -177,7 +179,7 @@ const SuccessScreen: React.FC<{ tx: string }> = ({ tx }) => {
<BlueSpacing20 />
<BlueTextCentered>{loc.settings.success_transaction_broadcasted}</BlueTextCentered>
<BlueSpacing10 />
<BlueButtonLink title={loc.settings.open_link_in_explorer} onPress={() => Linking.openURL(`https://mempool.space/tx/${tx}`)} />
<BlueButtonLink title={loc.settings.open_link_in_explorer} onPress={() => Linking.openURL(url)} />
</View>
</BlueCard>
</View>

View file

@ -24,7 +24,6 @@ import {
import DocumentPicker from 'react-native-document-picker';
import { Icon } from '@rneui/themed';
import RNFS from 'react-native-fs';
import { btcToSatoshi, fiatToBTC } from '../../blue_modules/currency';
import * as fs from '../../blue_modules/fs';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
@ -40,8 +39,7 @@ import Button from '../../components/Button';
import CoinsSelected from '../../components/CoinsSelected';
import InputAccessoryAllFunds, { InputAccessoryAllFundsAccessoryViewID } from '../../components/InputAccessoryAllFunds';
import { useTheme } from '../../components/themes';
import ToolTipMenu from '../../components/TooltipMenu';
import { requestCameraAuthorization, scanQrHelper } from '../../helpers/scan-qr';
import { scanQrHelper } from '../../helpers/scan-qr';
import loc, { formatBalance, formatBalanceWithoutSuffix } from '../../loc';
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
import NetworkTransactionFees, { NetworkTransactionFee } from '../../models/networkTransactionFees';
@ -58,6 +56,7 @@ import SelectFeeModal from '../../components/SelectFeeModal';
import { useKeyboard } from '../../hooks/useKeyboard';
import { DismissKeyboardInputAccessory, DismissKeyboardInputAccessoryViewID } from '../../components/DismissKeyboardInputAccessory';
import ActionSheet from '../ActionSheet';
import HeaderMenuButton from '../../components/HeaderMenuButton';
interface IPaymentDestinations {
address: string; // btc address or payment code
@ -601,7 +600,6 @@ const SendDetails = () => {
if (tx && routeParams.launchedBy && psbt) {
console.warn('navigating back to ', routeParams.launchedBy);
feeModalRef.current?.dismiss();
// @ts-ignore idk how to fix FIXME?
@ -609,14 +607,12 @@ const SendDetails = () => {
}
if (wallet?.type === WatchOnlyWallet.type) {
feeModalRef.current?.dismiss();
// watch-only wallets with enabled HW wallet support have different flow. we have to show PSBT to user as QR code
// so he can scan it and sign it. then we have to scan it back from user (via camera and QR code), and ask
// user whether he wants to broadcast it
navigation.navigate('PsbtWithHardwareWallet', {
memo: transactionMemo,
fromWallet: wallet,
walletID: wallet.getID(),
psbt,
launchedBy: routeParams.launchedBy,
});
@ -625,8 +621,6 @@ const SendDetails = () => {
}
if (wallet?.type === MultisigHDWallet.type) {
feeModalRef.current?.dismiss();
navigation.navigate('PsbtMultisig', {
memo: transactionMemo,
psbtBase64: psbt.toBase64(),
@ -651,7 +645,6 @@ const SendDetails = () => {
// (ez can be the case for single-address wallet when doing self-payment for consolidation)
recipients = outputs;
}
feeModalRef.current?.dismiss();
navigation.navigate('Confirm', {
fee: new BigNumber(fee).dividedBy(100000000).toNumber(),
@ -680,22 +673,13 @@ const SendDetails = () => {
*
* @returns {Promise<void>}
*/
const importQrTransaction = () => {
const importQrTransaction = async () => {
if (wallet?.type !== WatchOnlyWallet.type) {
return presentAlert({ title: loc.errors.error, message: 'Importing transaction in non-watchonly wallet (this should never happen)' });
}
requestCameraAuthorization().then(() => {
feeModalRef.current?.dismiss();
navigation.navigate('ScanQRCodeRoot', {
screen: 'ScanQRCode',
params: {
onBarScanned: importQrTransactionOnBarScanned,
showFileImportButton: false,
},
});
});
const data = await scanQrHelper(route.name, true);
importQrTransactionOnBarScanned(data);
};
const importQrTransactionOnBarScanned = (ret: any) => {
@ -708,8 +692,6 @@ const SendDetails = () => {
// this looks like NOT base64, so maybe its transaction's hex
// we dont support it in this flow
} else {
feeModalRef.current?.dismiss();
// psbt base64?
// we construct PSBT object and pass to next screen
@ -718,7 +700,7 @@ const SendDetails = () => {
navigation.navigate('PsbtWithHardwareWallet', {
memo: transactionMemo,
fromWallet: wallet,
walletID: wallet.getID(),
psbt,
});
setIsLoading(false);
@ -752,7 +734,7 @@ const SendDetails = () => {
const file = await RNFS.readFile(res.uri, 'ascii');
const psbt = bitcoin.Psbt.fromBase64(file);
const txhex = psbt.extractTransaction().toHex();
navigation.navigate('PsbtWithHardwareWallet', { memo: transactionMemo, fromWallet: wallet, txhex });
navigation.navigate('PsbtWithHardwareWallet', { memo: transactionMemo, walletID: wallet.getID(), txhex });
setIsLoading(false);
return;
@ -763,7 +745,7 @@ const SendDetails = () => {
// so user can do smth with it:
const file = await RNFS.readFile(res.uri, 'ascii');
const psbt = bitcoin.Psbt.fromBase64(file);
navigation.navigate('PsbtWithHardwareWallet', { memo: transactionMemo, fromWallet: wallet, psbt });
navigation.navigate('PsbtWithHardwareWallet', { memo: transactionMemo, walletID: wallet.getID(), psbt });
setIsLoading(false);
return;
@ -772,7 +754,7 @@ const SendDetails = () => {
if (DeeplinkSchemaMatch.isTXNFile(res.uri)) {
// plain text file with txhex ready to broadcast
const file = (await RNFS.readFile(res.uri, 'ascii')).replace('\n', '').replace('\r', '');
navigation.navigate('PsbtWithHardwareWallet', { memo: transactionMemo, fromWallet: wallet, txhex: file });
navigation.navigate('PsbtWithHardwareWallet', { memo: transactionMemo, walletID: wallet.getID(), txhex: file });
setIsLoading(false);
return;
@ -853,15 +835,8 @@ const SendDetails = () => {
};
const importTransactionMultisigScanQr = async () => {
await requestCameraAuthorization().then(() => {
navigation.navigate('ScanQRCodeRoot', {
screen: 'ScanQRCode',
params: {
onBarScanned,
showFileImportButton: true,
},
});
});
const data = await scanQrHelper(route.name, true);
onBarScanned(data);
};
const handleAddRecipient = () => {
@ -1055,18 +1030,7 @@ const SendDetails = () => {
const setHeaderRightOptions = () => {
navigation.setOptions({
// eslint-disable-next-line react/no-unstable-nested-components
headerRight: () => (
<ToolTipMenu
disabled={isLoading}
isButton
isMenuPrimaryAction
onPressMenuItem={headerRightOnPress}
actions={headerRightActions()}
testID="advancedOptionsMenuButton"
>
<Icon size={22} name="more-horiz" type="material" color={colors.foregroundColor} style={styles.advancedOptions} />
</ToolTipMenu>
),
headerRight: () => <HeaderMenuButton disabled={isLoading} onPressMenuItem={headerRightOnPress} actions={headerRightActions()} />,
});
};
@ -1185,8 +1149,6 @@ const SendDetails = () => {
accessibilityRole="button"
style={styles.selectTouch}
onPress={() => {
feeModalRef.current?.dismiss();
navigation.navigate('SelectWallet', { chainType: Chain.ONCHAIN });
}}
>
@ -1499,11 +1461,6 @@ const styles = StyleSheet.create({
alignItems: 'center',
paddingHorizontal: 10,
},
advancedOptions: {
minWidth: 40,
height: 40,
justifyContent: 'center',
},
frozenContainer: {
flexDirection: 'row',
justifyContent: 'center',

View file

@ -5,7 +5,7 @@ import { BlueButtonLink, BlueCard, BlueSpacing10, BlueSpacing20, BlueText } from
import Button from '../../components/Button';
import SafeArea from '../../components/SafeArea';
import { useTheme } from '../../components/themes';
import { requestCameraAuthorization } from '../../helpers/scan-qr';
import { scanQrHelper } from '../../helpers/scan-qr';
import loc from '../../loc';
import { useStorage } from '../../hooks/context/useStorage';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
@ -57,16 +57,7 @@ const IsItMyAddress = () => {
};
const importScan = () => {
requestCameraAuthorization().then(() => {
navigate('ScanQRCodeRoot', {
screen: 'ScanQRCode',
params: {
launchedBy: name,
onBarScanned,
showFileImportButton: true,
},
});
});
scanQrHelper(name, true, onBarScanned);
};
const clearAddressInput = () => {

View file

@ -60,7 +60,7 @@ const PsbtMultisigQRCode = () => {
};
const openScanner = async () => {
const scanned = await scanQrHelper(name, true, undefined);
const scanned = await scanQrHelper(name, true);
onBarScanned({ data: scanned });
};

View file

@ -15,18 +15,19 @@ import { DynamicQRCode } from '../../components/DynamicQRCode';
import SaveFileButton from '../../components/SaveFileButton';
import { SecondButton } from '../../components/SecondButton';
import { useTheme } from '../../components/themes';
import { requestCameraAuthorization } from '../../helpers/scan-qr';
import { scanQrHelper } from '../../helpers/scan-qr';
import { useBiometrics, unlockWithBiometrics } from '../../hooks/useBiometrics';
import loc from '../../loc';
import { useStorage } from '../../hooks/context/useStorage';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
const PsbtWithHardwareWallet = () => {
const { txMetadata, fetchAndSaveWalletTransactions, isElectrumDisabled } = useStorage();
const { txMetadata, fetchAndSaveWalletTransactions, isElectrumDisabled, wallets } = useStorage();
const { isBiometricUseCapableAndEnabled } = useBiometrics();
const navigation = useExtendedNavigation();
const route = useRoute();
const { fromWallet, memo, psbt, deepLinkPSBT, launchedBy } = route.params;
const { walletID, memo, psbt, deepLinkPSBT, launchedBy } = route.params;
const wallet = wallets.find(w => w.getID() === walletID);
const routeParamsPSBT = useRef(route.params.psbt);
const routeParamsTXHex = route.params.txhex;
const { colors } = useTheme();
@ -60,7 +61,7 @@ const PsbtWithHardwareWallet = () => {
});
const _combinePSBT = receivedPSBT => {
return fromWallet.combinePsbt(psbt, receivedPSBT);
return wallet.combinePsbt(psbt, receivedPSBT);
};
const onBarScanned = ret => {
@ -104,7 +105,7 @@ const PsbtWithHardwareWallet = () => {
if (deepLinkPSBT) {
const newPsbt = bitcoin.Psbt.fromBase64(deepLinkPSBT);
try {
const Tx = fromWallet.combinePsbt(routeParamsPSBT.current, newPsbt);
const Tx = wallet.combinePsbt(routeParamsPSBT.current, newPsbt);
setTxHex(Tx.toHex());
} catch (Err) {
presentAlert({ message: Err });
@ -128,7 +129,7 @@ const PsbtWithHardwareWallet = () => {
try {
await BlueElectrum.ping();
await BlueElectrum.waitTillConnected();
const result = await fromWallet.broadcastTx(txHex);
const result = await wallet.broadcastTx(txHex);
if (result) {
setIsLoading(false);
const txDecoded = bitcoin.Transaction.fromHex(txHex);
@ -139,7 +140,7 @@ const PsbtWithHardwareWallet = () => {
}
navigation.navigate('Success', { amount: undefined });
await new Promise(resolve => setTimeout(resolve, 3000)); // sleep to make sure network propagates
fetchAndSaveWalletTransactions(fromWallet.getID());
fetchAndSaveWalletTransactions(wallet.getID());
} else {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
setIsLoading(false);
@ -214,17 +215,11 @@ const PsbtWithHardwareWallet = () => {
}
};
const openScanner = () => {
requestCameraAuthorization().then(() => {
navigation.navigate('ScanQRCodeRoot', {
screen: 'ScanQRCode',
params: {
launchedBy: route.name,
showFileImportButton: false,
onBarScanned,
},
});
});
const openScanner = async () => {
const data = await scanQrHelper(route.name, true);
if (data) {
onBarScanned(data);
}
};
if (txHex) return _renderBroadcastHex();

View file

@ -13,12 +13,14 @@ import loc from '../../loc';
import { BitcoinUnit } from '../../models/bitcoinUnits';
import HandOffComponent from '../../components/HandOffComponent';
import { HandOffActivityType } from '../../components/types';
import { useSettings } from '../../hooks/context/useSettings';
const Success = () => {
const pop = () => {
getParent().pop();
};
const { colors } = useTheme();
const { selectedBlockExplorer } = useSettings();
const { getParent } = useNavigation();
const { amount, fee, amountUnit = BitcoinUnit.BTC, invoiceDescription = '', onDonePressed = pop, txid } = useRoute().params;
const stylesHook = StyleSheet.create({
@ -52,7 +54,7 @@ const Success = () => {
<HandOffComponent
title={loc.transactions.details_title}
type={HandOffActivityType.ViewInBlockExplorer}
url={`https://mempool.space/tx/${txid}`}
url={`${selectedBlockExplorer}/tx/${txid}`}
/>
)}
</SafeArea>

View file

@ -2,7 +2,6 @@ import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import React, { useEffect, useReducer } from 'react';
import { ScrollView, TouchableWithoutFeedback, View } from 'react-native';
import { BlueCard, BlueText } from '../../BlueComponents';
import { TWallet } from '../../class/wallets/types';
import ListItem from '../../components/ListItem';
import useOnAppLaunch from '../../hooks/useOnAppLaunch';
@ -105,10 +104,9 @@ const DefaultView: React.FC = () => {
value: state.isViewAllWalletsSwitchEnabled,
disabled: wallets.length <= 0,
}}
subtitle={loc.settings.default_desc}
/>
<BlueCard>
<BlueText>{loc.settings.default_desc}</BlueText>
</BlueCard>
{!state.isViewAllWalletsSwitchEnabled && (
<ListItem
title={loc.settings.default_info}

View file

@ -1,7 +1,5 @@
import React, { useCallback, useEffect, useReducer, useRef } from 'react';
import { Alert, Platform, ScrollView, StyleSheet, Text, TouchableWithoutFeedback, View } from 'react-native';
import { BlueCard, BlueSpacing20, BlueText } from '../../BlueComponents';
import presentAlert from '../../components/Alert';
import ListItem, { TouchableOpacityWrapper } from '../../components/ListItem';
import { useTheme } from '../../components/themes';
import { unlockWithBiometrics, useBiometrics } from '../../hooks/useBiometrics';
@ -13,6 +11,9 @@ import PromptPasswordConfirmationModal, {
PromptPasswordConfirmationModalHandle,
} from '../../components/PromptPasswordConfirmationModal';
import { popToTop } from '../../NavigationService';
import presentAlert from '../../components/Alert';
import { Header } from '../../components/Header';
import { BlueSpacing20 } from '../../BlueComponents';
enum ActionType {
SetLoading = 'SET_LOADING',
@ -72,9 +73,6 @@ const EncryptStorage = () => {
root: {
backgroundColor: colors.background,
},
headerText: {
color: colors.foregroundColor,
},
});
const initializeState = useCallback(async () => {
@ -139,15 +137,6 @@ const EncryptStorage = () => {
navigate('PlausibleDeniability');
};
const renderPasscodeExplanation = () => {
return Platform.OS === 'android' && Platform.Version >= 30 ? (
<>
<BlueText />
<BlueText>{loc.formatString(loc.settings.biometrics_fail, { type: deviceBiometricType! })}</BlueText>
</>
) : null;
};
return (
<ScrollView
contentContainerStyle={[styles.root, styleHooks.root]}
@ -157,26 +146,30 @@ const EncryptStorage = () => {
<View style={styles.paddingTop} />
{state.deviceBiometricCapable && (
<>
<Text adjustsFontSizeToFit style={[styles.headerText, styleHooks.headerText]}>
{loc.settings.biometrics}
</Text>
<Header leftText={loc.settings.biometrics} />
<ListItem
title={loc.formatString(loc.settings.encrypt_use, { type: deviceBiometricType! })}
Component={TouchableWithoutFeedback}
switch={{ value: biometricEnabled, onValueChange: onUseBiometricSwitch, disabled: state.currentLoadingSwitch !== null }}
switch={{
value: biometricEnabled,
onValueChange: onUseBiometricSwitch,
disabled: state.currentLoadingSwitch !== null,
}}
isLoading={state.currentLoadingSwitch === 'biometric' && state.isLoading}
containerStyle={[styles.row, styleHooks.root]}
subtitle={
<>
<Text style={styles.subtitleText}>{loc.formatString(loc.settings.encrypt_use_expl, { type: deviceBiometricType! })}</Text>
{Platform.OS === 'android' && Platform.Version >= 30 && (
<Text style={styles.subtitleText}>{loc.formatString(loc.settings.biometrics_fail, { type: deviceBiometricType! })}</Text>
)}
</>
}
/>
<BlueCard>
<BlueText>{loc.formatString(loc.settings.encrypt_use_expl, { type: deviceBiometricType! })}</BlueText>
{renderPasscodeExplanation()}
</BlueCard>
<BlueSpacing20 />
</>
)}
<Text adjustsFontSizeToFit style={[styles.headerText, styleHooks.headerText]}>
{loc.settings.encrypt_tstorage}
</Text>
<BlueSpacing20 />
<Header leftText={loc.settings.encrypt_tstorage} />
<ListItem
testID="EncyptedAndPasswordProtected"
title={loc.settings.encrypt_enc_and_pass}
@ -211,7 +204,7 @@ const EncryptStorage = () => {
dispatch({ type: ActionType.SetModalType, payload: MODAL_TYPES.SUCCESS });
success = true;
} catch (error) {
presentAlert({ title: loc.errors.error, message: (error as Error).message });
presentAlert({ message: (error as Error).message });
success = false;
}
} else if (state.modalType === MODAL_TYPES.ENTER_PASSWORD) {
@ -243,12 +236,11 @@ const styles = StyleSheet.create({
flex: 1,
},
paddingTop: { paddingTop: 19 },
headerText: {
fontWeight: 'bold',
fontSize: 30,
marginLeft: 17,
},
row: { minHeight: 60 },
subtitleText: {
fontSize: 14,
marginTop: 5,
},
});
export default EncryptStorage;

View file

@ -1,7 +1,7 @@
import { useNavigation } from '@react-navigation/native';
import React from 'react';
import { Platform, ScrollView, StyleSheet } from 'react-native';
import { BlueCard, BlueSpacing20, BlueText } from '../../BlueComponents';
import { BlueSpacing20 } from '../../BlueComponents';
import ListItem, { PressableWrapper } from '../../components/ListItem';
import { useTheme } from '../../components/themes';
import loc from '../../loc';
@ -50,14 +50,10 @@ const GeneralSettings: React.FC = () => {
title={loc.settings.general_continuity}
Component={PressableWrapper}
switch={{ onValueChange: onHandOffUseEnabledChange, value: isHandOffUseEnabled }}
subtitle={loc.settings.general_continuity_e}
/>
<BlueCard>
<BlueText>{loc.settings.general_continuity_e}</BlueText>
</BlueCard>
<BlueSpacing20 />
</>
) : null}
<BlueSpacing20 />
<ListItem
Component={PressableWrapper}
title="Legacy URv1 QR"

View file

@ -1,30 +1,34 @@
import { useNavigation } from '@react-navigation/native';
import React from 'react';
import { ScrollView } from 'react-native';
import Notifications from '../../blue_modules/notifications';
import { isNotificationsCapable } from '../../blue_modules/notifications';
import ListItem from '../../components/ListItem';
import loc from '../../loc';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
const NetworkSettings = () => {
const { navigate } = useNavigation();
const NetworkSettings: React.FC = () => {
const navigation = useExtendedNavigation();
const navigateToElectrumSettings = () => {
navigate('ElectrumSettings');
navigation.navigate('ElectrumSettings');
};
const navigateToLightningSettings = () => {
navigate('LightningSettings');
navigation.navigate('LightningSettings');
};
const navigateToBlockExplorerSettings = () => {
navigation.navigate('SettingsBlockExplorer');
};
return (
<ScrollView contentInsetAdjustmentBehavior="automatic" automaticallyAdjustContentInsets>
<ListItem title={loc.settings.block_explorer} onPress={navigateToBlockExplorerSettings} testID="BlockExplorerSettings" chevron />
<ListItem title={loc.settings.network_electrum} onPress={navigateToElectrumSettings} testID="ElectrumSettings" chevron />
<ListItem title={loc.settings.lightning_settings} onPress={navigateToLightningSettings} testID="LightningSettings" chevron />
{Notifications.isNotificationsCapable && (
{isNotificationsCapable && (
<ListItem
title={loc.settings.notifications}
onPress={() => navigate('NotificationSettings')}
onPress={() => navigation.navigate('NotificationSettings')}
testID="NotificationSettings"
chevron
/>

View file

@ -0,0 +1,219 @@
import React, { useRef, useCallback, useState, useEffect } from 'react';
import { SectionList, StyleSheet, TextInput, SectionListRenderItemInfo, SectionListData, View, LayoutAnimation } from 'react-native';
import ListItem from '../../components/ListItem';
import loc from '../../loc';
import { useTheme } from '../../components/themes';
import {
getBlockExplorersList,
BlockExplorer,
isValidUrl,
normalizeUrl,
BLOCK_EXPLORERS,
removeBlockExplorer,
} from '../../models/blockExplorer';
import presentAlert from '../../components/Alert';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import { useSettings } from '../../hooks/context/useSettings';
import SettingsBlockExplorerCustomUrlItem from '../../components/SettingsBlockExplorerCustomUrlListItem';
import { Header } from '../../components/Header';
type BlockExplorerItem = BlockExplorer | string;
interface SectionData extends SectionListData<BlockExplorerItem> {
title?: string;
data: BlockExplorerItem[];
}
const SettingsBlockExplorer: React.FC = () => {
const { colors } = useTheme();
const { selectedBlockExplorer, setBlockExplorerStorage } = useSettings();
const customUrlInputRef = useRef<TextInput>(null);
const [customUrl, setCustomUrl] = useState<string>(selectedBlockExplorer.key === 'custom' ? selectedBlockExplorer.url : '');
const [isCustomEnabled, setIsCustomEnabled] = useState<boolean>(selectedBlockExplorer.key === 'custom');
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const predefinedExplorers = getBlockExplorersList().filter(explorer => explorer.key !== 'custom');
const sections: SectionData[] = [
{
title: loc._.suggested,
data: predefinedExplorers,
},
{
title: loc.wallets.details_advanced,
data: ['custom'],
},
];
const handleExplorerPress = useCallback(
async (explorer: BlockExplorer) => {
const success = await setBlockExplorerStorage(explorer);
if (success) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
setIsCustomEnabled(false);
} else {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({ message: loc.settings.block_explorer_error_saving_custom });
}
},
[setBlockExplorerStorage],
);
const handleCustomUrlChange = useCallback((url: string) => {
setCustomUrl(url);
}, []);
const handleSubmitCustomUrl = useCallback(async () => {
if (isSubmitting) return;
setIsSubmitting(true);
const customUrlNormalized = normalizeUrl(customUrl);
if (!isValidUrl(customUrlNormalized)) {
presentAlert({ message: loc.settings.block_explorer_invalid_custom_url });
customUrlInputRef.current?.focus();
setIsSubmitting(false);
return;
}
const customExplorer: BlockExplorer = {
key: 'custom',
name: 'Custom',
url: customUrlNormalized,
};
const success = await setBlockExplorerStorage(customExplorer);
if (success) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
} else {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({ message: loc.settings.block_explorer_error_saving_custom });
}
setIsSubmitting(false);
}, [customUrl, setBlockExplorerStorage, isSubmitting]);
const handleCustomSwitchToggle = useCallback(
async (value: boolean) => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setIsCustomEnabled(value);
if (value) {
await removeBlockExplorer();
customUrlInputRef.current?.focus();
} else {
const defaultExplorer = BLOCK_EXPLORERS.default;
const success = await setBlockExplorerStorage(defaultExplorer);
if (success) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
} else {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
if (!isSubmitting) {
presentAlert({ message: loc.settings.block_explorer_error_saving_custom });
}
}
}
},
[setBlockExplorerStorage, isSubmitting],
);
useEffect(() => {
return () => {
if (isCustomEnabled) {
const customUrlNormalized = normalizeUrl(customUrl);
if (!isValidUrl(customUrlNormalized)) {
(async () => {
const success = await setBlockExplorerStorage(BLOCK_EXPLORERS.default);
if (!success) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({ message: loc.settings.block_explorer_error_saving_custom });
}
})();
}
}
};
}, [customUrl, isCustomEnabled, setBlockExplorerStorage]);
const renderItem = useCallback(
({ item, section }: SectionListRenderItemInfo<BlockExplorerItem, SectionData>) => {
if (section.title === loc._.suggested) {
const explorer = item as BlockExplorer;
const isSelected = !isCustomEnabled && normalizeUrl(selectedBlockExplorer.url || '') === normalizeUrl(explorer.url || '');
return (
<ListItem
title={explorer.name}
onPress={() => handleExplorerPress(explorer)}
checkmark={isSelected}
disabled={isCustomEnabled}
containerStyle={[{ backgroundColor: colors.background }, styles.rowHeight]}
/>
);
} else {
return (
<SettingsBlockExplorerCustomUrlItem
isCustomEnabled={isCustomEnabled}
onSwitchToggle={handleCustomSwitchToggle}
customUrl={customUrl}
onCustomUrlChange={handleCustomUrlChange}
onSubmitCustomUrl={handleSubmitCustomUrl}
inputRef={customUrlInputRef}
/>
);
}
},
[
selectedBlockExplorer,
isCustomEnabled,
handleExplorerPress,
colors.background,
handleCustomSwitchToggle,
customUrl,
handleCustomUrlChange,
handleSubmitCustomUrl,
],
);
// @ts-ignore: renderSectionHeader type is not correct
const renderSectionHeader = useCallback(({ section }) => {
const { title } = section;
if (title) {
return (
<View style={styles.container}>
<Header leftText={title} />
</View>
);
}
return null;
}, []);
return (
<SectionList<BlockExplorerItem, SectionData>
sections={sections}
keyExtractor={(item, index) => {
if (typeof item === 'string') {
return `custom-${index}`;
} else {
return item.key;
}
}}
renderItem={renderItem}
renderSectionHeader={renderSectionHeader}
contentInsetAdjustmentBehavior="automatic"
automaticallyAdjustContentInsets
style={[styles.root, { backgroundColor: colors.background }]}
stickySectionHeadersEnabled={false}
/>
);
};
export default SettingsBlockExplorer;
const styles = StyleSheet.create({
root: {
flex: 1,
},
container: {
paddingTop: 24,
},
rowHeight: {
minHeight: 60,
},
});

View file

@ -2,7 +2,6 @@ import React, { useEffect, useState } from 'react';
import { Platform, Pressable, ScrollView, StyleSheet, Text, TouchableWithoutFeedback, View } from 'react-native';
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, { PressableWrapper } from '../../components/ListItem';
import { useTheme } from '../../components/themes';
@ -10,6 +9,7 @@ import { setBalanceDisplayAllowed } from '../../components/WidgetCommunication';
import loc from '../../loc';
import { useStorage } from '../../hooks/context/useStorage';
import { useSettings } from '../../hooks/context/useSettings';
import { BlueSpacing20 } from '../../BlueComponents';
enum SettingsPrivacySection {
None,
@ -45,9 +45,6 @@ const SettingsPrivacy: React.FC = () => {
root: {
backgroundColor: colors.background,
},
widgetsHeader: {
color: colors.foregroundColor,
},
});
useEffect(() => {
@ -130,13 +127,13 @@ const SettingsPrivacy: React.FC = () => {
disabled: isLoading === SettingsPrivacySection.All,
testID: 'ClipboardSwitch',
}}
subtitle={
<Pressable accessibilityRole="button">
<Text style={styles.subtitleText}>{loc.settings.privacy_clipboard_explanation}</Text>
</Pressable>
}
/>
<BlueCard>
<Pressable accessibilityRole="button">
<BlueText>{loc.settings.privacy_clipboard_explanation}</BlueText>
</Pressable>
</BlueCard>
<BlueSpacing20 />
<ListItem
title={loc.settings.privacy_quickactions}
Component={TouchableWithoutFeedback}
@ -146,12 +143,14 @@ const SettingsPrivacy: React.FC = () => {
disabled: isLoading === SettingsPrivacySection.All || storageIsEncrypted,
testID: 'QuickActionsSwitch',
}}
subtitle={
<>
<Text style={styles.subtitleText}>{loc.settings.privacy_quickactions_explanation}</Text>
{storageIsEncrypted && <Text style={styles.subtitleText}>{loc.settings.encrypted_feature_disabled}</Text>}
</>
}
/>
<BlueCard>
<BlueText>{loc.settings.privacy_quickactions_explanation}</BlueText>
<BlueSpacing20 />
{storageIsEncrypted && <BlueText>{loc.settings.encrypted_feature_disabled}</BlueText>}
</BlueCard>
<ListItem
title={loc.total_balance_view.title}
Component={PressableWrapper}
@ -161,11 +160,9 @@ const SettingsPrivacy: React.FC = () => {
disabled: isLoading === SettingsPrivacySection.All || wallets.length < 2,
testID: 'TotalBalanceSwitch',
}}
subtitle={<Text style={styles.subtitleText}>{loc.total_balance_view.explanation}</Text>}
/>
<BlueCard>
<BlueText>{loc.total_balance_view.explanation}</BlueText>
<BlueSpacing20 />
</BlueCard>
<ListItem
title={loc.settings.privacy_temporary_screenshots}
Component={TouchableWithoutFeedback}
@ -174,24 +171,24 @@ const SettingsPrivacy: React.FC = () => {
value: !isPrivacyBlurEnabled,
disabled: isLoading === SettingsPrivacySection.All,
}}
subtitle={<Text style={styles.subtitleText}>{loc.settings.privacy_temporary_screenshots_instructions}</Text>}
/>
<BlueCard>
<BlueText>{loc.settings.privacy_temporary_screenshots_instructions}</BlueText>
</BlueCard>
<ListItem
title={loc.settings.privacy_do_not_track}
Component={TouchableWithoutFeedback}
switch={{ onValueChange: onDoNotTrackValueChange, value: isDoNotTrackEnabled, disabled: isLoading === SettingsPrivacySection.All }}
switch={{
onValueChange: onDoNotTrackValueChange,
value: isDoNotTrackEnabled,
disabled: isLoading === SettingsPrivacySection.All,
}}
subtitle={<Text style={styles.subtitleText}>{loc.settings.privacy_do_not_track_explanation}</Text>}
/>
<BlueCard>
<BlueText>{loc.settings.privacy_do_not_track_explanation}</BlueText>
</BlueCard>
{Platform.OS === 'ios' && (
<>
<BlueSpacing40 />
<Text adjustsFontSizeToFit style={[styles.widgetsHeader, styleHooks.widgetsHeader]}>
{loc.settings.widgets}
</Text>
<BlueSpacing20 />
<Header leftText={loc.settings.widgets} />
<ListItem
title={loc.settings.total_balance}
Component={TouchableWithoutFeedback}
@ -200,18 +197,17 @@ const SettingsPrivacy: React.FC = () => {
value: storageIsEncrypted ? false : isWidgetBalanceDisplayAllowed,
disabled: isLoading === SettingsPrivacySection.All || storageIsEncrypted,
}}
subtitle={
<>
<Text style={styles.subtitleText}>{loc.settings.total_balance_explanation}</Text>
{storageIsEncrypted && <Text style={styles.subtitleText}>{loc.settings.encrypted_feature_disabled}</Text>}
</>
}
/>
<BlueCard>
<BlueText>{loc.settings.total_balance_explanation}</BlueText>
<BlueSpacing20 />
{storageIsEncrypted && <BlueText>{loc.settings.encrypted_feature_disabled}</BlueText>}
</BlueCard>
</>
)}
<BlueSpacing20 />
<ListItem title={loc.settings.privacy_system_settings} chevron onPress={openApplicationSettings} testID="PrivacySystemSettings" />
<BlueSpacing20 />
</ScrollView>
);
};
@ -220,14 +216,14 @@ const styles = StyleSheet.create({
root: {
flex: 1,
},
widgetsHeader: {
fontWeight: 'bold',
fontSize: 30,
marginLeft: 17,
},
headerContainer: {
paddingVertical: 16,
},
subtitleText: {
fontSize: 14,
marginTop: 5,
},
});
export default SettingsPrivacy;

View file

@ -416,10 +416,9 @@ export default class ElectrumSettings extends Component {
value: this.state.isOfflineMode,
testID: 'ElectrumConnectionEnabledSwitch',
}}
subtitle={loc.settings.electrum_offline_description}
/>
<BlueCard>
<BlueText>{loc.settings.electrum_offline_description}</BlueText>
</BlueCard>
{!this.state.isOfflineMode && this.renderElectrumSettings()}
</ScrollView>
);

View file

@ -105,14 +105,11 @@ const NotificationSettings = () => {
<ListItem
Component={TouchableWithoutFeedback}
title={loc.settings.push_notifications}
subtitle={loc.settings.groundcontrol_explanation}
switch={{ onValueChange: onNotificationsSwitch, value: isNotificationsEnabled, testID: 'NotificationsSwitch' }}
/>
<BlueSpacing20 />
<BlueCard>
<BlueText>{loc.settings.groundcontrol_explanation}</BlueText>
</BlueCard>
<ButtonRNElements
icon={{
name: 'github',

View file

@ -19,6 +19,7 @@ import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList';
import { useStorage } from '../../hooks/context/useStorage';
import { HandOffActivityType } from '../../components/types';
import { useSettings } from '../../hooks/context/useSettings';
const actionKeys = {
CopyToClipboard: 'copyToClipboard',
@ -63,6 +64,7 @@ const TransactionDetails = () => {
const { setOptions, navigate } = useExtendedNavigation<NavigationProps>();
const { hash, walletID } = useRoute<RouteProps>().params;
const { saveToDisk, txMetadata, counterpartyMetadata, wallets, getTransactions } = useStorage();
const { selectedBlockExplorer } = useSettings();
const [from, setFrom] = useState<string[]>([]);
const [to, setTo] = useState<string[]>([]);
const [isLoading, setIsLoading] = useState(true);
@ -159,7 +161,7 @@ const TransactionDetails = () => {
);
const handleOnOpenTransactionOnBlockExplorerTapped = () => {
const url = `https://mempool.space/tx/${tx?.hash}`;
const url = `${selectedBlockExplorer}/tx/${tx?.hash}`;
Linking.canOpenURL(url)
.then(supported => {
if (supported) {
@ -184,7 +186,7 @@ const TransactionDetails = () => {
};
const handleCopyPress = (stringToCopy: string) => {
Clipboard.setString(stringToCopy !== actionKeys.CopyToClipboard ? stringToCopy : `https://mempool.space/tx/${tx?.hash}`);
Clipboard.setString(stringToCopy !== actionKeys.CopyToClipboard ? stringToCopy : `${selectedBlockExplorer}/tx/${tx?.hash}`);
};
if (isLoading || !tx) {
@ -255,7 +257,7 @@ const TransactionDetails = () => {
<HandOffComponent
title={loc.transactions.details_title}
type={HandOffActivityType.ViewInBlockExplorer}
url={`https://mempool.space/tx/${tx.hash}`}
url={`${selectedBlockExplorer}/tx/${tx.hash}`}
/>
<BlueCard>
<View>

View file

@ -21,6 +21,7 @@ import { useStorage } from '../../hooks/context/useStorage';
import { HandOffActivityType } from '../../components/types';
import HeaderRightButton from '../../components/HeaderRightButton';
import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList';
import { useSettings } from '../../hooks/context/useSettings';
enum ButtonStatus {
Possible,
@ -97,6 +98,7 @@ const TransactionStatus = () => {
const { navigate, setOptions, goBack } = useNavigation<TransactionStatusProps['navigation']>();
const { colors } = useTheme();
const wallet = useRef(wallets.find(w => w.getID() === walletID));
const { selectedBlockExplorer } = useSettings();
const fetchTxInterval = useRef<NodeJS.Timeout>();
const stylesHook = StyleSheet.create({
value: {
@ -481,7 +483,7 @@ const TransactionStatus = () => {
<HandOffComponent
title={loc.transactions.details_title}
type={HandOffActivityType.ViewInBlockExplorer}
url={`https://mempool.space/tx/${tx.hash}`}
url={`${selectedBlockExplorer}/tx/${tx.hash}`}
/>
<View style={styles.container}>

View file

@ -25,11 +25,10 @@ import WalletButton from '../../components/WalletButton';
import loc from '../../loc';
import { Chain } from '../../models/bitcoinUnits';
import { useStorage } from '../../hooks/context/useStorage';
import ToolTipMenu from '../../components/TooltipMenu';
import { Icon } from '@rneui/themed';
import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
import { Action } from '../../components/types';
import { getLNDHub } from '../../helpers/lndHub';
import HeaderMenuButton from '../../components/HeaderMenuButton';
enum ButtonSelected {
// @ts-ignore: Return later to update
@ -228,9 +227,7 @@ const WalletsAdd: React.FC = () => {
const HeaderRight = useMemo(
() => (
<ToolTipMenu
isButton
isMenuPrimaryAction
<HeaderMenuButton
onPressMenuItem={(id: string) => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
if (id === HDSegwitBech32Wallet.type) {
@ -246,11 +243,9 @@ const WalletsAdd: React.FC = () => {
}
}}
actions={toolTipActions}
>
<Icon size={22} name="more-horiz" type="material" color={colors.foregroundColor} />
</ToolTipMenu>
/>
),
[colors.foregroundColor, handleOnLightningButtonPressed, navigateToEntropy, toolTipActions],
[handleOnLightningButtonPressed, navigateToEntropy, toolTipActions],
);
useEffect(() => {

View file

@ -1,10 +1,10 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useRoute } from '@react-navigation/native';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { RouteProp, useRoute } from '@react-navigation/native';
import { ActivityIndicator, FlatList, LayoutAnimation, StyleSheet, View } from 'react-native';
import IdleTimerManager from 'react-native-idle-timer';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import { BlueButtonLink, BlueFormLabel, BlueSpacing10, BlueSpacing20 } from '../../BlueComponents';
import { HDSegwitBech32Wallet } from '../../class';
import { BlueButtonLink, BlueFormLabel, BlueSpacing10, BlueSpacing20, BlueText } from '../../BlueComponents';
import { HDSegwitBech32Wallet, WatchOnlyWallet } from '../../class';
import startImport from '../../class/wallet-import';
import presentAlert from '../../components/Alert';
import Button from '../../components/Button';
@ -15,20 +15,44 @@ import prompt from '../../helpers/prompt';
import loc from '../../loc';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import { useStorage } from '../../hooks/context/useStorage';
import { AddWalletStackParamList } from '../../navigation/AddWalletStack';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { THDWalletForWatchOnly, TWallet } from '../../class/wallets/types';
import { navigate } from '../../NavigationService';
const ImportWalletDiscovery = () => {
const navigation = useExtendedNavigation();
type RouteProps = RouteProp<AddWalletStackParamList, 'ImportWalletDiscovery'>;
type NavigationProp = NativeStackNavigationProp<AddWalletStackParamList, 'ImportWalletDiscovery'>;
type TReturn = {
cancelled?: boolean;
stopped?: boolean;
wallets: TWallet[];
};
type ImportTask = {
promise: Promise<TReturn>;
stop: () => void;
};
type WalletEntry = {
wallet: TWallet | THDWalletForWatchOnly;
subtitle: string;
id: string;
};
const ImportWalletDiscovery: React.FC = () => {
const navigation = useExtendedNavigation<NavigationProp>();
const { colors } = useTheme();
const route = useRoute();
const route = useRoute<RouteProps>();
const { importText, askPassphrase, searchAccounts } = route.params;
const task = useRef();
const task = useRef<ImportTask | null>(null);
const { addAndSaveWallet } = useStorage();
const [loading, setLoading] = useState(true);
const [wallets, setWallets] = useState([]);
const [password, setPassword] = useState();
const [selected, setSelected] = useState(0);
const [progress, setProgress] = useState();
const importing = useRef(false);
const [loading, setLoading] = useState<boolean>(true);
const [wallets, setWallets] = useState<WalletEntry[]>([]);
const [password, setPassword] = useState<string | undefined>();
const [selected, setSelected] = useState<number>(0);
const [progress, setProgress] = useState<string | undefined>();
const importing = useRef<boolean>(false);
const bip39 = useMemo(() => {
const hd = new HDSegwitBech32Wallet();
hd.setSecret(importText);
@ -44,32 +68,46 @@ const ImportWalletDiscovery = () => {
},
});
const saveWallet = wallet => {
if (importing.current) return;
importing.current = true;
addAndSaveWallet(wallet);
navigation.getParent().pop();
};
const saveWallet = useCallback(
(wallet: TWallet | THDWalletForWatchOnly) => {
if (importing.current) return;
importing.current = true;
addAndSaveWallet(wallet);
navigate('WalletsList');
},
[addAndSaveWallet],
);
useEffect(() => {
const onProgress = data => setProgress(data);
const onProgress = (data: string) => setProgress(data);
const onWallet = wallet => {
const onWallet = (wallet: TWallet | THDWalletForWatchOnly) => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
const id = wallet.getID();
let subtitle;
let subtitle: string | undefined;
try {
subtitle = wallet.getDerivationPath?.();
// For watch-only wallets, display the descriptor or xpub
if (wallet.type === WatchOnlyWallet.type) {
if (wallet.isHd() && wallet.getSecret()) {
subtitle = wallet.getSecret(); // Display descriptor
} else {
subtitle = wallet.getAddress(); // Display address
}
} else {
subtitle = (wallet as THDWalletForWatchOnly).getDerivationPath?.();
}
} catch (e) {}
setWallets(w => [...w, { wallet, subtitle, id }]);
setWallets(w => [...w, { wallet, subtitle: subtitle || '', id }]);
};
const onPassword = async (title, subtitle) => {
const onPassword = async (title: string, subtitle: string) => {
try {
const pass = await prompt(title, subtitle);
setPassword(pass);
return pass;
} catch (e) {
} catch (e: any) {
if (e.message === 'Cancel Pressed') {
navigation.goBack();
}
@ -84,7 +122,7 @@ const ImportWalletDiscovery = () => {
task.current.promise
.then(({ cancelled, wallets: w }) => {
if (cancelled) return;
if (w.length === 1) saveWallet(w[0]); // instantly save wallet if only one has been discovered
if (w.length === 1) saveWallet(w[0]); // Instantly save wallet if only one has been discovered
if (w.length === 0) {
triggerHapticFeedback(HapticFeedbackTypes.ImpactLight);
}
@ -99,16 +137,19 @@ const ImportWalletDiscovery = () => {
IdleTimerManager.setIdleTimerDisabled(false);
});
return () => task.current.stop();
return () => {
task.current?.stop();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleCustomDerivation = () => {
task.current.stop();
task.current?.stop();
navigation.navigate('ImportCustomDerivationPath', { importText, password });
};
const renderItem = ({ item, index }) => (
const renderItem = ({ item, index }: { item: WalletEntry; index: number }) => (
<WalletToImport
key={item.id}
title={item.wallet.typeReadable}
@ -121,22 +162,45 @@ const ImportWalletDiscovery = () => {
/>
);
const keyExtractor = w => w.id;
const keyExtractor = (w: WalletEntry) => w.id;
const ListHeaderComponent = useMemo(
() => (
<>
{wallets && wallets.length > 0 ? (
<>
<BlueSpacing20 />
<BlueFormLabel>{loc.wallets.import_discovery_subtitle}</BlueFormLabel>
<BlueSpacing10 />
</>
) : null}
</>
),
[wallets],
);
const ListEmptyComponent = useMemo(
() => (
<View style={styles.noWallets}>
<BlueText style={styles.center}>{loc.wallets.import_discovery_no_wallets}</BlueText>
<BlueSpacing20 />
</View>
),
[],
);
return (
<SafeArea style={[styles.root, stylesHook.root]}>
<BlueSpacing20 />
<BlueFormLabel>{loc.wallets.import_discovery_subtitle}</BlueFormLabel>
<BlueSpacing20 />
{!loading && wallets.length === 0 ? (
<View style={styles.noWallets}>
<BlueFormLabel>{loc.wallets.import_discovery_no_wallets}</BlueFormLabel>
</View>
) : (
<FlatList contentContainerStyle={styles.flatListContainer} data={wallets} keyExtractor={keyExtractor} renderItem={renderItem} />
)}
<FlatList
ListHeaderComponent={ListHeaderComponent}
contentContainerStyle={styles.flatListContainer}
data={wallets}
ListEmptyComponent={ListEmptyComponent}
keyExtractor={keyExtractor}
renderItem={renderItem}
automaticallyAdjustContentInsets
contentInsetAdjustmentBehavior="always"
/>
<View style={[styles.center, stylesHook.center]}>
{loading && (
<>
@ -157,9 +221,12 @@ const ImportWalletDiscovery = () => {
<BlueSpacing10 />
<View style={styles.buttonContainer}>
<Button
disabled={wallets.length === 0}
disabled={wallets?.length === 0}
title={loc.wallets.import_do_import}
onPress={() => saveWallet(wallets[selected].wallet)}
onPress={() => {
if (wallets.length === 0) return;
saveWallet(wallets[selected].wallet);
}}
/>
</View>
</View>
@ -169,7 +236,6 @@ const ImportWalletDiscovery = () => {
const styles = StyleSheet.create({
root: {
paddingTop: 40,
flex: 1,
},
flatListContainer: {

View file

@ -499,7 +499,7 @@ const ManageWallets: React.FC = () => {
return (
<GestureHandlerRootView style={[{ backgroundColor: colors.background }, styles.root]}>
<NestableScrollContainer contentInsetAdjustmentBehavior="automatic" automaticallyAdjustContentInsets>
<NestableScrollContainer contentInsetAdjustmentBehavior="automatic" automaticallyAdjustContentInsets scrollEnabled>
{renderHeader}
<NestableDraggableFlatList
data={state.tempOrder.filter((item): item is WalletItem => item.type === ItemType.WalletSection)}

View file

@ -128,17 +128,23 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
}
}, [wallet, isElectrumDisabled, isLoading, saveToDisk, pageSize]);
useEffect(() => {
if (wallet && wallet.getLastTxFetch() === 0) {
refreshTransactions();
}
}, [wallet, refreshTransactions]);
useFocusEffect(
useCallback(() => {
const task = InteractionManager.runAfterInteractions(() => {
if (wallet && wallet.getLastTxFetch() === 0) {
refreshTransactions();
}
});
return () => task.cancel();
}, [refreshTransactions, wallet]),
);
useEffect(() => {
if (wallet) {
setSelectedWalletID(wallet.getID());
setSelectedWalletID(walletID);
}
}, [wallet, setSelectedWalletID]);
}, [wallet, setSelectedWalletID, walletID]);
const isLightning = (): boolean => wallet?.chain === Chain.OFFCHAIN || false;
@ -173,7 +179,7 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
navigate('SendDetailsRoot', {
screen: 'SendDetails',
params: {
walletID: wallet?.getID(),
walletID,
},
});
};
@ -182,8 +188,8 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
assert(wallet?.type === LightningCustodianWallet.type, `internal error, wallet is not ${LightningCustodianWallet.type}`);
navigate('WalletTransactions', {
walletType: wallet?.type,
walletID: wallet?.getID(),
key: `WalletTransactions-${wallet?.getID()}`,
walletID,
key: `WalletTransactions-${walletID}`,
}); // navigating back to ln wallet screen
// getting refill address, either cached or from the server:
@ -252,7 +258,7 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
if (!isLoading) {
setIsLoading(true);
const params = {
walletID: wallet?.getID(),
walletID,
uri: ret?.data ? ret.data : ret,
};
if (wallet?.chain === Chain.ONCHAIN) {
@ -263,7 +269,7 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
setIsLoading(false);
}
},
[wallet, navigate, isLoading],
[isLoading, walletID, wallet?.chain, navigate],
);
const choosePhoto = () => {
@ -288,7 +294,7 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
const sendButtonPress = () => {
if (wallet?.chain === Chain.OFFCHAIN) {
return navigate('ScanLndInvoiceRoot', { screen: 'ScanLndInvoice', params: { walletID: wallet.getID() } });
return navigate('ScanLndInvoiceRoot', { screen: 'ScanLndInvoice', params: { walletID } });
}
if (wallet?.type === WatchOnlyWallet.type && wallet.isHd() && !wallet.useWithHardwareWalletEnabled()) {
@ -407,7 +413,7 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
navigate('WalletExportRoot', {
screen: 'WalletExport',
params: {
walletID: wallet!.getID(),
walletID,
},
});
});
@ -460,9 +466,9 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
text={loc.receive.header}
onPress={() => {
if (wallet.chain === Chain.OFFCHAIN) {
navigate('LNDCreateInvoiceRoot', { screen: 'LNDCreateInvoice', params: { walletID: wallet.getID() } });
navigate('LNDCreateInvoiceRoot', { screen: 'LNDCreateInvoice', params: { walletID } });
} else {
navigate('ReceiveDetailsRoot', { screen: 'ReceiveDetails', params: { walletID: wallet.getID() } });
navigate('ReceiveDetailsRoot', { screen: 'ReceiveDetails', params: { walletID } });
}
}}
icon={

View file

@ -469,8 +469,10 @@ const WalletsAddMultisigStep2 = () => {
const scanOrOpenFile = async () => {
await provideMnemonicsModalRef.current.dismiss();
const scanned = await scanQrHelper(name, true, undefined);
onBarScanned({ data: scanned });
const scanned = await scanQrHelper(name, true);
if (scanned) {
onBarScanned(scanned);
}
};
const dashType = ({ index, lastIndex, isChecked, isFocus }) => {

View file

@ -4,18 +4,17 @@ import { Keyboard, Platform, StyleSheet, TouchableWithoutFeedback, View, ScrollV
import { BlueButtonLink, BlueFormLabel, BlueFormMultiInput, BlueSpacing20 } from '../../BlueComponents';
import Button from '../../components/Button';
import { useTheme } from '../../components/themes';
import { requestCameraAuthorization } from '../../helpers/scan-qr';
import { scanQrHelper } from '../../helpers/scan-qr';
import usePrivacy from '../../hooks/usePrivacy';
import loc from '../../loc';
import {
DoneAndDismissKeyboardInputAccessory,
DoneAndDismissKeyboardInputAccessoryViewID,
} from '../../components/DoneAndDismissKeyboardInputAccessory';
import { Icon } from '@rneui/themed';
import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
import { useKeyboard } from '../../hooks/useKeyboard';
import ToolTipMenu from '../../components/TooltipMenu';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import HeaderMenuButton from '../../components/HeaderMenuButton';
const WalletsImport = () => {
const navigation = useExtendedNavigation();
@ -97,16 +96,11 @@ const WalletsImport = () => {
setTimeout(() => importMnemonic(value), 500);
};
const importScan = () => {
requestCameraAuthorization().then(() =>
navigation.navigate('ScanQRCodeRoot', {
screen: 'ScanQRCode',
params: {
launchedBy: route.name,
showFileImportButton: true,
},
}),
);
const importScan = async () => {
const data = await scanQrHelper(navigation, true);
if (data) {
onBarScanned(data);
}
};
const speedBackdoorTap = () => {
@ -140,18 +134,8 @@ const WalletsImport = () => {
}, [askPassphrase, searchAccounts]);
const HeaderRight = useMemo(
() => (
<ToolTipMenu
isButton
testID="HeaderRightButton"
isMenuPrimaryAction
onPressMenuItem={toolTipOnPressMenuItem}
actions={toolTipActions}
>
<Icon size={22} name="more-horiz" type="material" color={colors.foregroundColor} />
</ToolTipMenu>
),
[toolTipOnPressMenuItem, toolTipActions, colors.foregroundColor],
() => <HeaderMenuButton onPressMenuItem={toolTipOnPressMenuItem} actions={toolTipActions} />,
[toolTipOnPressMenuItem, toolTipActions],
);
// Adding the ToolTipMenu to the header

View file

@ -178,6 +178,7 @@ describe('BlueWallet UI Tests - no wallets', () => {
await yo('WalletsList');
await expect(element(by.id('cr34t3d'))).toBeVisible();
await element(by.id('cr34t3d')).tap();
await yo('ReceiveButton');
await element(by.id('ReceiveButton')).tap();
await element(by.text('Yes, I have.')).tap();
try {
@ -506,7 +507,7 @@ describe('BlueWallet UI Tests - no wallets', () => {
await element(by.id('CreateButton')).tap();
await yo('Multisig Vault');
await element(by.id('Multisig Vault')).tap(); // go inside the wallet
await yo('ReceiveButton');
await element(by.id('ReceiveButton')).tap();
await element(by.text('Yes, I have.')).tap();
try {
@ -571,6 +572,7 @@ describe('BlueWallet UI Tests - no wallets', () => {
// sending...
await yo('SendButton');
await element(by.id('SendButton')).tap();
await element(by.id('AddressInput')).replaceText('bc1q063ctu6jhe5k4v8ka99qac8rcm2tzjjnuktyrl');
@ -677,9 +679,9 @@ describe('BlueWallet UI Tests - no wallets', () => {
await element(by.id('MnemonicInput')).replaceText(
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
);
await element(by.id('HeaderRightButton')).tap();
await element(by.id('HeaderMenuButton')).tap();
await element(by.text('Passphrase')).tap();
await element(by.id('HeaderRightButton')).tap();
await element(by.id('HeaderMenuButton')).tap();
await element(by.text('Search accounts')).tap();
await element(by.id('DoImport')).tap();
await sleep(1000);

View file

@ -40,6 +40,7 @@ describe('BlueWallet UI Tests - import BIP84 wallet', () => {
await element(by.text('Imported HD SegWit (BIP84 Bech32 Native)')).tap();
// lets create real transaction:
await yo('SendButton');
await element(by.id('SendButton')).tap();
await element(by.id('AddressInput')).replaceText('bc1q063ctu6jhe5k4v8ka99qac8rcm2tzjjnuktyrl');
await element(by.id('BitcoinAmountInput')).typeText('0.0001\n');
@ -188,6 +189,7 @@ describe('BlueWallet UI Tests - import BIP84 wallet', () => {
// go inside the wallet
await element(by.text('Imported HD SegWit (BIP84 Bech32 Native)')).tap();
await yo('SendButton');
await element(by.id('SendButton')).tap();
// lets create real transaction:
@ -202,23 +204,23 @@ describe('BlueWallet UI Tests - import BIP84 wallet', () => {
await element(by.text('OK')).tap();
// lest add another two outputs
await element(by.id('advancedOptionsMenuButton')).tap();
await element(by.id('HeaderMenuButton')).tap();
await element(by.text('Add Recipient')).tap();
await yo('Transaction1'); // adding a recipient autoscrolls it to the last one
await element(by.id('AddressInput').withAncestor(by.id('Transaction1'))).replaceText('bc1q063ctu6jhe5k4v8ka99qac8rcm2tzjjnuktyrl');
await element(by.id('BitcoinAmountInput').withAncestor(by.id('Transaction1'))).replaceText('0.0002\n');
await element(by.id('advancedOptionsMenuButton')).tap();
await element(by.id('HeaderMenuButton')).tap();
await element(by.text('Add Recipient')).tap();
await yo('Transaction2'); // adding a recipient autoscrolls it to the last one
// remove last output, check if second output is shown
await element(by.id('advancedOptionsMenuButton')).tap();
await element(by.id('HeaderMenuButton')).tap();
await element(by.text('Remove Recipient')).tap();
await yo('Transaction1');
// adding it again
await element(by.id('advancedOptionsMenuButton')).tap();
await element(by.id('HeaderMenuButton')).tap();
await element(by.text('Add Recipient')).tap();
await yo('Transaction2'); // adding a recipient autoscrolls it to the last one
await element(by.id('AddressInput').withAncestor(by.id('Transaction2'))).replaceText('bc1qh6tf004ty7z7un2v5ntu4mkf630545gvhs45u7');
@ -227,7 +229,7 @@ describe('BlueWallet UI Tests - import BIP84 wallet', () => {
// remove second output
await element(by.id('Transaction2')).swipe('right', 'fast', NaN, 0.2);
await sleep(5000);
await element(by.id('advancedOptionsMenuButton')).tap();
await element(by.id('HeaderMenuButton')).tap();
await element(by.text('Remove Recipient')).tap();
// creating and verifying. tx should have 3 outputs
@ -262,7 +264,7 @@ describe('BlueWallet UI Tests - import BIP84 wallet', () => {
// go inside the wallet
await element(by.text('Imported HD SegWit (BIP84 Bech32 Native)')).tap();
await yo('SendButton');
await element(by.id('SendButton')).tap();
// set fee rate
@ -275,7 +277,7 @@ describe('BlueWallet UI Tests - import BIP84 wallet', () => {
// first send MAX output
await element(by.id('AddressInput')).replaceText('bc1qnapskphjnwzw2w3dk4anpxntunc77v6qrua0f7');
await element(by.id('BitcoinAmountInput')).typeText('0.0001\n');
await element(by.id('advancedOptionsMenuButton')).tap();
await element(by.id('HeaderMenuButton')).tap();
await element(by.text('Use Full Balance')).tap();
await element(by.text('OK')).tap();
@ -294,7 +296,7 @@ describe('BlueWallet UI Tests - import BIP84 wallet', () => {
// add second output with amount
await device.pressBack();
await device.pressBack();
await element(by.id('advancedOptionsMenuButton')).tap();
await element(by.id('HeaderMenuButton')).tap();
await element(by.text('Add Recipient')).tap();
await yo('Transaction1');
await element(by.id('AddressInput').withAncestor(by.id('Transaction1'))).replaceText('bc1q063ctu6jhe5k4v8ka99qac8rcm2tzjjnuktyrl');
@ -332,9 +334,10 @@ describe('BlueWallet UI Tests - import BIP84 wallet', () => {
// go inside the wallet
await element(by.text('Imported HD SegWit (BIP84 Bech32 Native)')).tap();
await yo('SendButton');
await element(by.id('SendButton')).tap();
await element(by.id('advancedOptionsMenuButton')).tap();
await element(by.id('HeaderMenuButton')).tap();
await element(by.text('Sign a transaction')).tap();
// tapping 5 times invisible button is a backdoor:
@ -391,6 +394,7 @@ describe('BlueWallet UI Tests - import BIP84 wallet', () => {
// go to receive screen and check that payment code is there
await yo('ReceiveButton');
await element(by.id('ReceiveButton')).tap();
try {
@ -451,15 +455,17 @@ describe('BlueWallet UI Tests - import BIP84 wallet', () => {
await device.pressBack();
await device.pressBack();
await element(by.text('Imported HD SegWit (BIP84 Bech32 Native)')).tap();
await yo('SendButton');
await element(by.id('SendButton')).tap();
await element(by.id('advancedOptionsMenuButton')).tap();
await element(by.id('HeaderMenuButton')).tap();
await element(by.text('Insert Contact')).tap();
await element(by.id('ContactListItem0')).tap();
await element(by.id('BitcoinAmountInput')).typeText('0.0001\n');
await element(by.id('advancedOptionsMenuButton')).tap();
await element(by.id('HeaderMenuButton')).tap();
await element(by.text('Add Recipient')).tap();
await element(by.id('advancedOptionsMenuButton')).tap();
await element(by.id('HeaderMenuButton')).tap();
await element(by.text('Insert Contact')).tap();
await element(by.id('ContactListItem1')).tap();
await element(by.id('BitcoinAmountInput')).atIndex(1).typeText('0.0002\n');
@ -600,8 +606,9 @@ describe('BlueWallet UI Tests - import BIP84 wallet', () => {
await device.launchApp({ newInstance: true });
await yo('WalletsList');
await element(by.text('Imported HD SegWit (BIP84 Bech32 Native)')).tap();
await yo('SendButton');
await element(by.id('SendButton')).tap();
await element(by.id('advancedOptionsMenuButton')).tap();
await element(by.id('HeaderMenuButton')).tap();
await element(by.text('Coin Control')).tap();
await waitFor(element(by.id('Loading'))) // wait for outputs to be loaded
.not.toExist()
@ -620,7 +627,7 @@ describe('BlueWallet UI Tests - import BIP84 wallet', () => {
await element(by.text('test2')).atIndex(0).tap();
await element(by.id('UseCoin')).tap();
await element(by.id('AddressInput')).replaceText('bc1q063ctu6jhe5k4v8ka99qac8rcm2tzjjnuktyrl');
await element(by.id('advancedOptionsMenuButton')).tap();
await element(by.id('HeaderMenuButton')).tap();
await element(by.text('Use Full Balance')).tap();
await element(by.text('OK')).tap();
// setting fee rate:
@ -647,9 +654,10 @@ describe('BlueWallet UI Tests - import BIP84 wallet', () => {
await device.pressBack();
// create tx with unfrozen input
await yo('SendButton');
await element(by.id('SendButton')).tap();
await element(by.id('AddressInput')).replaceText('bc1q063ctu6jhe5k4v8ka99qac8rcm2tzjjnuktyrl');
await element(by.id('advancedOptionsMenuButton')).tap();
await element(by.id('HeaderMenuButton')).tap();
await element(by.text('Use Full Balance')).tap();
await element(by.text('OK')).tap();
// setting fee rate:

View file

@ -29,7 +29,6 @@ describe('BlueWallet UI Tests - import Watch-only wallet (zpub)', () => {
'0.0001',
);
await sleep(15000);
await element(by.id('ReceiveButton')).tap();
try {
// in case emulator has no google services and doesnt support pushes
@ -51,11 +50,10 @@ describe('BlueWallet UI Tests - import Watch-only wallet (zpub)', () => {
await expect(element(by.text('bitcoin:bc1qc8wun6lf9vcajpddtgdpd2pdrp0kwp29j6upgv?amount=1&label=Test'))).toBeVisible();
await device.pressBack();
await element(by.id('SendButton')).tap();
await element(by.text('OK')).tap();
await element(by.id('advancedOptionsMenuButton')).tap();
await element(by.id('HeaderMenuButton')).tap();
await element(by.text('Import Transaction (QR)')).tap(); // opens camera
// produced by real Keystone device using MNEMONICS_KEYSTONE