Merge pull request #7271 from BlueWallet/event

REF: useMenuElements
This commit is contained in:
GLaDOS 2024-11-09 12:29:41 +00:00 committed by GitHub
commit e51433105b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 828 additions and 605 deletions

View File

@ -27,8 +27,11 @@ function Notifications(props) {
let token = await AsyncStorage.getItem(PUSH_TOKEN);
token = JSON.parse(token);
return token;
} catch (_) {}
return false;
} catch (e) {
console.error(e);
AsyncStorage.removeItem(PUSH_TOKEN);
throw e;
}
};
/**
@ -287,7 +290,21 @@ function Notifications(props) {
};
Notifications.getSavedUri = async function () {
return AsyncStorage.getItem(GROUNDCONTROL_BASE_URI);
try {
const baseUriStored = await AsyncStorage.getItem(GROUNDCONTROL_BASE_URI);
if (baseUriStored) {
baseURI = baseUriStored;
}
return baseUriStored;
} catch (e) {
console.error(e);
try {
await AsyncStorage.setItem(GROUNDCONTROL_BASE_URI, groundControlUri);
} catch (storageError) {
console.error('Failed to reset URI:', storageError);
}
throw e;
}
};
/**
@ -385,7 +402,16 @@ function Notifications(props) {
const stringified = await AsyncStorage.getItem(NOTIFICATIONS_STORAGE);
notifications = JSON.parse(stringified);
if (!Array.isArray(notifications)) notifications = [];
} catch (_) {}
} catch (e) {
if (e instanceof SyntaxError) {
console.error('Invalid notifications format:', e);
notifications = [];
await AsyncStorage.setItem(NOTIFICATIONS_STORAGE, '[]');
} else {
console.error('Error accessing notifications:', e);
throw e;
}
}
return notifications;
};
@ -396,7 +422,11 @@ function Notifications(props) {
const stringified = await AsyncStorage.getItem(NOTIFICATIONS_STORAGE);
notifications = JSON.parse(stringified);
if (!Array.isArray(notifications)) notifications = [];
} catch (_) {}
} catch (e) {
console.error(e);
// Start fresh with just the new notification
notifications = [];
}
notifications.push(notification);
await AsyncStorage.setItem(NOTIFICATIONS_STORAGE, JSON.stringify(notifications));
@ -420,7 +450,11 @@ function Notifications(props) {
app_version: appVersion,
}),
});
} catch (_) {}
} catch (e) {
console.error(e);
await AsyncStorage.setItem('lang', 'en');
throw e;
}
};
Notifications.clearStoredNotifications = async function () {
@ -455,7 +489,13 @@ function Notifications(props) {
if (baseUriStored) {
baseURI = baseUriStored;
}
} catch (_) {}
} catch (e) {
console.error(e);
console.warn('Failed to load custom URI, falling back to default');
baseURI = groundControlUri;
// Attempt to reset in background
AsyncStorage.setItem(GROUNDCONTROL_BASE_URI, groundControlUri).catch(err => console.error('Failed to reset URI:', err));
}
// every launch should clear badges:
Notifications.setApplicationIconBadgeNumber(0);

View File

@ -2,8 +2,7 @@ import 'react-native-gesture-handler'; // should be on top
import { CommonActions } from '@react-navigation/native';
import React, { lazy, Suspense, useCallback, useEffect, useRef } from 'react';
import { AppState, AppStateStatus, Linking, NativeEventEmitter, NativeModules, Platform } from 'react-native';
import { AppState, AppStateStatus, Linking } from 'react-native';
import A from '../blue_modules/analytics';
import BlueClipboard from '../blue_modules/clipboard';
import { updateExchangeRate } from '../blue_modules/currency';
@ -18,15 +17,13 @@ import ActionSheet from '../screen/ActionSheet';
import { useStorage } from '../hooks/context/useStorage';
import RNQRGenerator from 'rn-qr-generator';
import presentAlert from './Alert';
import useMenuElements from '../hooks/useMenuElements';
import { useSettings } from '../hooks/context/useSettings';
import useWidgetCommunication from '../hooks/useWidgetCommunication';
import useWatchConnectivity from '../hooks/useWatchConnectivity';
const MenuElements = lazy(() => import('../components/MenuElements'));
const DeviceQuickActions = lazy(() => import('../components/DeviceQuickActions'));
const HandOffComponentListener = lazy(() => import('../components/HandOffComponentListener'));
const WidgetCommunication = lazy(() => import('../components/WidgetCommunication'));
const WatchConnectivity = lazy(() => import('./WatchConnectivity'));
// @ts-ignore: NativeModules.EventEmitter is not typed
const eventEmitter = Platform.OS === 'ios' ? new NativeEventEmitter(NativeModules.EventEmitter) : undefined;
const ClipboardContentType = Object.freeze({
BITCOIN: 'BITCOIN',
@ -36,8 +33,14 @@ const ClipboardContentType = Object.freeze({
const CompanionDelegates = () => {
const { wallets, addWallet, saveToDisk, fetchAndSaveWalletTransactions, refreshAllWalletTransactions, setSharedCosigner } = useStorage();
const appState = useRef<AppStateStatus>(AppState.currentState);
const { isHandOffUseEnabled, isQuickActionsEnabled } = useSettings();
const clipboardContent = useRef<undefined | string>();
useWatchConnectivity();
useWidgetCommunication();
useMenuElements();
const processPushNotifications = useCallback(async () => {
await new Promise(resolve => setTimeout(resolve, 200));
// @ts-ignore: Notifications type is not defined
@ -223,32 +226,15 @@ const CompanionDelegates = () => {
[processPushNotifications, showClipboardAlert, wallets],
);
const onNotificationReceived = useCallback(
async (notification: { data: { data: any } }) => {
const payload = Object.assign({}, notification, notification.data);
if (notification.data && notification.data.data) Object.assign(payload, notification.data.data);
// @ts-ignore: Notifications type is not defined
payload.foreground = true;
// @ts-ignore: Notifications type is not defined
await Notifications.addNotification(payload);
// @ts-ignore: Notifications type is not defined
if (payload.foreground) await processPushNotifications();
},
[processPushNotifications],
);
const addListeners = useCallback(() => {
const urlSubscription = Linking.addEventListener('url', handleOpenURL);
const appStateSubscription = AppState.addEventListener('change', handleAppStateChange);
const notificationSubscription = eventEmitter?.addListener('onNotificationReceived', onNotificationReceived);
return {
urlSubscription,
appStateSubscription,
notificationSubscription,
};
}, [handleOpenURL, handleAppStateChange, onNotificationReceived]);
}, [handleOpenURL, handleAppStateChange]);
useEffect(() => {
const subscriptions = addListeners();
@ -256,7 +242,6 @@ const CompanionDelegates = () => {
return () => {
subscriptions.urlSubscription?.remove();
subscriptions.appStateSubscription?.remove();
subscriptions.notificationSubscription?.remove();
};
}, [addListeners]);
@ -264,11 +249,8 @@ const CompanionDelegates = () => {
<>
<Notifications onProcessNotifications={processPushNotifications} />
<Suspense fallback={null}>
<MenuElements />
<DeviceQuickActions />
<HandOffComponentListener />
<WidgetCommunication />
<WatchConnectivity />
{isQuickActionsEnabled && <DeviceQuickActions />}
{isHandOffUseEnabled && <HandOffComponentListener />}
</Suspense>
</>
);

View File

@ -9,12 +9,12 @@ import { saveLanguage, STORAGE_KEY } from '../../loc';
import { FiatUnit, TFiatUnit } from '../../models/fiatUnit';
import { getEnabled as getIsDeviceQuickActionsEnabled, setEnabled as setIsDeviceQuickActionsEnabled } from '../DeviceQuickActions';
import { getIsHandOffUseEnabled, setIsHandOffUseEnabled } from '../HandOffComponent';
import { isBalanceDisplayAllowed, setBalanceDisplayAllowed } from '../WidgetCommunication';
import { useStorage } from '../../hooks/context/useStorage';
import { BitcoinUnit } from '../../models/bitcoinUnits';
import { TotalWalletsBalanceKey, TotalWalletsBalancePreferredUnit } from '../TotalWalletsBalance';
import { BLOCK_EXPLORERS, getBlockExplorerUrl, saveBlockExplorer, BlockExplorer, normalizeUrl } from '../../models/blockExplorer';
import * as BlueElectrum from '../../blue_modules/BlueElectrum';
import { isBalanceDisplayAllowed, setBalanceDisplayAllowed } from '../../hooks/useWidgetCommunication';
const getDoNotTrackStorage = async (): Promise<boolean> => {
try {
@ -77,7 +77,7 @@ interface SettingsContextType {
isHandOffUseEnabled: boolean;
setIsHandOffUseEnabledAsyncStorage: (value: boolean) => Promise<void>;
isPrivacyBlurEnabled: boolean;
setIsPrivacyBlurEnabledState: (value: boolean) => void;
setIsPrivacyBlurEnabled: (value: boolean) => void;
isDoNotTrackEnabled: boolean;
setDoNotTrackStorage: (value: boolean) => Promise<void>;
isWidgetBalanceDisplayAllowed: boolean;
@ -108,7 +108,7 @@ const defaultSettingsContext: SettingsContextType = {
isHandOffUseEnabled: false,
setIsHandOffUseEnabledAsyncStorage: async () => {},
isPrivacyBlurEnabled: true,
setIsPrivacyBlurEnabledState: () => {},
setIsPrivacyBlurEnabled: () => {},
isDoNotTrackEnabled: false,
setDoNotTrackStorage: async () => {},
isWidgetBalanceDisplayAllowed: true,
@ -314,16 +314,6 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = React.m
console.error('Error setting isQuickActionsEnabled:', e);
}
}, []);
const setIsPrivacyBlurEnabledState = useCallback((value: boolean): void => {
try {
setIsPrivacyBlurEnabled(value);
console.debug(`Privacy blur: ${value}`);
} catch (e) {
console.error('Error setting isPrivacyBlurEnabled:', e);
}
}, []);
const setIsTotalBalanceEnabledStorage = useCallback(async (value: boolean): Promise<void> => {
try {
await setTotalBalanceViewEnabledStorage(value);
@ -364,7 +354,7 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = React.m
isHandOffUseEnabled,
setIsHandOffUseEnabledAsyncStorage,
isPrivacyBlurEnabled,
setIsPrivacyBlurEnabledState,
setIsPrivacyBlurEnabled,
isDoNotTrackEnabled,
setDoNotTrackStorage,
isWidgetBalanceDisplayAllowed,
@ -394,7 +384,7 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = React.m
isHandOffUseEnabled,
setIsHandOffUseEnabledAsyncStorage,
isPrivacyBlurEnabled,
setIsPrivacyBlurEnabledState,
setIsPrivacyBlurEnabled,
isDoNotTrackEnabled,
setDoNotTrackStorage,
isWidgetBalanceDisplayAllowed,

View File

@ -35,8 +35,6 @@ interface StorageContextType {
resetWallets: () => void;
walletTransactionUpdateStatus: WalletTransactionsStatus | string;
setWalletTransactionUpdateStatus: (status: WalletTransactionsStatus | string) => void;
reloadTransactionsMenuActionFunction: () => void;
setReloadTransactionsMenuActionFunction: (func: () => void) => void;
getTransactions: typeof BlueApp.getTransactions;
fetchWalletBalances: typeof BlueApp.fetchWalletBalances;
fetchWalletTransactions: typeof BlueApp.fetchWalletTransactions;
@ -72,7 +70,6 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
);
const [walletsInitialized, setWalletsInitialized] = useState<boolean>(false);
const [currentSharedCosigner, setCurrentSharedCosigner] = useState<string>('');
const [reloadTransactionsMenuActionFunction, setReloadTransactionsMenuActionFunction] = useState<() => void>(() => {});
const saveToDisk = useCallback(
async (force: boolean = false) => {
@ -273,8 +270,6 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
isPasswordInUse: BlueApp.isPasswordInUse,
walletTransactionUpdateStatus,
setWalletTransactionUpdateStatus,
reloadTransactionsMenuActionFunction,
setReloadTransactionsMenuActionFunction,
}),
[
wallets,
@ -293,8 +288,6 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
resetWallets,
walletTransactionUpdateStatus,
setWalletTransactionUpdateStatus,
reloadTransactionsMenuActionFunction,
setReloadTransactionsMenuActionFunction,
],
);

View File

@ -1,69 +0,0 @@
import { CommonActions } from '@react-navigation/native';
import { useCallback, useEffect } from 'react';
import { NativeEventEmitter, NativeModules, Platform } from 'react-native';
import * as NavigationService from '../NavigationService';
import { useStorage } from '../hooks/context/useStorage';
/*
Component for iPadOS and macOS menu items with keyboard shortcuts.
EventEmitter on the native side should receive a payload and rebuild menus.
*/
const eventEmitter = Platform.OS === 'ios' || Platform.OS === 'macos' ? new NativeEventEmitter(NativeModules.EventEmitter) : undefined;
const MenuElements = () => {
const { walletsInitialized, reloadTransactionsMenuActionFunction } = useStorage();
// BlueWallet -> Settings
const openSettings = useCallback(() => {
dispatchNavigate('Settings');
}, []);
// File -> Add Wallet
const addWalletMenuAction = useCallback(() => {
dispatchNavigate('AddWalletRoot');
}, []);
// File -> Add Wallet
const importWalletMenuAction = useCallback(() => {
dispatchNavigate('AddWalletRoot', 'ImportWallet');
}, []);
const dispatchNavigate = (routeName: string, screen?: string) => {
const action = screen
? CommonActions.navigate({
name: routeName,
params: { screen },
})
: CommonActions.navigate({
name: routeName,
});
NavigationService.dispatch(action);
};
const reloadTransactionsMenuElementsFunction = useCallback(() => {
if (reloadTransactionsMenuActionFunction && typeof reloadTransactionsMenuActionFunction === 'function') {
reloadTransactionsMenuActionFunction();
}
}, [reloadTransactionsMenuActionFunction]);
useEffect(() => {
console.debug('MenuElements: useEffect');
if (walletsInitialized) {
eventEmitter?.addListener('openSettings', openSettings);
eventEmitter?.addListener('addWalletMenuAction', addWalletMenuAction);
eventEmitter?.addListener('importWalletMenuAction', importWalletMenuAction);
eventEmitter?.addListener('reloadTransactionsMenuAction', reloadTransactionsMenuElementsFunction);
}
return () => {
eventEmitter?.removeAllListeners('openSettings');
eventEmitter?.removeAllListeners('addWalletMenuAction');
eventEmitter?.removeAllListeners('importWalletMenuAction');
eventEmitter?.removeAllListeners('reloadTransactionsMenuAction');
};
}, [addWalletMenuAction, importWalletMenuAction, openSettings, reloadTransactionsMenuElementsFunction, walletsInitialized]);
return null;
};
export default MenuElements;

View File

@ -1,5 +0,0 @@
const MenuElements = () => {
return null;
};
export default MenuElements;

View File

@ -1,246 +0,0 @@
import React, { useEffect, useRef } from 'react';
import {
transferCurrentComplicationUserInfo,
transferUserInfo,
updateApplicationContext,
useInstalled,
useReachability,
watchEvents,
} from 'react-native-watch-connectivity';
import Notifications from '../blue_modules/notifications';
import { MultisigHDWallet } from '../class';
import loc, { formatBalance, transactionTimeToReadable } from '../loc';
import { Chain } from '../models/bitcoinUnits';
import { FiatUnit } from '../models/fiatUnit';
import { useSettings } from '../hooks/context/useSettings';
import { useStorage } from '../hooks/context/useStorage';
function WatchConnectivity() {
const { walletsInitialized, wallets, fetchWalletTransactions, saveToDisk, txMetadata } = useStorage();
const { preferredFiatCurrency } = useSettings();
const isReachable = useReachability();
const isInstalled = useInstalled(); // true | false
const messagesListenerActive = useRef(false);
const lastPreferredCurrency = useRef(FiatUnit.USD.endPointKey);
useEffect(() => {
let messagesListener = () => {};
if (isInstalled && isReachable && walletsInitialized && messagesListenerActive.current === false) {
messagesListener = watchEvents.addListener('message', handleMessages);
messagesListenerActive.current = true;
} else {
messagesListener();
messagesListenerActive.current = false;
}
return () => {
messagesListener();
messagesListenerActive.current = false;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [walletsInitialized, isReachable, isInstalled]);
useEffect(() => {
console.log(`Apple Watch: isInstalled: ${isInstalled}, isReachable: ${isReachable}, walletsInitialized: ${walletsInitialized}`);
if (isInstalled && walletsInitialized) {
constructWalletsToSendToWatch().then(walletsToProcess => {
if (walletsToProcess) {
if (isReachable) {
transferUserInfo(walletsToProcess);
console.log('Apple Watch: sent info to watch transferUserInfo');
} else {
updateApplicationContext(walletsToProcess);
console.log('Apple Watch: sent info to watch context');
}
}
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [walletsInitialized, wallets, isReachable, isInstalled]);
useEffect(() => {
if (walletsInitialized && isReachable && isInstalled) {
updateApplicationContext({ isWalletsInitialized: walletsInitialized, randomID: Math.floor(Math.random() * 11) });
}
}, [isInstalled, isReachable, walletsInitialized]);
useEffect(() => {
if (isInstalled && isReachable && walletsInitialized && preferredFiatCurrency) {
const preferredFiatCurrencyParsed = preferredFiatCurrency ?? FiatUnit.USD;
try {
if (lastPreferredCurrency.current !== preferredFiatCurrencyParsed.endPointKey) {
transferCurrentComplicationUserInfo({
preferredFiatCurrency: preferredFiatCurrencyParsed.endPointKey,
});
lastPreferredCurrency.current = preferredFiatCurrency.endPointKey;
} else {
console.log('WatchConnectivity lastPreferredCurrency has not changed');
}
} catch (e) {
console.log('WatchConnectivity useEffect preferredFiatCurrency error');
console.log(e);
}
}
}, [preferredFiatCurrency, walletsInitialized, isReachable, isInstalled]);
const handleMessages = (message, reply) => {
if (message.request === 'createInvoice') {
handleLightningInvoiceCreateRequest(message.walletIndex, message.amount, message.description)
.then(createInvoiceRequest => reply({ invoicePaymentRequest: createInvoiceRequest }))
.catch(e => {
console.log(e);
reply({});
});
} else if (message.message === 'sendApplicationContext') {
constructWalletsToSendToWatch().then(walletsToProcess => {
if (walletsToProcess) {
updateApplicationContext(walletsToProcess);
}
});
} else if (message.message === 'fetchTransactions') {
fetchWalletTransactions()
.then(() => saveToDisk())
.finally(() => reply({}));
} else if (message.message === 'hideBalance') {
const walletIndex = message.walletIndex;
const wallet = wallets[walletIndex];
wallet.hideBalance = message.hideBalance;
saveToDisk().finally(() => reply({}));
}
};
const handleLightningInvoiceCreateRequest = async (walletIndex, amount, description = loc.lnd.placeholder) => {
const wallet = wallets[walletIndex];
if (wallet.allowReceive() && amount > 0) {
try {
const invoiceRequest = await wallet.addInvoice(amount, description);
// lets decode payreq and subscribe groundcontrol so we can receive push notification when our invoice is paid
try {
// Let's verify if notifications are already configured. Otherwise the watch app will freeze waiting for user approval in iOS app
if (await Notifications.isNotificationsEnabled()) {
const decoded = await wallet.decodeInvoice(invoiceRequest);
Notifications.majorTomToGroundControl([], [decoded.payment_hash], []);
}
} catch (e) {
console.log('WatchConnectivity - Running in Simulator');
console.log(e);
}
return invoiceRequest;
} catch (error) {
return error;
}
}
};
const constructWalletsToSendToWatch = async () => {
if (!Array.isArray(wallets)) {
console.log('No Wallets set to sync with Watch app. Exiting...');
return;
}
if (!walletsInitialized) {
console.log('Wallets not initialized. Exiting...');
return;
}
const walletsToProcess = [];
for (const wallet of wallets) {
let receiveAddress;
if (wallet.chain === Chain.ONCHAIN) {
try {
receiveAddress = await wallet.getAddressAsync();
} catch (_) {}
if (!receiveAddress) {
// either sleep expired or getAddressAsync threw an exception
receiveAddress = wallet._getExternalAddressByIndex(wallet.next_free_address_index);
}
} else if (wallet.chain === Chain.OFFCHAIN) {
try {
await wallet.getAddressAsync();
receiveAddress = wallet.getAddress();
} catch (_) {}
if (!receiveAddress) {
// either sleep expired or getAddressAsync threw an exception
receiveAddress = wallet.getAddress();
}
}
const transactions = wallet.getTransactions(10);
const watchTransactions = [];
for (const transaction of transactions) {
let type = 'pendingConfirmation';
let memo = '';
let amount = 0;
if ('confirmations' in transaction && !(transaction.confirmations > 0)) {
type = 'pendingConfirmation';
} else if (transaction.type === 'user_invoice' || transaction.type === 'payment_request') {
const currentDate = new Date();
const now = (currentDate.getTime() / 1000) | 0; // eslint-disable-line no-bitwise
const invoiceExpiration = transaction.timestamp + transaction.expire_time;
if (invoiceExpiration > now) {
type = 'pendingConfirmation';
} else if (invoiceExpiration < now) {
if (transaction.ispaid) {
type = 'received';
} else {
type = 'sent';
}
}
} else if (transaction.value / 100000000 < 0) {
type = 'sent';
} else {
type = 'received';
}
if (transaction.type === 'user_invoice' || transaction.type === 'payment_request') {
amount = isNaN(transaction.value) ? '0' : amount;
const currentDate = new Date();
const now = (currentDate.getTime() / 1000) | 0; // eslint-disable-line no-bitwise
const invoiceExpiration = transaction.timestamp + transaction.expire_time;
if (invoiceExpiration > now) {
amount = formatBalance(transaction.value, wallet.getPreferredBalanceUnit(), true).toString();
} else if (invoiceExpiration < now) {
if (transaction.ispaid) {
amount = formatBalance(transaction.value, wallet.getPreferredBalanceUnit(), true).toString();
} else {
amount = loc.lnd.expired;
}
} else {
amount = formatBalance(transaction.value, wallet.getPreferredBalanceUnit(), true).toString();
}
} else {
amount = formatBalance(transaction.value, wallet.getPreferredBalanceUnit(), true).toString();
}
if (txMetadata[transaction.hash] && txMetadata[transaction.hash].memo) {
memo = txMetadata[transaction.hash].memo;
} else if (transaction.memo) {
memo = transaction.memo;
}
const watchTX = { type, amount, memo, time: transactionTimeToReadable(transaction.received) };
watchTransactions.push(watchTX);
}
const walletInformation = {
label: wallet.getLabel(),
balance: formatBalance(Number(wallet.getBalance()), wallet.getPreferredBalanceUnit(), true),
type: wallet.type,
preferredBalanceUnit: wallet.getPreferredBalanceUnit(),
receiveAddress,
transactions: watchTransactions,
hideBalance: wallet.hideBalance,
};
if (wallet.chain === Chain.ONCHAIN && wallet.type !== MultisigHDWallet.type) {
walletInformation.xpub = wallet.getXpub() ? wallet.getXpub() : wallet.getSecret();
}
if (wallet.allowBIP47() && wallet.isBIP47Enabled()) {
walletInformation.paymentCode = wallet.getBIP47PaymentCode();
}
walletsToProcess.push(walletInformation);
}
return { wallets: walletsToProcess, randomID: Math.floor(Math.random() * 11) };
};
return <></>;
}
export default WatchConnectivity;

View File

@ -1,4 +0,0 @@
const WatchConnectivity = () => {
return null;
};
export default WatchConnectivity;

View File

@ -1,91 +0,0 @@
import React, { useEffect, useCallback } from 'react';
import DefaultPreference from 'react-native-default-preference';
import { TWallet, Transaction } from '../class/wallets/types';
import { useSettings } from '../hooks/context/useSettings';
import { useStorage } from '../hooks/context/useStorage';
import { GROUP_IO_BLUEWALLET } from '../blue_modules/currency';
enum WidgetCommunicationKeys {
AllWalletsSatoshiBalance = 'WidgetCommunicationAllWalletsSatoshiBalance',
AllWalletsLatestTransactionTime = 'WidgetCommunicationAllWalletsLatestTransactionTime',
DisplayBalanceAllowed = 'WidgetCommunicationDisplayBalanceAllowed',
LatestTransactionIsUnconfirmed = 'WidgetCommunicationLatestTransactionIsUnconfirmed',
}
export const isBalanceDisplayAllowed = async (): Promise<boolean> => {
try {
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
const displayBalance = await DefaultPreference.get(WidgetCommunicationKeys.DisplayBalanceAllowed);
return displayBalance === '1';
} catch {
await setBalanceDisplayAllowed(true);
return true;
}
};
export const setBalanceDisplayAllowed = async (value: boolean): Promise<void> => {
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
if (value) {
await DefaultPreference.set(WidgetCommunicationKeys.DisplayBalanceAllowed, '1');
} else {
await DefaultPreference.clear(WidgetCommunicationKeys.DisplayBalanceAllowed);
}
};
const allWalletsBalanceAndTransactionTime = async (
wallets: TWallet[],
walletsInitialized: boolean,
): Promise<{ allWalletsBalance: number; latestTransactionTime: number | string }> => {
if (!walletsInitialized || !(await isBalanceDisplayAllowed())) {
return { allWalletsBalance: 0, latestTransactionTime: 0 };
}
let balance = 0;
let latestTransactionTime: number | string = 0;
for (const wallet of wallets) {
if (wallet.hideBalance) continue;
balance += await wallet.getBalance();
const transactions: Transaction[] = await wallet.getTransactions();
for (const transaction of transactions) {
const transactionTime = await wallet.getLatestTransactionTimeEpoch();
if (transaction.confirmations > 0 && transactionTime > Number(latestTransactionTime)) {
latestTransactionTime = transactionTime;
}
}
if (latestTransactionTime === 0 && transactions[0]?.confirmations === 0) {
latestTransactionTime = WidgetCommunicationKeys.LatestTransactionIsUnconfirmed;
}
}
return { allWalletsBalance: balance, latestTransactionTime };
};
const WidgetCommunication: React.FC = () => {
const { wallets, walletsInitialized } = useStorage();
const { isWidgetBalanceDisplayAllowed } = useSettings();
const syncWidgetBalanceWithWallets = useCallback(async (): Promise<void> => {
try {
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
const { allWalletsBalance, latestTransactionTime } = await allWalletsBalanceAndTransactionTime(wallets, walletsInitialized);
await Promise.all([
DefaultPreference.set(WidgetCommunicationKeys.AllWalletsSatoshiBalance, String(allWalletsBalance)),
DefaultPreference.set(WidgetCommunicationKeys.AllWalletsLatestTransactionTime, String(latestTransactionTime)),
]);
} catch (error) {
console.error('Failed to sync widget balance with wallets:', error);
}
}, [wallets, walletsInitialized]);
useEffect(() => {
if (walletsInitialized) {
syncWidgetBalanceWithWallets();
}
}, [wallets, walletsInitialized, isWidgetBalanceDisplayAllowed, syncWidgetBalanceWithWallets]);
return null;
};
export default WidgetCommunication;

View File

@ -1,13 +0,0 @@
import React from 'react';
export const isBalanceDisplayAllowed = async (): Promise<boolean> => {
return true;
};
export const setBalanceDisplayAllowed = async (value: boolean): Promise<void> => {};
const WidgetCommunication: React.FC = () => {
return null; // This component does not render anything.
};
export default WidgetCommunication;

View File

@ -0,0 +1,65 @@
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { NativeEventEmitter, NativeModules, Platform } from 'react-native';
import { CommonActions } from '@react-navigation/native';
import * as NavigationService from '../NavigationService';
import { useStorage } from './context/useStorage';
/*
Hook for managing iPadOS and macOS menu actions with keyboard shortcuts.
Uses MenuElementsEmitter for event handling.
*/
const { MenuElementsEmitter } = NativeModules;
const eventEmitter =
(Platform.OS === 'ios' || Platform.OS === 'macos') && MenuElementsEmitter ? new NativeEventEmitter(MenuElementsEmitter) : null;
const useMenuElements = () => {
const { walletsInitialized } = useStorage();
const reloadTransactionsMenuActionRef = useRef<() => void>(() => {});
const setReloadTransactionsMenuActionFunction = useCallback((newFunction: () => void) => {
console.debug('Setting reloadTransactionsMenuActionFunction.');
reloadTransactionsMenuActionRef.current = newFunction;
}, []);
const dispatchNavigate = useCallback((routeName: string, screen?: string) => {
NavigationService.dispatch(CommonActions.navigate({ name: routeName, params: screen ? { screen } : undefined }));
}, []);
const eventActions = useMemo(
() => ({
openSettings: () => dispatchNavigate('Settings'),
addWallet: () => dispatchNavigate('AddWalletRoot'),
importWallet: () => dispatchNavigate('AddWalletRoot', 'ImportWallet'),
reloadTransactions: () => {
console.debug('Calling reloadTransactionsMenuActionFunction');
reloadTransactionsMenuActionRef.current?.();
},
}),
[dispatchNavigate],
);
useEffect(() => {
if (!walletsInitialized || !eventEmitter) return;
console.debug('Setting up menu event listeners');
const listeners = [
eventEmitter.addListener('openSettings', eventActions.openSettings),
eventEmitter.addListener('addWalletMenuAction', eventActions.addWallet),
eventEmitter.addListener('importWalletMenuAction', eventActions.importWallet),
eventEmitter.addListener('reloadTransactionsMenuAction', eventActions.reloadTransactions),
];
return () => {
console.debug('Removing menu event listeners');
listeners.forEach(listener => listener.remove());
};
}, [walletsInitialized, eventActions]);
return {
setReloadTransactionsMenuActionFunction,
};
};
export default useMenuElements;

9
hooks/useMenuElements.ts Normal file
View File

@ -0,0 +1,9 @@
const useMenuElements = () => {
const setReloadTransactionsMenuActionFunction = (_: () => void) => {};
return {
setReloadTransactionsMenuActionFunction,
};
};
export default useMenuElements;

View File

@ -0,0 +1,241 @@
import { useCallback, useEffect, useRef } from 'react';
import {
transferCurrentComplicationUserInfo,
transferUserInfo,
updateApplicationContext,
useInstalled,
usePaired,
useReachability,
watchEvents,
} from 'react-native-watch-connectivity';
import Notifications from '../blue_modules/notifications';
import { MultisigHDWallet } from '../class';
import loc, { formatBalance, transactionTimeToReadable } from '../loc';
import { Chain } from '../models/bitcoinUnits';
import { FiatUnit } from '../models/fiatUnit';
import { useSettings } from '../hooks/context/useSettings';
import { useStorage } from '../hooks/context/useStorage';
interface Message {
request?: string;
message?: string;
walletIndex?: number;
amount?: number;
description?: string;
hideBalance?: boolean;
}
interface Reply {
(response: Record<string, any>): void;
}
interface LightningInvoiceCreateRequest {
walletIndex: number;
amount: number;
description?: string;
}
interface Transaction {
type: string;
amount: string;
memo: string;
time: string;
}
export function useWatchConnectivity() {
const { walletsInitialized, wallets, fetchWalletTransactions, saveToDisk, txMetadata } = useStorage();
const { preferredFiatCurrency } = useSettings();
const isReachable = useReachability();
const isInstalled = useInstalled();
const isPaired = usePaired();
const messagesListenerActive = useRef(false);
const lastPreferredCurrency = useRef(FiatUnit.USD.endPointKey);
// Set up message listener only when conditions are met
useEffect(() => {
if (!isInstalled || !isPaired || !walletsInitialized || !isReachable) {
console.debug('Apple Watch not installed, not paired, or other conditions not met. Exiting message listener setup.');
return;
}
const messagesListener = watchEvents.addListener('message', (message: any) => handleMessages(message, () => {}));
messagesListenerActive.current = true;
return () => {
messagesListener();
messagesListenerActive.current = false;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [walletsInitialized, isReachable, isInstalled, isPaired]);
// Send wallet data to Apple Watch
useEffect(() => {
if (!isInstalled || !isPaired || !walletsInitialized) return;
const sendWalletData = async () => {
try {
const walletsToProcess = await constructWalletsToSendToWatch();
if (walletsToProcess) {
if (isReachable) {
transferUserInfo(walletsToProcess);
console.debug('Apple Watch: sent info to watch transferUserInfo');
} else {
updateApplicationContext(walletsToProcess);
console.debug('Apple Watch: sent info to watch context');
}
}
} catch (error) {
console.debug('Failed to send wallets to watch:', error);
}
};
sendWalletData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [walletsInitialized, isReachable, isInstalled, isPaired]);
// Update application context with wallet status
useEffect(() => {
if (!isInstalled || !isPaired || !walletsInitialized || !isReachable) return;
updateApplicationContext({ isWalletsInitialized: walletsInitialized, randomID: Math.floor(Math.random() * 11) });
}, [isReachable, walletsInitialized, isInstalled, isPaired]);
// Update preferred fiat currency to Apple Watch if it changes
useEffect(() => {
if (!isInstalled || !isPaired || !walletsInitialized || !isReachable || !preferredFiatCurrency) return;
if (lastPreferredCurrency.current !== preferredFiatCurrency.endPointKey) {
try {
transferCurrentComplicationUserInfo({ preferredFiatCurrency: preferredFiatCurrency.endPointKey });
lastPreferredCurrency.current = preferredFiatCurrency.endPointKey;
console.debug('Apple Watch: updated preferred fiat currency');
} catch (error) {
console.debug('Error updating preferredFiatCurrency on watch:', error);
}
} else {
console.debug('WatchConnectivity lastPreferredCurrency has not changed');
}
}, [preferredFiatCurrency, walletsInitialized, isReachable, isInstalled, isPaired]);
const handleMessages = useCallback(
async (message: Message, reply: Reply) => {
try {
if (message.request === 'createInvoice') {
const createInvoiceRequest = await handleLightningInvoiceCreateRequest({
walletIndex: message.walletIndex!,
amount: message.amount!,
description: message.description,
});
reply({ invoicePaymentRequest: createInvoiceRequest });
} else if (message.message === 'sendApplicationContext') {
const walletsToProcess = await constructWalletsToSendToWatch();
if (walletsToProcess) updateApplicationContext(walletsToProcess);
} else if (message.message === 'fetchTransactions') {
await fetchWalletTransactions();
await saveToDisk();
reply({});
} else if (message.message === 'hideBalance') {
wallets[message.walletIndex!].hideBalance = message.hideBalance!;
await saveToDisk();
reply({});
}
} catch (error) {
console.debug('Error handling message:', error);
reply({});
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[fetchWalletTransactions, saveToDisk, wallets],
);
const handleLightningInvoiceCreateRequest = useCallback(
async ({ walletIndex, amount, description = loc.lnd.placeholder }: LightningInvoiceCreateRequest): Promise<string | undefined> => {
const wallet = wallets[walletIndex];
if (wallet.allowReceive() && amount > 0) {
try {
if ('addInvoice' in wallet) {
const invoiceRequest = await wallet.addInvoice(amount, description);
// @ts-ignore: Notifications type is not defined
if (await Notifications.isNotificationsEnabled()) {
const decoded = await wallet.decodeInvoice(invoiceRequest);
// @ts-ignore: Notifications type is not defined
Notifications.majorTomToGroundControl([], [decoded.payment_hash], []);
return invoiceRequest;
}
return invoiceRequest;
}
} catch (invoiceError) {
console.debug('Error creating invoice:', invoiceError);
}
}
},
[wallets],
);
// Construct wallet data to send to the watch, including transaction details
const constructWalletsToSendToWatch = useCallback(async () => {
if (!Array.isArray(wallets) || !walletsInitialized) return;
const walletsToProcess = await Promise.allSettled(
wallets.map(async wallet => {
try {
let receiveAddress;
try {
receiveAddress = wallet.chain === Chain.ONCHAIN ? await wallet.getAddressAsync() : wallet.getAddress();
} catch {
receiveAddress =
wallet.chain === Chain.ONCHAIN
? 'next_free_address_index' in wallet && '_getExternalAddressByIndex' in wallet
? wallet._getExternalAddressByIndex(wallet.next_free_address_index)
: wallet.getAddress()
: wallet.getAddress();
}
const transactions: Transaction[] = wallet
.getTransactions()
.slice(0, 10)
.map((transaction: any) => ({
type: transaction.confirmations ? 'pendingConfirmation' : 'received',
amount: formatBalance(transaction.value, wallet.getPreferredBalanceUnit(), true).toString(),
memo: txMetadata[transaction.hash]?.memo || transaction.memo || '',
time: transactionTimeToReadable(transaction.received),
}));
return {
label: wallet.getLabel(),
balance: formatBalance(Number(wallet.getBalance()), wallet.getPreferredBalanceUnit(), true),
type: wallet.type,
preferredBalanceUnit: wallet.getPreferredBalanceUnit(),
receiveAddress,
transactions,
hideBalance: wallet.hideBalance,
...(wallet.chain === Chain.ONCHAIN &&
wallet.type !== MultisigHDWallet.type && {
xpub: wallet.getXpub() || wallet.getSecret(),
}),
...(wallet.allowBIP47() &&
wallet.isBIP47Enabled() &&
'getBIP47PaymentCode' in wallet && { paymentCode: wallet.getBIP47PaymentCode() }),
};
} catch (error) {
console.error('Failed to construct wallet:', {
walletLabel: wallet.getLabel(),
walletType: wallet.type,
error,
});
return null; // Ensure failed wallet returns null so it's excluded from final results
}
}),
);
const processedWallets = walletsToProcess
.filter(result => result.status === 'fulfilled' && result.value !== null)
.map(result => (result as PromiseFulfilledResult<any>).value);
console.debug('Constructed wallets to process for Apple Watch');
return { wallets: processedWallets, randomID: Math.floor(Math.random() * 11) };
}, [wallets, walletsInitialized, txMetadata]);
return null;
}
export default useWatchConnectivity;

View File

@ -0,0 +1,2 @@
const useWatchConnectivity = () => {};
export default useWatchConnectivity;

View File

@ -0,0 +1,141 @@
import { useEffect, useRef } from 'react';
import DefaultPreference from 'react-native-default-preference';
import { Transaction, TWallet } from '../class/wallets/types';
import { useSettings } from '../hooks/context/useSettings';
import { useStorage } from '../hooks/context/useStorage';
import { GROUP_IO_BLUEWALLET } from '../blue_modules/currency';
import debounce from '../blue_modules/debounce';
enum WidgetCommunicationKeys {
AllWalletsSatoshiBalance = 'WidgetCommunicationAllWalletsSatoshiBalance',
AllWalletsLatestTransactionTime = 'WidgetCommunicationAllWalletsLatestTransactionTime',
DisplayBalanceAllowed = 'WidgetCommunicationDisplayBalanceAllowed',
LatestTransactionIsUnconfirmed = 'WidgetCommunicationLatestTransactionIsUnconfirmed',
}
DefaultPreference.setName(GROUP_IO_BLUEWALLET);
export const isBalanceDisplayAllowed = async (): Promise<boolean> => {
try {
const displayBalance = await DefaultPreference.get(WidgetCommunicationKeys.DisplayBalanceAllowed);
if (displayBalance === '1') {
return true;
} else if (displayBalance === '0') {
return false;
} else {
// Preference not set, initialize it to '1' (allowed) and return true
await DefaultPreference.set(WidgetCommunicationKeys.DisplayBalanceAllowed, '1');
return true;
}
} catch (error) {
console.error('Failed to get DisplayBalanceAllowed:', error);
return true;
}
};
export const setBalanceDisplayAllowed = async (allowed: boolean): Promise<void> => {
try {
if (allowed) {
await DefaultPreference.set(WidgetCommunicationKeys.DisplayBalanceAllowed, '1');
} else {
await DefaultPreference.set(WidgetCommunicationKeys.DisplayBalanceAllowed, '0');
}
} catch (error) {
console.error('Failed to set DisplayBalanceAllowed:', error);
}
};
export const calculateBalanceAndTransactionTime = async (
wallets: TWallet[],
walletsInitialized: boolean,
): Promise<{
allWalletsBalance: number;
latestTransactionTime: number | string;
}> => {
if (!walletsInitialized || !(await isBalanceDisplayAllowed())) {
return { allWalletsBalance: 0, latestTransactionTime: 0 };
}
const results = await Promise.allSettled(
wallets.map(async wallet => {
if (wallet.hideBalance) return { balance: 0, latestTransactionTime: 0 };
const balance = await wallet.getBalance();
const transactions: Transaction[] = await wallet.getTransactions();
const confirmedTransactions = transactions.filter(t => t.confirmations > 0);
const latestTransactionTime =
confirmedTransactions.length > 0
? Math.max(...confirmedTransactions.map(t => t.received || t.time || 0))
: WidgetCommunicationKeys.LatestTransactionIsUnconfirmed;
return { balance, latestTransactionTime };
}),
);
const allWalletsBalance = results.reduce((acc, result) => acc + (result.status === 'fulfilled' ? result.value.balance : 0), 0);
const latestTransactionTime = results.reduce(
(max, result) =>
result.status === 'fulfilled' && typeof result.value.latestTransactionTime === 'number' && result.value.latestTransactionTime > max
? result.value.latestTransactionTime
: max,
0,
);
return { allWalletsBalance, latestTransactionTime };
};
export const syncWidgetBalanceWithWallets = async (
wallets: TWallet[],
walletsInitialized: boolean,
cachedBalance: { current: number },
cachedLatestTransactionTime: { current: number | string },
): Promise<void> => {
try {
const { allWalletsBalance, latestTransactionTime } = await calculateBalanceAndTransactionTime(wallets, walletsInitialized);
if (cachedBalance.current !== allWalletsBalance || cachedLatestTransactionTime.current !== latestTransactionTime) {
await Promise.all([
DefaultPreference.set(WidgetCommunicationKeys.AllWalletsSatoshiBalance, String(allWalletsBalance)),
DefaultPreference.set(WidgetCommunicationKeys.AllWalletsLatestTransactionTime, String(latestTransactionTime)),
]);
cachedBalance.current = allWalletsBalance;
cachedLatestTransactionTime.current = latestTransactionTime;
}
} catch (error) {
console.error('Failed to sync widget balance with wallets:', error);
}
};
const debouncedSyncWidgetBalanceWithWallets = debounce(
async (
wallets: TWallet[],
walletsInitialized: boolean,
cachedBalance: { current: number },
cachedLatestTransactionTime: { current: number | string },
) => {
await syncWidgetBalanceWithWallets(wallets, walletsInitialized, cachedBalance, cachedLatestTransactionTime);
},
500,
);
const useWidgetCommunication = (): void => {
const { wallets, walletsInitialized } = useStorage();
const { isWidgetBalanceDisplayAllowed } = useSettings();
const cachedBalance = useRef<number>(0);
const cachedLatestTransactionTime = useRef<number | string>(0);
useEffect(() => {
if (walletsInitialized) {
debouncedSyncWidgetBalanceWithWallets(wallets, walletsInitialized, cachedBalance, cachedLatestTransactionTime);
}
}, [wallets, walletsInitialized, isWidgetBalanceDisplayAllowed]);
useEffect(() => {
return () => {
debouncedSyncWidgetBalanceWithWallets.cancel();
};
}, []);
};
export default useWidgetCommunication;

View File

@ -0,0 +1,9 @@
const useWidgetCommunication = (): void => {};
export const isBalanceDisplayAllowed = async (): Promise<boolean> => {
return true;
};
export const setBalanceDisplayAllowed = async (_allowed: boolean): Promise<void> => {};
export default useWidgetCommunication;

View File

@ -118,6 +118,7 @@
B450109F2C0FCDA500619044 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B450109B2C0FCD8A00619044 /* Utilities.swift */; };
B45010A62C1507DE00619044 /* CustomSegmentedControlManager.m in Sources */ = {isa = PBXBuildFile; fileRef = B45010A52C1507DE00619044 /* CustomSegmentedControlManager.m */; };
B4549F362B82B10D002E3153 /* ci_post_clone.sh in Resources */ = {isa = PBXBuildFile; fileRef = B4549F352B82B10D002E3153 /* ci_post_clone.sh */; };
B45942C42CDECF2400B3DC2E /* MenuElementsEmitter.m in Sources */ = {isa = PBXBuildFile; fileRef = B4C075292CDDB3C500322A84 /* MenuElementsEmitter.m */; };
B461B852299599F800E431AA /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = B461B851299599F800E431AA /* AppDelegate.mm */; };
B4742E972CCDBE8300380EEE /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B4742E962CCDBE8300380EEE /* Localizable.xcstrings */; };
B4742E982CCDBE8300380EEE /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B4742E962CCDBE8300380EEE /* Localizable.xcstrings */; };
@ -384,6 +385,8 @@
B4AB225C2B02AD12001F4328 /* XMLParserDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XMLParserDelegate.swift; sourceTree = "<group>"; };
B4B1A4612BFA73110072E3BB /* WidgetHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetHelper.swift; sourceTree = "<group>"; };
B4B31A352C77BBA000663334 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/Interface.strings; sourceTree = "<group>"; };
B4C075282CDDB3BE00322A84 /* MenuElementsEmitter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MenuElementsEmitter.h; sourceTree = "<group>"; };
B4C075292CDDB3C500322A84 /* MenuElementsEmitter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MenuElementsEmitter.m; sourceTree = "<group>"; };
B4D0B2612C1DEA11006B6B1B /* ReceivePageInterfaceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceivePageInterfaceController.swift; sourceTree = "<group>"; };
B4D0B2632C1DEA99006B6B1B /* ReceiveType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiveType.swift; sourceTree = "<group>"; };
B4D0B2652C1DEB7F006B6B1B /* ReceiveInterfaceMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiveInterfaceMode.swift; sourceTree = "<group>"; };
@ -476,6 +479,7 @@
13B07FAE1A68108700A75B9A /* BlueWallet */ = {
isa = PBXGroup;
children = (
B4C0752B2CDDB3CC00322A84 /* MenuElementsEmitter */,
B461B850299599F800E431AA /* AppDelegate.h */,
B461B851299599F800E431AA /* AppDelegate.mm */,
32C7944323B8879D00BE2AFA /* BlueWalletRelease.entitlements */,
@ -811,6 +815,15 @@
path = BlueWalletUITests;
sourceTree = "<group>";
};
B4C0752B2CDDB3CC00322A84 /* MenuElementsEmitter */ = {
isa = PBXGroup;
children = (
B4C075292CDDB3C500322A84 /* MenuElementsEmitter.m */,
B4C075282CDDB3BE00322A84 /* MenuElementsEmitter.h */,
);
path = MenuElementsEmitter;
sourceTree = "<group>";
};
FAA856B639C61E61D2CF90A8 /* Pods */ = {
isa = PBXGroup;
children = (
@ -1225,6 +1238,7 @@
B45010A62C1507DE00619044 /* CustomSegmentedControlManager.m in Sources */,
B44033CE2BCC352900162242 /* UserDefaultsGroup.swift in Sources */,
13B07FC11A68108700A75B9A /* main.m in Sources */,
B45942C42CDECF2400B3DC2E /* MenuElementsEmitter.m in Sources */,
B461B852299599F800E431AA /* AppDelegate.mm in Sources */,
B44033F42BCC377F00162242 /* WidgetData.swift in Sources */,
B49A28C52CD1A894006B08E4 /* MarketData.swift in Sources */,

View File

@ -7,6 +7,7 @@
#import <UserNotifications/UserNotifications.h>
#import <RNCPushNotificationIOS.h>
#import "EventEmitter.h"
#import "MenuElementsEmitter.h"
#import <React/RCTRootView.h>
#import <Bugsnag/Bugsnag.h>
#import "BlueWallet-Swift.h"
@ -152,18 +153,27 @@
- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity
restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler
{
[self.userDefaultsGroup setValue:@{@"activityType": userActivity.activityType, @"userInfo": userActivity.userInfo} forKey:@"onUserActivityOpen"];
NSDictionary *userActivityData = @{@"activityType": userActivity.activityType, @"userInfo": userActivity.userInfo};
[self.userDefaultsGroup setValue:userActivityData forKey:@"onUserActivityOpen"];
// Check if the activity type matches the allowed types
if ([userActivity.activityType isEqualToString:@"io.bluewallet.bluewallet.receiveonchain"] ||
[userActivity.activityType isEqualToString:@"io.bluewallet.bluewallet.xpub"] ||
[userActivity.activityType isEqualToString:@"io.bluewallet.bluewallet.blockexplorer"]) {
[EventEmitter.sharedInstance sendUserActivity:userActivityData];
return YES;
}
if (userActivity.activityType == NSUserActivityTypeBrowsingWeb) {
return [RCTLinkingManager application:application
continueUserActivity:userActivity
restorationHandler:restorationHandler];
}
else {
[EventEmitter.sharedInstance sendUserActivity:@{@"activityType": userActivity.activityType, @"userInfo": userActivity.userInfo}];
return YES;
}
// If activity type does not match any of the specified types, do nothing
return NO;
}
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
return [RCTLinkingManager application:app openURL:url options:options];
}
@ -184,7 +194,6 @@
-(void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler
{
NSDictionary *userInfo = notification.request.content.userInfo;
[EventEmitter.sharedInstance sendNotification:userInfo];
completionHandler(UNNotificationPresentationOptionSound | UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionBadge);
}
@ -234,24 +243,24 @@
}
- (void)openSettings:(UIKeyCommand *)keyCommand {
[EventEmitter.sharedInstance openSettings];
[MenuElementsEmitter.sharedInstance openSettings];
}
- (void)addWalletAction:(UIKeyCommand *)keyCommand {
// Implement the functionality for adding a wallet
[EventEmitter.sharedInstance addWalletMenuAction];
[MenuElementsEmitter.sharedInstance addWalletMenuAction];
NSLog(@"Add Wallet action performed");
}
- (void)importWalletAction:(UIKeyCommand *)keyCommand {
// Implement the functionality for adding a wallet
[EventEmitter.sharedInstance importWalletMenuAction];
[MenuElementsEmitter.sharedInstance importWalletMenuAction];
NSLog(@"Import Wallet action performed");
}
- (void)reloadTransactionsAction:(UIKeyCommand *)keyCommand {
// Implement the functionality for adding a wallet
[EventEmitter.sharedInstance reloadTransactionsMenuAction];
[MenuElementsEmitter.sharedInstance reloadTransactionsMenuAction];
NSLog(@"Reload Transactions action performed");
}

View File

@ -12,11 +12,6 @@
@interface EventEmitter : RCTEventEmitter <RCTBridgeModule>
+ (EventEmitter *)sharedInstance;
- (void)sendNotification:(NSDictionary *)userInfo;
- (void)openSettings;
- (void)addWalletMenuAction;
- (void)importWalletMenuAction;
- (void)reloadTransactionsMenuAction;
- (void)sendUserActivity:(NSDictionary *)userInfo;
@end

View File

@ -18,7 +18,11 @@ RCT_EXPORT_MODULE();
return YES;
}
+ (EventEmitter *)sharedInstance {
+ (instancetype)sharedInstance {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
@ -27,47 +31,24 @@ RCT_EXPORT_MODULE();
}
- (instancetype)init {
sharedInstance = [super init];
return sharedInstance;
self = [super init];
return self;
}
- (NSArray<NSString *> *)supportedEvents {
return @[@"onNotificationReceived",@"openSettings",@"onUserActivityOpen",@"addWalletMenuAction", @"importWalletMenuAction", @"reloadTransactionsMenuAction"];
}
- (void)sendNotification:(NSDictionary *)userInfo
{
[sharedInstance sendEventWithName:@"onNotificationReceived" body:userInfo];
return @[@"onUserActivityOpen"];
}
- (void)sendUserActivity:(NSDictionary *)userInfo
{
[sharedInstance sendEventWithName:@"onUserActivityOpen" body:userInfo];
[self sendEventWithName:@"onUserActivityOpen" body:userInfo];
}
RCT_REMAP_METHOD(getMostRecentUserActivity, resolve: (RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
RCT_EXPORT_METHOD(getMostRecentUserActivity:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.io.bluewallet.bluewallet"];
resolve([defaults valueForKey:@"onUserActivityOpen"]);
}
- (void)openSettings
{
[sharedInstance sendEventWithName:@"openSettings" body:nil];
}
- (void)addWalletMenuAction {
[sharedInstance sendEventWithName:@"addWalletMenuAction" body:nil];
}
- (void)importWalletMenuAction {
[sharedInstance sendEventWithName:@"importWalletMenuAction" body:nil];
}
- (void)reloadTransactionsMenuAction {
[sharedInstance sendEventWithName:@"reloadTransactionsMenuAction" body:nil];
NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.io.bluewallet.bluewallet"];
resolve([defaults valueForKey:@"onUserActivityOpen"]);
}
@end

View File

@ -48,6 +48,9 @@
},
"China (Chinese Yuan)" : {
},
"Choose your preferred currency." : {
},
"Choose your preferred fiat currency." : {
@ -262,6 +265,9 @@
},
"Venezuela (Venezuelan Bolívar Soberano)" : {
},
"View the current Bitcoin market rate in your preferred currency." : {
},
"View the current Bitcoin market rate in your preferred fiat currency." : {

17
ios/MenuElementsEmitter.h Normal file
View File

@ -0,0 +1,17 @@
//
// MenuElementsEmitter.h
// BlueWallet
//
#import <React/RCTEventEmitter.h>
@interface MenuElementsEmitter : RCTEventEmitter
+ (instancetype)sharedInstance;
- (void)openSettings;
- (void)addWalletMenuAction;
- (void)importWalletMenuAction;
- (void)reloadTransactionsMenuAction;
@end

58
ios/MenuElementsEmitter.m Normal file
View File

@ -0,0 +1,58 @@
//
// MenuElementsEmitter.m
// BlueWallet
//
#import "MenuElementsEmitter.h"
static MenuElementsEmitter *sharedInstance;
@implementation MenuElementsEmitter
RCT_EXPORT_MODULE();
+ (BOOL)requiresMainQueueSetup {
return YES;
}
+ (instancetype)sharedInstance {
if (!sharedInstance) {
sharedInstance = [[self alloc] init];
}
return sharedInstance;
}
- (instancetype)init {
self = [super init];
if (self) {
sharedInstance = self;
}
return self;
}
- (NSArray<NSString *> *)supportedEvents {
return @[
@"openSettings",
@"addWalletMenuAction",
@"importWalletMenuAction",
@"reloadTransactionsMenuAction"
];
}
- (void)openSettings {
[self sendEventWithName:@"openSettings" body:nil];
}
- (void)addWalletMenuAction {
[self sendEventWithName:@"addWalletMenuAction" body:nil];
}
- (void)importWalletMenuAction {
[self sendEventWithName:@"importWalletMenuAction" body:nil];
}
- (void)reloadTransactionsMenuAction {
[self sendEventWithName:@"reloadTransactionsMenuAction" body:nil];
}
@end

View File

@ -0,0 +1,27 @@
//
// MenuElementsEmitter.h
// BlueWallet
//
// Created by Marcos Rodriguez on 11/7/24.
// Copyright © 2024 BlueWallet. All rights reserved.
//
//
// MenuElementsEmitter.h
// BlueWallet
//
#import <React/RCTEventEmitter.h>
@interface MenuElementsEmitter : RCTEventEmitter
+ (instancetype)sharedInstance;
- (void)openSettings;
- (void)addWalletMenuAction;
- (void)importWalletMenuAction;
- (void)reloadTransactionsMenuAction;
- (NSArray<NSString *> *)supportedEvents;
@end

View File

@ -0,0 +1,62 @@
//
// MenuElementsEmitter.m
// BlueWallet
//
// Created by Marcos Rodriguez on 11/7/24.
// Copyright © 2024 BlueWallet. All rights reserved.
//
#import "MenuElementsEmitter.h"
static MenuElementsEmitter *sharedInstance;
@implementation MenuElementsEmitter
RCT_EXPORT_MODULE();
+ (BOOL)requiresMainQueueSetup {
return YES;
}
+ (instancetype)sharedInstance {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
- (instancetype)init {
self = [super init];
if (self) {
sharedInstance = self;
}
return self;
}
- (NSArray<NSString *> *)supportedEvents {
return @[
@"openSettings",
@"addWalletMenuAction",
@"importWalletMenuAction",
@"reloadTransactionsMenuAction"
];
}
- (void)openSettings {
[self sendEventWithName:@"openSettings" body:nil];
}
- (void)addWalletMenuAction {
[self sendEventWithName:@"addWalletMenuAction" body:nil];
}
- (void)importWalletMenuAction {
[self sendEventWithName:@"importWalletMenuAction" body:nil];
}
- (void)reloadTransactionsMenuAction {
[self sendEventWithName:@"reloadTransactionsMenuAction" body:nil];
}
@end

View File

@ -19,11 +19,13 @@ import { useTheme } from '../../components/themes';
import { disallowScreenshot } from 'react-native-screen-capture';
import loc from '../../loc';
import { BitcoinUnit } from '../../models/bitcoinUnits';
import { useSettings } from '../../hooks/context/useSettings';
const SendCreate = () => {
const { fee, recipients, memo = '', satoshiPerByte, psbt, showAnimatedQr, tx } = useRoute().params;
const transaction = bitcoin.Transaction.fromHex(tx);
const size = transaction.virtualSize();
const { isPrivacyBlurEnabled } = useSettings();
const { colors } = useTheme();
const { setOptions } = useNavigation();
@ -47,11 +49,11 @@ const SendCreate = () => {
useEffect(() => {
console.log('send/create - useEffect');
disallowScreenshot(true);
disallowScreenshot(isPrivacyBlurEnabled);
return () => {
disallowScreenshot(false);
};
}, []);
}, [isPrivacyBlurEnabled]);
const exportTXN = useCallback(async () => {
const fileName = `${Date.now()}.txn`;

View File

@ -5,7 +5,6 @@ import A from '../../blue_modules/analytics';
import { Header } from '../../components/Header';
import ListItem, { PressableWrapper } from '../../components/ListItem';
import { useTheme } from '../../components/themes';
import { setBalanceDisplayAllowed } from '../../components/WidgetCommunication';
import loc from '../../loc';
import { useStorage } from '../../hooks/context/useStorage';
import { useSettings } from '../../hooks/context/useSettings';
@ -28,7 +27,7 @@ const SettingsPrivacy: React.FC = () => {
isDoNotTrackEnabled,
setDoNotTrackStorage,
isPrivacyBlurEnabled,
setIsPrivacyBlurEnabledState,
setIsPrivacyBlurEnabled,
isWidgetBalanceDisplayAllowed,
setIsWidgetBalanceDisplayAllowedStorage,
isClipboardGetContentEnabled,
@ -82,7 +81,6 @@ const SettingsPrivacy: React.FC = () => {
const onWidgetsTotalBalanceValueChange = async (value: boolean) => {
setIsLoading(SettingsPrivacySection.Widget);
try {
await setBalanceDisplayAllowed(value);
setIsWidgetBalanceDisplayAllowedStorage(value);
} catch (e) {
console.debug('onWidgetsTotalBalanceValueChange catch', e);
@ -102,7 +100,7 @@ const SettingsPrivacy: React.FC = () => {
const onTemporaryScreenshotsValueChange = (value: boolean) => {
setIsLoading(SettingsPrivacySection.TemporaryScreenshots);
setIsPrivacyBlurEnabledState(!value);
setIsPrivacyBlurEnabled(!value);
setIsLoading(SettingsPrivacySection.None);
};

View File

@ -11,6 +11,7 @@ import { disallowScreenshot } from 'react-native-screen-capture';
import loc from '../../loc';
import { useStorage } from '../../hooks/context/useStorage';
import { ExportMultisigCoordinationSetupStackRootParamList } from '../../navigation/ExportMultisigCoordinationSetupStack';
import { useSettings } from '../../hooks/context/useSettings';
const enum ActionType {
SET_LOADING = 'SET_LOADING',
@ -72,6 +73,7 @@ const ExportMultisigCoordinationSetup: React.FC = () => {
const { params } = useRoute<RouteProp<ExportMultisigCoordinationSetupStackRootParamList, 'ExportMultisigCoordinationSetup'>>();
const walletID = params.walletID;
const { wallets } = useStorage();
const { isPrivacyBlurEnabled } = useSettings();
const wallet: TWallet | undefined = wallets.find(w => w.getID() === walletID);
const dynamicQRCode = useRef<any>();
const { colors } = useTheme();
@ -99,7 +101,7 @@ const ExportMultisigCoordinationSetup: React.FC = () => {
dispatch({ type: ActionType.SET_LOADING, isLoading: true });
const task = InteractionManager.runAfterInteractions(() => {
disallowScreenshot(true);
disallowScreenshot(isPrivacyBlurEnabled);
if (wallet) {
setTimeout(async () => {
try {

View File

@ -137,6 +137,7 @@ const ImportWalletDiscovery: React.FC = () => {
});
return () => {
keepAwake(false);
task.current?.stop();
};
// eslint-disable-next-line react-hooks/exhaustive-deps

View File

@ -6,12 +6,14 @@ import { useTheme } from '../../components/themes';
import { disallowScreenshot } from 'react-native-screen-capture';
import loc from '../../loc';
import { useStorage } from '../../hooks/context/useStorage';
import { useSettings } from '../../hooks/context/useSettings';
const PleaseBackup: React.FC = () => {
const { wallets } = useStorage();
const { walletID } = useRoute().params as { walletID: string };
const wallet = wallets.find(w => w.getID() === walletID);
const navigation = useNavigation();
const { isPrivacyBlurEnabled } = useSettings();
const { colors } = useTheme();
const stylesHook = StyleSheet.create({
@ -37,7 +39,7 @@ const PleaseBackup: React.FC = () => {
useEffect(() => {
BackHandler.addEventListener('hardwareBackPress', handleBackButton);
disallowScreenshot(true);
disallowScreenshot(isPrivacyBlurEnabled);
return () => {
BackHandler.removeEventListener('hardwareBackPress', handleBackButton);
disallowScreenshot(false);

View File

@ -54,7 +54,7 @@ const ViewEditMultisigCosigners: React.FC = () => {
const { colors } = useTheme();
const { wallets, setWalletsWithNewOrder } = useStorage();
const { isBiometricUseCapableAndEnabled } = useBiometrics();
const { isElectrumDisabled } = useSettings();
const { isElectrumDisabled, isPrivacyBlurEnabled } = useSettings();
const { navigate, dispatch, addListener } = useExtendedNavigation();
const openScannerButtonRef = useRef();
const route = useRoute();
@ -191,7 +191,7 @@ const ViewEditMultisigCosigners: React.FC = () => {
if (hasLoaded.current) return;
setIsLoading(true);
disallowScreenshot(true);
disallowScreenshot(isPrivacyBlurEnabled);
const task = InteractionManager.runAfterInteractions(async () => {
if (!w.current) {

View File

@ -13,6 +13,7 @@ import { useStorage } from '../../hooks/context/useStorage';
import { HandOffActivityType } from '../../components/types';
import { WalletExportStackParamList } from '../../navigation/WalletExportStack';
import useAppState from '../../hooks/useAppState';
import { useSettings } from '../../hooks/context/useSettings';
type RouteProps = RouteProp<WalletExportStackParamList, 'WalletExport'>;
@ -21,6 +22,7 @@ const WalletExport: React.FC = () => {
const { walletID } = useRoute<RouteProps>().params;
const [isLoading, setIsLoading] = useState(true);
const { goBack } = useNavigation();
const { isPrivacyBlurEnabled } = useSettings();
const { colors } = useTheme();
const wallet = wallets.find(w => w.getID() === walletID);
const [qrCodeSize, setQRCodeSize] = useState(90);
@ -43,7 +45,7 @@ const WalletExport: React.FC = () => {
useFocusEffect(
useCallback(() => {
disallowScreenshot(true);
disallowScreenshot(isPrivacyBlurEnabled);
const task = InteractionManager.runAfterInteractions(async () => {
if (wallet) {
if (!wallet.getUserHasSavedExport()) {
@ -54,10 +56,10 @@ const WalletExport: React.FC = () => {
}
});
return () => {
task.cancel();
disallowScreenshot(false);
task.cancel();
};
}, [wallet, saveToDisk]),
}, [isPrivacyBlurEnabled, wallet, saveToDisk]),
);
const secrets: string[] = (() => {

View File

@ -42,6 +42,7 @@ import getWalletTransactionsOptions from '../../navigation/helpers/getWalletTran
import { presentWalletExportReminder } from '../../helpers/presentWalletExportReminder';
import selectWallet from '../../helpers/select-wallet';
import assert from 'assert';
import useMenuElements from '../../hooks/useMenuElements';
import { useSettings } from '../../hooks/context/useSettings';
const buttonFontSize =
@ -52,8 +53,8 @@ const buttonFontSize =
type WalletTransactionsProps = NativeStackScreenProps<DetailViewStackParamList, 'WalletTransactions'>;
const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
const { wallets, saveToDisk, setSelectedWalletID, setReloadTransactionsMenuActionFunction } = useStorage();
const { isElectrumDisabled } = useSettings();
const { wallets, saveToDisk, setSelectedWalletID } = useStorage();
const { setReloadTransactionsMenuActionFunction } = useMenuElements();
const { isBiometricUseCapableAndEnabled } = useBiometrics();
const [isLoading, setIsLoading] = useState(false);
const { walletID } = route.params;
@ -64,6 +65,7 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
const navigation = useExtendedNavigation();
const { setOptions, navigate } = navigation;
const { colors } = useTheme();
const { isElectrumDisabled } = useSettings();
const walletActionButtonsRef = useRef<View>(null);
const stylesHook = StyleSheet.create({
@ -372,7 +374,7 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
task.cancel();
setReloadTransactionsMenuActionFunction(() => {});
};
}, [setReloadTransactionsMenuActionFunction, refreshTransactions]),
}, [refreshTransactions, setReloadTransactionsMenuActionFunction]),
);
const refreshProps = isDesktop || isElectrumDisabled ? {} : { refreshing: isLoading, onRefresh: refreshTransactions };

View File

@ -23,6 +23,7 @@ import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import { useStorage } from '../../hooks/context/useStorage';
import TotalWalletsBalance from '../../components/TotalWalletsBalance';
import { useSettings } from '../../hooks/context/useSettings';
import useMenuElements from '../../hooks/useMenuElements';
const WalletsListSections = { CAROUSEL: 'CAROUSEL', TRANSACTIONS: 'TRANSACTIONS' };
@ -98,14 +99,8 @@ const WalletsList: React.FC = () => {
const { isLargeScreen } = useIsLargeScreen();
const walletsCarousel = useRef<any>();
const currentWalletIndex = useRef<number>(0);
const {
wallets,
getTransactions,
getBalance,
refreshAllWalletTransactions,
setSelectedWalletID,
setReloadTransactionsMenuActionFunction,
} = useStorage();
const { setReloadTransactionsMenuActionFunction } = useMenuElements();
const { wallets, getTransactions, getBalance, refreshAllWalletTransactions, setSelectedWalletID } = useStorage();
const { isTotalBalanceEnabled, isElectrumDisabled } = useSettings();
const { width } = useWindowDimensions();
const { colors, scanImage } = useTheme();
@ -129,6 +124,38 @@ const WalletsList: React.FC = () => {
},
});
/**
* Forcefully fetches TXs and balance for ALL wallets.
* Triggered manually by user on pull-to-refresh.
*/
const refreshTransactions = useCallback(
async (showLoadingIndicator = true, showUpdateStatusIndicator = false) => {
if (isElectrumDisabled) {
dispatch({ type: ActionTypes.SET_LOADING, payload: false });
return;
}
dispatch({ type: ActionTypes.SET_LOADING, payload: showLoadingIndicator });
refreshAllWalletTransactions(undefined, showUpdateStatusIndicator).finally(() => {
dispatch({ type: ActionTypes.SET_LOADING, payload: false });
});
},
[isElectrumDisabled, refreshAllWalletTransactions],
);
const onRefresh = useCallback(() => {
console.debug('WalletsList onRefresh');
refreshTransactions(true, false);
// Optimized for Mac option doesn't like RN Refresh component. Menu Elements now handles it for macOS
}, [refreshTransactions]);
const verifyBalance = useCallback(() => {
if (getBalance() !== 0) {
A(A.ENUM.GOT_NONZERO_BALANCE);
} else {
A(A.ENUM.GOT_ZERO_BALANCE);
}
}, [getBalance]);
useFocusEffect(
useCallback(() => {
const task = InteractionManager.runAfterInteractions(() => {
@ -140,8 +167,7 @@ const WalletsList: React.FC = () => {
task.cancel();
setReloadTransactionsMenuActionFunction(() => {});
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []),
}, [onRefresh, setReloadTransactionsMenuActionFunction, verifyBalance, setSelectedWalletID]),
);
useEffect(() => {
@ -161,32 +187,6 @@ const WalletsList: React.FC = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [route.params?.scannedData]);
const verifyBalance = useCallback(() => {
if (getBalance() !== 0) {
A(A.ENUM.GOT_NONZERO_BALANCE);
} else {
A(A.ENUM.GOT_ZERO_BALANCE);
}
}, [getBalance]);
/**
* Forcefully fetches TXs and balance for ALL wallets.
* Triggered manually by user on pull-to-refresh.
*/
const refreshTransactions = useCallback(
async (showLoadingIndicator = true, showUpdateStatusIndicator = false) => {
if (isElectrumDisabled) {
dispatch({ type: ActionTypes.SET_LOADING, payload: false });
return;
}
dispatch({ type: ActionTypes.SET_LOADING, payload: showLoadingIndicator });
refreshAllWalletTransactions(undefined, showUpdateStatusIndicator).finally(() => {
dispatch({ type: ActionTypes.SET_LOADING, payload: false });
});
},
[isElectrumDisabled, refreshAllWalletTransactions],
);
useEffect(() => {
refreshTransactions(false, true);
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -408,12 +408,6 @@ const WalletsList: React.FC = () => {
});
}, [pasteFromClipboard, onBarScanned, routeName]);
const onRefresh = useCallback(() => {
console.debug('WalletsList onRefresh');
refreshTransactions(true, false);
// Optimized for Mac option doesn't like RN Refresh component. Menu Elements now handles it for macOS
}, [refreshTransactions]);
const refreshProps = isDesktop || isElectrumDisabled ? {} : { refreshing: isLoading, onRefresh };
const sections: SectionData[] = [

View File

@ -36,6 +36,7 @@ import { scanQrHelper } from '../../helpers/scan-qr';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import ToolTipMenu from '../../components/TooltipMenu';
import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
import { useSettings } from '../../hooks/context/useSettings';
const staticCache = {};
@ -59,15 +60,16 @@ const WalletsAddMultisigStep2 = () => {
const [importText, setImportText] = useState('');
const [askPassphrase, setAskPassphrase] = useState(false);
const openScannerButton = useRef();
const { isPrivacyBlurEnabled } = useSettings();
const data = useRef(new Array(n));
useFocusEffect(
useCallback(() => {
disallowScreenshot(true);
disallowScreenshot(isPrivacyBlurEnabled);
return () => {
disallowScreenshot(false);
};
}, []),
}, [isPrivacyBlurEnabled]),
);
useEffect(() => {

View File

@ -16,6 +16,7 @@ import { useKeyboard } from '../../hooks/useKeyboard';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import Clipboard from '@react-native-clipboard/clipboard';
import HeaderMenuButton from '../../components/HeaderMenuButton';
import { useSettings } from '../../hooks/context/useSettings';
const WalletsImport = () => {
const navigation = useExtendedNavigation();
@ -30,7 +31,7 @@ const WalletsImport = () => {
const [searchAccountsMenuState, setSearchAccountsMenuState] = useState(false);
const [askPassphraseMenuState, setAskPassphraseMenuState] = useState(false);
const [clearClipboardMenuState, setClearClipboardMenuState] = useState(true);
const { isPrivacyBlurEnabled } = useSettings();
// Styles
const styles = StyleSheet.create({
root: {
@ -61,11 +62,11 @@ const WalletsImport = () => {
});
useEffect(() => {
disallowScreenshot(true);
disallowScreenshot(isPrivacyBlurEnabled);
return () => {
disallowScreenshot(false);
};
}, []);
}, [isPrivacyBlurEnabled]);
useEffect(() => {
if (triggerImport) importButtonPressed();

View File

@ -10,6 +10,7 @@ import { useTheme } from '../../components/themes';
import { disallowScreenshot } from 'react-native-screen-capture';
import loc from '../../loc';
import { useStorage } from '../../hooks/context/useStorage';
import { useSettings } from '../../hooks/context/useSettings';
const PleaseBackupLNDHub = () => {
const { wallets } = useStorage();
@ -18,6 +19,7 @@ const PleaseBackupLNDHub = () => {
const navigation = useNavigation();
const { colors } = useTheme();
const [qrCodeSize, setQRCodeSize] = useState(90);
const { isPrivacyBlurEnabled } = useSettings();
const handleBackButton = useCallback(() => {
navigation.getParent().pop();
@ -38,13 +40,13 @@ const PleaseBackupLNDHub = () => {
});
useEffect(() => {
disallowScreenshot(true);
disallowScreenshot(isPrivacyBlurEnabled);
BackHandler.addEventListener('hardwareBackPress', handleBackButton);
return () => {
disallowScreenshot(false);
BackHandler.removeEventListener('hardwareBackPress', handleBackButton);
};
}, [handleBackButton]);
}, [handleBackButton, isPrivacyBlurEnabled]);
const pop = () => navigation.getParent().pop();

View File

@ -13,6 +13,7 @@ import loc from '../../loc';
import { styles, useDynamicStyles } from './xpub.styles';
import { useStorage } from '../../hooks/context/useStorage';
import { HandOffActivityType } from '../../components/types';
import { useSettings } from '../../hooks/context/useSettings';
type WalletXpubRouteProp = RouteProp<{ params: { walletID: string; xpub: string } }, 'params'>;
export type RootStackParamList = {
@ -27,6 +28,7 @@ const WalletXpub: React.FC = () => {
const route = useRoute<WalletXpubRouteProp>();
const { walletID, xpub } = route.params;
const wallet = wallets.find(w => w.getID() === walletID);
const { isPrivacyBlurEnabled } = useSettings();
const [isLoading, setIsLoading] = useState<boolean>(true);
const [xPubText, setXPubText] = useState<string | undefined>(undefined);
const navigation = useNavigation<NavigationProp<RootStackParamList, 'WalletXpub'>>();
@ -36,7 +38,7 @@ const WalletXpub: React.FC = () => {
useFocusEffect(
useCallback(() => {
disallowScreenshot(true);
disallowScreenshot(isPrivacyBlurEnabled);
// Skip execution if walletID hasn't changed
if (lastWalletIdRef.current === walletID) {
return;
@ -55,10 +57,10 @@ const WalletXpub: React.FC = () => {
});
lastWalletIdRef.current = walletID;
return () => {
task.cancel();
disallowScreenshot(false);
task.cancel();
};
}, [walletID, wallet, xpub, navigation]),
}, [isPrivacyBlurEnabled, walletID, wallet, xpub, navigation]),
);
useEffect(() => {