Merge branch 'master' into notio
@ -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
|
||||
|
@ -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 },
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
|
@ -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 = ({
|
||||
|
||||
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;
|
||||
|
@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -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}
|
||||
>
|
||||
|
89
components/SettingsBlockExplorerCustomUrlListItem.tsx
Normal 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,
|
||||
},
|
||||
});
|
@ -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) => {
|
||||
|
@ -31,6 +31,7 @@ export const BlueDefaultTheme = {
|
||||
outgoingForegroundColor: '#d0021b',
|
||||
successColor: '#37c0a1',
|
||||
failedColor: '#ff0000',
|
||||
placeholderTextColor: '#81868e',
|
||||
shadowColor: '#000000',
|
||||
inverseForegroundColor: '#ffffff',
|
||||
hdborderColor: '#68BBE1',
|
||||
|
@ -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
|
||||
)
|
||||
|
BIN
ios/BlueWallet/Images.xcassets/AppIcon.appiconset/1024 1.png
Normal file
After Width: | Height: | Size: 41 KiB |
BIN
ios/BlueWallet/Images.xcassets/AppIcon.appiconset/1024 2.png
Normal file
After Width: | Height: | Size: 193 KiB |
Before Width: | Height: | Size: 180 KiB After Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 10 KiB |
BIN
ios/BlueWallet/Images.xcassets/AppIcon.appiconset/128pt@1x.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
ios/BlueWallet/Images.xcassets/AppIcon.appiconset/128pt@2x.png
Normal file
After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 767 B |
BIN
ios/BlueWallet/Images.xcassets/AppIcon.appiconset/16pt@1x.png
Normal file
After Width: | Height: | Size: 333 B |
BIN
ios/BlueWallet/Images.xcassets/AppIcon.appiconset/16pt@2x.png
Normal file
After Width: | Height: | Size: 614 B |
Before Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 29 KiB |
BIN
ios/BlueWallet/Images.xcassets/AppIcon.appiconset/256pt@1x.png
Normal file
After Width: | Height: | Size: 5.3 KiB |
BIN
ios/BlueWallet/Images.xcassets/AppIcon.appiconset/256pt@2x.png
Normal file
After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.9 KiB |
BIN
ios/BlueWallet/Images.xcassets/AppIcon.appiconset/32pt@1x.png
Normal file
After Width: | Height: | Size: 614 B |
BIN
ios/BlueWallet/Images.xcassets/AppIcon.appiconset/32pt@2x.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 75 KiB |
Before Width: | Height: | Size: 75 KiB |
BIN
ios/BlueWallet/Images.xcassets/AppIcon.appiconset/512pt@1x.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
ios/BlueWallet/Images.xcassets/AppIcon.appiconset/512pt@2x.png
Normal file
After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 9.3 KiB |
Before Width: | Height: | Size: 9.3 KiB |
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 20 KiB |
@ -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" : {
|
||||
|
@ -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}",
|
||||
|
@ -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
@ -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;
|
||||
}
|
||||
};
|
@ -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;
|
||||
|
@ -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"
|
||||
|
@ -54,6 +54,7 @@ export type DetailViewStackParamList = {
|
||||
About: undefined;
|
||||
DefaultView: undefined;
|
||||
ElectrumSettings: undefined;
|
||||
SettingsBlockExplorer: undefined;
|
||||
EncryptStorage: undefined;
|
||||
Language: undefined;
|
||||
LightningSettings: {
|
||||
|
@ -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'));
|
||||
|
@ -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 />
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
/>
|
219
screen/settings/SettingsBlockExplorer.tsx
Normal 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,
|
||||
},
|
||||
});
|
@ -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>
|
||||
|
@ -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}>
|
||||
|
@ -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: {
|
@ -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={
|
||||
|
@ -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');
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
||||
|