BlueWallet/hooks/useWatchConnectivity.ios.ts

239 lines
9 KiB
TypeScript
Raw Normal View History

2024-11-08 21:33:35 -04:00
import { useCallback, useEffect, useRef } from 'react';
import {
transferCurrentComplicationUserInfo,
2023-10-30 12:38:12 -04:00
transferUserInfo,
2024-05-20 10:54:13 +01:00
updateApplicationContext,
useInstalled,
2024-11-08 21:33:35 -04:00
usePaired,
2024-05-20 10:54:13 +01:00
useReachability,
watchEvents,
} from 'react-native-watch-connectivity';
2024-05-18 11:36:16 -04:00
import { MultisigHDWallet } from '../class';
2024-05-20 10:54:13 +01:00
import loc, { formatBalance, transactionTimeToReadable } from '../loc';
import { Chain } from '../models/bitcoinUnits';
import { FiatUnit } from '../models/fiatUnit';
2024-05-31 13:18:01 -04:00
import { useSettings } from '../hooks/context/useSettings';
import { useStorage } from '../hooks/context/useStorage';
2024-11-11 18:29:45 -04:00
import { isNotificationsEnabled, majorTomToGroundControl } from '../blue_modules/notifications';
2020-07-18 20:33:43 +01:00
2024-11-08 23:00:38 -04:00
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();
2024-04-17 21:05:48 -04:00
const { preferredFiatCurrency } = useSettings();
const isReachable = useReachability();
2024-11-08 21:33:35 -04:00
const isInstalled = useInstalled();
const isPaired = usePaired();
const messagesListenerActive = useRef(false);
const lastPreferredCurrency = useRef(FiatUnit.USD.endPointKey);
2024-11-08 22:48:13 -04:00
// Set up message listener only when conditions are met
useEffect(() => {
2024-11-08 21:33:35 -04:00
if (!isInstalled || !isPaired || !walletsInitialized || !isReachable) {
console.debug('Apple Watch not installed, not paired, or other conditions not met. Exiting message listener setup.');
return;
}
2024-11-08 21:33:35 -04:00
2024-11-08 23:00:38 -04:00
const messagesListener = watchEvents.addListener('message', (message: any) => handleMessages(message, () => {}));
2024-11-08 21:33:35 -04:00
messagesListenerActive.current = true;
return () => {
messagesListener();
messagesListenerActive.current = false;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
2024-11-08 21:33:35 -04:00
}, [walletsInitialized, isReachable, isInstalled, isPaired]);
2021-02-24 19:52:55 -05:00
2024-11-08 23:00:38 -04:00
// Send wallet data to Apple Watch
2021-02-24 19:52:55 -05:00
useEffect(() => {
2024-11-08 22:48:13 -04:00
if (!isInstalled || !isPaired || !walletsInitialized) return;
2024-11-08 21:33:35 -04:00
2024-11-08 22:48:13 -04:00
const sendWalletData = async () => {
2024-11-08 21:33:35 -04:00
try {
const walletsToProcess = await constructWalletsToSendToWatch();
2023-10-30 12:38:12 -04:00
if (walletsToProcess) {
if (isReachable) {
transferUserInfo(walletsToProcess);
2024-11-08 21:33:35 -04:00
console.debug('Apple Watch: sent info to watch transferUserInfo');
2023-10-30 12:38:12 -04:00
} else {
updateApplicationContext(walletsToProcess);
2024-11-08 21:33:35 -04:00
console.debug('Apple Watch: sent info to watch context');
2023-10-30 12:38:12 -04:00
}
}
2024-11-08 21:33:35 -04:00
} catch (error) {
console.debug('Failed to send wallets to watch:', error);
}
2024-11-08 22:48:13 -04:00
};
sendWalletData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [walletsInitialized, isReachable, isInstalled, isPaired]);
2024-11-08 23:00:38 -04:00
// Update application context with wallet status
useEffect(() => {
2024-11-08 22:48:13 -04:00
if (!isInstalled || !isPaired || !walletsInitialized || !isReachable) return;
2024-11-08 21:33:35 -04:00
updateApplicationContext({ isWalletsInitialized: walletsInitialized, randomID: Math.floor(Math.random() * 11) });
}, [isReachable, walletsInitialized, isInstalled, isPaired]);
2024-11-08 22:48:13 -04:00
// Update preferred fiat currency to Apple Watch if it changes
useEffect(() => {
2024-11-08 22:48:13 -04:00
if (!isInstalled || !isPaired || !walletsInitialized || !isReachable || !preferredFiatCurrency) return;
2024-11-08 21:33:35 -04:00
2024-11-08 22:38:58 -04:00
if (lastPreferredCurrency.current !== preferredFiatCurrency.endPointKey) {
try {
2024-11-08 22:48:13 -04:00
transferCurrentComplicationUserInfo({ preferredFiatCurrency: preferredFiatCurrency.endPointKey });
2024-11-08 22:38:58 -04:00
lastPreferredCurrency.current = preferredFiatCurrency.endPointKey;
2024-11-08 21:33:35 -04:00
console.debug('Apple Watch: updated preferred fiat currency');
} catch (error) {
console.debug('Error updating preferredFiatCurrency on watch:', error);
}
2024-11-08 21:33:35 -04:00
} else {
console.debug('WatchConnectivity lastPreferredCurrency has not changed');
}
2024-11-08 22:48:13 -04:00
}, [preferredFiatCurrency, walletsInitialized, isReachable, isInstalled, isPaired]);
2019-05-02 16:33:03 -04:00
2024-11-08 22:48:13 -04:00
const handleMessages = useCallback(
2024-11-08 23:00:38 -04:00
async (message: Message, reply: Reply) => {
2019-05-02 16:33:03 -04:00
try {
2024-11-08 22:48:13 -04:00
if (message.request === 'createInvoice') {
2024-11-08 23:00:38 -04:00
const createInvoiceRequest = await handleLightningInvoiceCreateRequest({
walletIndex: message.walletIndex!,
amount: message.amount!,
description: message.description,
});
2024-11-08 22:48:13 -04:00
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') {
2024-11-08 23:00:38 -04:00
wallets[message.walletIndex!].hideBalance = message.hideBalance!;
2024-11-08 22:48:13 -04:00
await saveToDisk();
reply({});
}
} catch (error) {
console.debug('Error handling message:', error);
reply({});
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[fetchWalletTransactions, saveToDisk, wallets],
);
2020-07-18 20:33:43 +01:00
2024-11-08 22:48:13 -04:00
const handleLightningInvoiceCreateRequest = useCallback(
2024-11-08 23:00:38 -04:00
async ({ walletIndex, amount, description = loc.lnd.placeholder }: LightningInvoiceCreateRequest): Promise<string | undefined> => {
2024-11-08 22:48:13 -04:00
const wallet = wallets[walletIndex];
if (wallet.allowReceive() && amount > 0) {
try {
2024-11-08 23:00:38 -04:00
if ('addInvoice' in wallet) {
const invoiceRequest = await wallet.addInvoice(amount, description);
2024-11-11 18:29:45 -04:00
if (await isNotificationsEnabled()) {
2024-11-08 23:00:38 -04:00
const decoded = await wallet.decodeInvoice(invoiceRequest);
2024-11-11 18:29:45 -04:00
majorTomToGroundControl([], [decoded.payment_hash], []);
2024-11-08 23:00:38 -04:00
return invoiceRequest;
}
return invoiceRequest;
}
2024-11-08 22:48:13 -04:00
} catch (invoiceError) {
console.debug('Error creating invoice:', invoiceError);
}
2019-05-02 16:33:03 -04:00
}
2024-11-08 22:48:13 -04:00
},
[wallets],
);
2019-05-02 16:33:03 -04:00
2024-11-08 22:48:13 -04:00
// Construct wallet data to send to the watch, including transaction details
2024-11-08 21:33:35 -04:00
const constructWalletsToSendToWatch = useCallback(async () => {
if (!Array.isArray(wallets) || !walletsInitialized) return;
2024-11-08 22:48:13 -04:00
const walletsToProcess = await Promise.allSettled(
2024-11-08 21:33:35 -04:00
wallets.map(async wallet => {
try {
2024-11-08 22:48:13 -04:00
let receiveAddress;
try {
receiveAddress = wallet.chain === Chain.ONCHAIN ? await wallet.getAddressAsync() : wallet.getAddress();
} catch {
receiveAddress =
2024-11-08 23:00:38 -04:00
wallet.chain === Chain.ONCHAIN
? 'next_free_address_index' in wallet && '_getExternalAddressByIndex' in wallet
? wallet._getExternalAddressByIndex(wallet.next_free_address_index)
: wallet.getAddress()
: wallet.getAddress();
2024-11-08 22:48:13 -04:00
}
2024-11-08 23:00:38 -04:00
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),
}));
2024-11-08 22:48:13 -04:00
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(),
}),
2024-11-08 23:00:38 -04:00
...(wallet.allowBIP47() &&
wallet.isBIP47Enabled() &&
'getBIP47PaymentCode' in wallet && { paymentCode: wallet.getBIP47PaymentCode() }),
2024-11-08 22:48:13 -04:00
};
} 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
}
2024-11-08 21:33:35 -04:00
}),
);
2024-11-08 22:48:13 -04:00
const processedWallets = walletsToProcess
.filter(result => result.status === 'fulfilled' && result.value !== null)
2024-11-08 23:00:38 -04:00
.map(result => (result as PromiseFulfilledResult<any>).value);
2024-11-08 22:48:13 -04:00
2024-11-08 21:33:35 -04:00
console.debug('Constructed wallets to process for Apple Watch');
2024-11-08 22:48:13 -04:00
return { wallets: processedWallets, randomID: Math.floor(Math.random() * 11) };
2024-11-08 21:33:35 -04:00
}, [wallets, walletsInitialized, txMetadata]);
}
2020-10-12 12:36:35 -04:00
2024-11-08 23:00:38 -04:00
export default useWatchConnectivity;