Merge branch 'master' into notio

This commit is contained in:
Marcos Rodriguez Velez 2024-10-11 00:02:03 -04:00
commit 6dc7930ba5
67 changed files with 950 additions and 438 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

@ -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,
}: {
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);
}
if (Platform.OS !== 'android') {
type = AlertType.Alert;
}
// 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);
RNAlert.alert(title ?? message, title && message ? message : undefined, wrappedButtons, options);
break;
}
};
};
})();
export default presentAlert;

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,7 +139,6 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
);
const mainLogicPromise = new Promise<void>((resolve, reject) => {
try {
InteractionManager.runAfterInteractions(async () => {
let noErr = true;
try {
@ -148,30 +149,27 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
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');
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.warn(err);
console.error(err);
reject(err);
} finally {
setWalletTransactionUpdateStatus(WalletTransactionsStatus.NONE);
}
if (noErr) await saveToDisk(); // caching
if (noErr) await saveToDisk();
resolve();
});
} catch (err) {
reject(err);
} finally {
setWalletTransactionUpdateStatus(WalletTransactionsStatus.NONE);
}
});
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

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

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.0 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" : {

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

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

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

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

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

@ -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 => {
const saveWallet = useCallback(
(wallet: TWallet | THDWalletForWatchOnly) => {
if (importing.current) return;
importing.current = true;
addAndSaveWallet(wallet);
navigation.getParent().pop();
};
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

@ -128,17 +128,23 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
}
}, [wallet, isElectrumDisabled, isLoading, saveToDisk, pageSize]);
useEffect(() => {
useFocusEffect(
useCallback(() => {
const task = InteractionManager.runAfterInteractions(() => {
if (wallet && wallet.getLastTxFetch() === 0) {
refreshTransactions();
}
}, [wallet, 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

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

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:
@ -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
@ -332,6 +334,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();
await element(by.id('HeaderMenuButton')).tap();
@ -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,6 +455,8 @@ 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('HeaderMenuButton')).tap();
await element(by.text('Insert Contact')).tap();
@ -600,6 +606,7 @@ 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('HeaderMenuButton')).tap();
await element(by.text('Coin Control')).tap();
@ -647,6 +654,7 @@ 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('HeaderMenuButton')).tap();

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,7 +50,6 @@ 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();