2020-05-27 13:12:17 +02:00
|
|
|
import 'react-native-gesture-handler'; // should be on top
|
2024-05-20 11:54:13 +02:00
|
|
|
|
2024-05-13 18:43:57 +02:00
|
|
|
import { CommonActions } from '@react-navigation/native';
|
2024-11-15 01:42:56 +01:00
|
|
|
import { useCallback, useEffect, useRef } from 'react';
|
2024-11-09 00:12:14 +01:00
|
|
|
import { AppState, AppStateStatus, Linking } from 'react-native';
|
2024-05-20 11:54:13 +02:00
|
|
|
import A from '../blue_modules/analytics';
|
2024-11-10 15:25:39 +01:00
|
|
|
import { getClipboardContent } from '../blue_modules/clipboard';
|
2024-05-13 18:43:57 +02:00
|
|
|
import { updateExchangeRate } from '../blue_modules/currency';
|
2024-05-20 11:54:13 +02:00
|
|
|
import triggerHapticFeedback, { HapticFeedbackTypes } from '../blue_modules/hapticFeedback';
|
2024-11-11 23:29:45 +01:00
|
|
|
import {
|
2024-11-11 22:45:21 +01:00
|
|
|
clearStoredNotifications,
|
|
|
|
getDeliveredNotifications,
|
|
|
|
getStoredNotifications,
|
2024-11-11 23:29:45 +01:00
|
|
|
initializeNotifications,
|
2024-11-11 22:45:21 +01:00
|
|
|
removeAllDeliveredNotifications,
|
|
|
|
setApplicationIconBadgeNumber,
|
|
|
|
} from '../blue_modules/notifications';
|
2024-05-14 19:17:03 +02:00
|
|
|
import { LightningCustodianWallet } from '../class';
|
2024-05-20 11:54:13 +02:00
|
|
|
import DeeplinkSchemaMatch from '../class/deeplink-schema-match';
|
|
|
|
import loc from '../loc';
|
|
|
|
import { Chain } from '../models/bitcoinUnits';
|
|
|
|
import { navigationRef } from '../NavigationService';
|
2024-05-14 19:36:31 +02:00
|
|
|
import ActionSheet from '../screen/ActionSheet';
|
2024-05-31 17:52:29 +02:00
|
|
|
import { useStorage } from '../hooks/context/useStorage';
|
2024-10-23 06:27:34 +02:00
|
|
|
import RNQRGenerator from 'rn-qr-generator';
|
2024-10-23 06:54:25 +02:00
|
|
|
import presentAlert from './Alert';
|
2024-11-08 04:19:46 +01:00
|
|
|
import useMenuElements from '../hooks/useMenuElements';
|
2024-11-09 02:07:35 +01:00
|
|
|
import useWidgetCommunication from '../hooks/useWidgetCommunication';
|
2024-11-09 04:00:38 +01:00
|
|
|
import useWatchConnectivity from '../hooks/useWatchConnectivity';
|
2024-11-14 16:30:19 +01:00
|
|
|
import useDeviceQuickActions from '../hooks/useDeviceQuickActions';
|
2024-11-15 01:42:56 +01:00
|
|
|
import useHandoffListener from '../hooks/useHandoffListener';
|
2024-05-14 19:36:31 +02:00
|
|
|
|
2020-11-20 19:28:34 +01:00
|
|
|
const ClipboardContentType = Object.freeze({
|
|
|
|
BITCOIN: 'BITCOIN',
|
|
|
|
LIGHTNING: 'LIGHTNING',
|
|
|
|
});
|
2018-12-12 14:29:10 +01:00
|
|
|
|
2024-05-13 18:43:57 +02:00
|
|
|
const CompanionDelegates = () => {
|
|
|
|
const { wallets, addWallet, saveToDisk, fetchAndSaveWalletTransactions, refreshAllWalletTransactions, setSharedCosigner } = useStorage();
|
2024-05-14 19:17:03 +02:00
|
|
|
const appState = useRef<AppStateStatus>(AppState.currentState);
|
|
|
|
const clipboardContent = useRef<undefined | string>();
|
2019-03-02 13:13:12 +01:00
|
|
|
|
2024-11-09 04:00:38 +01:00
|
|
|
useWatchConnectivity();
|
2024-11-09 02:07:35 +01:00
|
|
|
useWidgetCommunication();
|
2024-11-08 04:19:46 +01:00
|
|
|
useMenuElements();
|
2024-11-14 16:30:19 +01:00
|
|
|
useDeviceQuickActions();
|
2024-11-15 01:42:56 +01:00
|
|
|
useHandoffListener();
|
2024-11-08 04:19:46 +01:00
|
|
|
|
2024-05-14 19:36:31 +02:00
|
|
|
const processPushNotifications = useCallback(async () => {
|
2020-08-10 16:17:50 +02:00
|
|
|
await new Promise(resolve => setTimeout(resolve, 200));
|
2024-11-12 00:07:15 +01:00
|
|
|
try {
|
|
|
|
const notifications2process = await getStoredNotifications();
|
|
|
|
await clearStoredNotifications();
|
2024-11-12 02:59:28 +01:00
|
|
|
setApplicationIconBadgeNumber(0);
|
2024-11-12 02:20:53 +01:00
|
|
|
|
2024-11-12 00:07:15 +01:00
|
|
|
const deliveredNotifications = await getDeliveredNotifications();
|
|
|
|
setTimeout(async () => {
|
|
|
|
try {
|
2024-11-12 02:42:38 +01:00
|
|
|
removeAllDeliveredNotifications();
|
2024-11-12 00:07:15 +01:00
|
|
|
} catch (error) {
|
|
|
|
console.error('Failed to remove delivered notifications:', error);
|
|
|
|
}
|
|
|
|
}, 5000);
|
2021-01-01 20:15:40 +01:00
|
|
|
|
2024-11-12 02:20:53 +01:00
|
|
|
// Process notifications
|
|
|
|
for (const payload of notifications2process) {
|
|
|
|
const wasTapped = payload.foreground === false || (payload.foreground === true && payload.userInteraction);
|
|
|
|
|
|
|
|
console.log('processing push notification:', payload);
|
|
|
|
let wallet;
|
|
|
|
switch (+payload.type) {
|
|
|
|
case 2:
|
|
|
|
case 3:
|
|
|
|
wallet = wallets.find(w => w.weOwnAddress(payload.address));
|
|
|
|
break;
|
|
|
|
case 1:
|
|
|
|
case 4:
|
|
|
|
wallet = wallets.find(w => w.weOwnTransaction(payload.txid || payload.hash));
|
|
|
|
break;
|
|
|
|
}
|
2020-08-10 16:17:50 +02:00
|
|
|
|
2024-11-12 02:20:53 +01:00
|
|
|
if (wallet) {
|
|
|
|
const walletID = wallet.getID();
|
|
|
|
fetchAndSaveWalletTransactions(walletID);
|
|
|
|
if (wasTapped) {
|
|
|
|
if (payload.type !== 3 || wallet.chain === Chain.OFFCHAIN) {
|
|
|
|
navigationRef.dispatch(
|
|
|
|
CommonActions.navigate({
|
|
|
|
name: 'WalletTransactions',
|
|
|
|
params: {
|
|
|
|
walletID,
|
|
|
|
walletType: wallet.type,
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
navigationRef.navigate('ReceiveDetailsRoot', {
|
|
|
|
screen: 'ReceiveDetails',
|
2021-08-31 15:54:29 +02:00
|
|
|
params: {
|
|
|
|
walletID,
|
2024-11-12 02:20:53 +01:00
|
|
|
address: payload.address,
|
2021-08-31 15:54:29 +02:00
|
|
|
},
|
2024-11-12 02:20:53 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
console.log('could not find wallet while processing push notification, NOP');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (deliveredNotifications.length > 0) {
|
|
|
|
for (const payload of deliveredNotifications) {
|
|
|
|
const wasTapped = payload.foreground === false || (payload.foreground === true && payload.userInteraction);
|
|
|
|
|
|
|
|
console.log('processing push notification:', payload);
|
|
|
|
let wallet;
|
|
|
|
switch (+payload.type) {
|
|
|
|
case 2:
|
|
|
|
case 3:
|
|
|
|
wallet = wallets.find(w => w.weOwnAddress(payload.address));
|
|
|
|
break;
|
|
|
|
case 1:
|
|
|
|
case 4:
|
|
|
|
wallet = wallets.find(w => w.weOwnTransaction(payload.txid || payload.hash));
|
|
|
|
break;
|
2021-08-31 15:54:29 +02:00
|
|
|
}
|
2021-01-01 20:15:40 +01:00
|
|
|
|
2024-11-12 02:20:53 +01:00
|
|
|
if (wallet) {
|
|
|
|
const walletID = wallet.getID();
|
|
|
|
fetchAndSaveWalletTransactions(walletID);
|
|
|
|
if (wasTapped) {
|
|
|
|
if (payload.type !== 3 || wallet.chain === Chain.OFFCHAIN) {
|
|
|
|
navigationRef.dispatch(
|
|
|
|
CommonActions.navigate({
|
|
|
|
name: 'WalletTransactions',
|
|
|
|
params: {
|
|
|
|
walletID,
|
|
|
|
walletType: wallet.type,
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
navigationRef.navigate('ReceiveDetailsRoot', {
|
|
|
|
screen: 'ReceiveDetails',
|
|
|
|
params: {
|
|
|
|
walletID,
|
|
|
|
address: payload.address,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
console.log('could not find wallet while processing push notification, NOP');
|
|
|
|
}
|
2020-12-19 10:11:37 +01:00
|
|
|
}
|
2020-08-10 16:17:50 +02:00
|
|
|
}
|
2020-12-26 11:57:42 +01:00
|
|
|
|
2024-11-12 02:20:53 +01:00
|
|
|
if (deliveredNotifications.length > 0) {
|
|
|
|
refreshAllWalletTransactions();
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
console.error('Failed to process push notifications:', error);
|
2020-12-30 03:13:56 +01:00
|
|
|
}
|
2020-12-26 11:57:42 +01:00
|
|
|
return false;
|
2024-05-14 19:36:31 +02:00
|
|
|
}, [fetchAndSaveWalletTransactions, refreshAllWalletTransactions, wallets]);
|
|
|
|
|
2024-11-11 23:29:45 +01:00
|
|
|
useEffect(() => {
|
|
|
|
initializeNotifications(processPushNotifications);
|
2024-11-11 23:47:01 +01:00
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
}, []);
|
2024-11-11 23:29:45 +01:00
|
|
|
|
2024-05-14 19:36:31 +02:00
|
|
|
const handleOpenURL = useCallback(
|
2024-10-23 06:27:34 +02:00
|
|
|
async (event: { url: string }): Promise<void> => {
|
|
|
|
const { url } = event;
|
|
|
|
|
2024-10-23 06:44:03 +02:00
|
|
|
if (url) {
|
|
|
|
const decodedUrl = decodeURIComponent(url);
|
|
|
|
const fileName = decodedUrl.split('/').pop()?.toLowerCase();
|
|
|
|
|
2024-10-23 21:32:29 +02:00
|
|
|
if (fileName && /\.(jpe?g|png)$/i.test(fileName)) {
|
2024-10-23 06:44:03 +02:00
|
|
|
try {
|
|
|
|
const values = await RNQRGenerator.detect({
|
|
|
|
uri: decodedUrl,
|
|
|
|
});
|
|
|
|
|
|
|
|
if (values && values.values.length > 0) {
|
2024-10-23 06:54:25 +02:00
|
|
|
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
|
2024-10-23 06:44:03 +02:00
|
|
|
DeeplinkSchemaMatch.navigationRouteFor(
|
|
|
|
{ url: values.values[0] },
|
|
|
|
(value: [string, any]) => navigationRef.navigate(...value),
|
|
|
|
{
|
|
|
|
wallets,
|
|
|
|
addWallet,
|
|
|
|
saveToDisk,
|
|
|
|
setSharedCosigner,
|
|
|
|
},
|
|
|
|
);
|
2024-10-23 06:54:25 +02:00
|
|
|
} else {
|
|
|
|
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
|
|
|
presentAlert({ message: loc.send.qr_error_no_qrcode });
|
2024-10-23 06:27:34 +02:00
|
|
|
}
|
2024-10-23 06:44:03 +02:00
|
|
|
} catch (error) {
|
|
|
|
console.error('Error detecting QR code:', error);
|
|
|
|
}
|
2024-11-16 01:01:06 +01:00
|
|
|
} else {
|
|
|
|
DeeplinkSchemaMatch.navigationRouteFor(event, (value: [string, any]) => navigationRef.navigate(...value), {
|
|
|
|
wallets,
|
|
|
|
addWallet,
|
|
|
|
saveToDisk,
|
|
|
|
setSharedCosigner,
|
2024-11-18 22:23:44 +01:00
|
|
|
});
|
2024-10-23 06:27:34 +02:00
|
|
|
}
|
|
|
|
}
|
2024-05-14 19:36:31 +02:00
|
|
|
},
|
2024-10-23 06:27:34 +02:00
|
|
|
[wallets, addWallet, saveToDisk, setSharedCosigner],
|
2024-05-14 19:36:31 +02:00
|
|
|
);
|
|
|
|
const showClipboardAlert = useCallback(
|
|
|
|
({ contentType }: { contentType: undefined | string }) => {
|
|
|
|
triggerHapticFeedback(HapticFeedbackTypes.ImpactLight);
|
2024-11-10 15:25:39 +01:00
|
|
|
getClipboardContent().then(clipboard => {
|
|
|
|
if (!clipboard) return;
|
|
|
|
ActionSheet.showActionSheetWithOptions(
|
|
|
|
{
|
|
|
|
title: loc._.clipboard,
|
|
|
|
message: contentType === ClipboardContentType.BITCOIN ? loc.wallets.clipboard_bitcoin : loc.wallets.clipboard_lightning,
|
|
|
|
options: [loc._.cancel, loc._.continue],
|
|
|
|
cancelButtonIndex: 0,
|
|
|
|
},
|
|
|
|
buttonIndex => {
|
|
|
|
switch (buttonIndex) {
|
|
|
|
case 0:
|
|
|
|
break;
|
|
|
|
case 1:
|
|
|
|
handleOpenURL({ url: clipboard });
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
);
|
|
|
|
});
|
2024-05-14 19:36:31 +02:00
|
|
|
},
|
|
|
|
[handleOpenURL],
|
|
|
|
);
|
|
|
|
|
|
|
|
const handleAppStateChange = useCallback(
|
|
|
|
async (nextAppState: AppStateStatus | undefined) => {
|
|
|
|
if (wallets.length === 0) return;
|
|
|
|
if ((appState.current.match(/background/) && nextAppState === 'active') || nextAppState === undefined) {
|
|
|
|
setTimeout(() => A(A.ENUM.APP_UNSUSPENDED), 2000);
|
|
|
|
updateExchangeRate();
|
|
|
|
const processed = await processPushNotifications();
|
|
|
|
if (processed) return;
|
2024-11-10 15:25:39 +01:00
|
|
|
const clipboard = await getClipboardContent();
|
|
|
|
if (!clipboard) return;
|
2024-05-14 19:36:31 +02:00
|
|
|
const isAddressFromStoredWallet = wallets.some(wallet => {
|
|
|
|
if (wallet.chain === Chain.ONCHAIN) {
|
|
|
|
return wallet.isAddressValid && wallet.isAddressValid(clipboard) && wallet.weOwnAddress(clipboard);
|
|
|
|
} else {
|
|
|
|
return (wallet as LightningCustodianWallet).isInvoiceGeneratedByWallet(clipboard) || wallet.weOwnAddress(clipboard);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
const isBitcoinAddress = DeeplinkSchemaMatch.isBitcoinAddress(clipboard);
|
|
|
|
const isLightningInvoice = DeeplinkSchemaMatch.isLightningInvoice(clipboard);
|
|
|
|
const isLNURL = DeeplinkSchemaMatch.isLnUrl(clipboard);
|
|
|
|
const isBothBitcoinAndLightning = DeeplinkSchemaMatch.isBothBitcoinAndLightning(clipboard);
|
|
|
|
if (
|
|
|
|
!isAddressFromStoredWallet &&
|
|
|
|
clipboardContent.current !== clipboard &&
|
|
|
|
(isBitcoinAddress || isLightningInvoice || isLNURL || isBothBitcoinAndLightning)
|
|
|
|
) {
|
|
|
|
let contentType;
|
|
|
|
if (isBitcoinAddress) {
|
|
|
|
contentType = ClipboardContentType.BITCOIN;
|
|
|
|
} else if (isLightningInvoice || isLNURL) {
|
|
|
|
contentType = ClipboardContentType.LIGHTNING;
|
|
|
|
} else if (isBothBitcoinAndLightning) {
|
|
|
|
contentType = ClipboardContentType.BITCOIN;
|
|
|
|
}
|
|
|
|
showClipboardAlert({ contentType });
|
2021-09-09 21:36:35 +02:00
|
|
|
}
|
2024-05-14 19:36:31 +02:00
|
|
|
clipboardContent.current = clipboard;
|
2020-04-06 17:33:45 +02:00
|
|
|
}
|
2024-05-14 19:36:31 +02:00
|
|
|
if (nextAppState) {
|
|
|
|
appState.current = nextAppState;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
[processPushNotifications, showClipboardAlert, wallets],
|
|
|
|
);
|
|
|
|
|
|
|
|
const addListeners = useCallback(() => {
|
|
|
|
const urlSubscription = Linking.addEventListener('url', handleOpenURL);
|
|
|
|
const appStateSubscription = AppState.addEventListener('change', handleAppStateChange);
|
|
|
|
|
|
|
|
return {
|
|
|
|
urlSubscription,
|
|
|
|
appStateSubscription,
|
|
|
|
};
|
2024-11-09 00:12:14 +01:00
|
|
|
}, [handleOpenURL, handleAppStateChange]);
|
2024-05-14 19:36:31 +02:00
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
const subscriptions = addListeners();
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
subscriptions.urlSubscription?.remove();
|
|
|
|
subscriptions.appStateSubscription?.remove();
|
|
|
|
};
|
|
|
|
}, [addListeners]);
|
2021-09-24 21:32:22 +02:00
|
|
|
|
2024-11-15 01:42:56 +01:00
|
|
|
return null;
|
2020-10-11 09:07:22 +02:00
|
|
|
};
|
2019-03-02 13:13:12 +01:00
|
|
|
|
2024-05-13 18:43:57 +02:00
|
|
|
export default CompanionDelegates;
|