BlueWallet/App.js

390 lines
14 KiB
JavaScript
Raw Normal View History

2020-05-27 13:12:17 +02:00
import 'react-native-gesture-handler'; // should be on top
import React, { useContext, useEffect, useRef, useState } from 'react';
import {
AppState,
DeviceEventEmitter,
NativeModules,
NativeEventEmitter,
KeyboardAvoidingView,
Linking,
Platform,
StyleSheet,
UIManager,
useColorScheme,
View,
2021-03-12 14:19:51 +01:00
StatusBar,
} from 'react-native';
2020-07-15 19:32:59 +02:00
import { NavigationContainer, CommonActions } from '@react-navigation/native';
2020-05-27 13:12:17 +02:00
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { navigationRef } from './NavigationService';
import * as NavigationService from './NavigationService';
2020-07-15 19:32:59 +02:00
import { BlueTextCentered, BlueButton, SecondButton } from './BlueComponents';
2019-03-05 02:04:40 +01:00
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import { Chain } from './models/bitcoinUnits';
import OnAppLaunch from './class/on-app-launch';
import DeeplinkSchemaMatch from './class/deeplink-schema-match';
2020-07-20 15:38:46 +02:00
import loc from './loc';
2020-07-15 19:32:59 +02:00
import { BlueDefaultTheme, BlueDarkTheme, BlueCurrentTheme } from './components/themes';
import BottomModal from './components/BottomModal';
import InitRoot from './Navigation';
import BlueClipboard from './blue_modules/clipboard';
import { isDesktop } from './blue_modules/environment';
import { BlueStorageContext } from './blue_modules/storage-context';
import WatchConnectivity from './WatchConnectivity';
import DeviceQuickActions from './class/quick-actions';
import Notifications from './blue_modules/notifications';
import WalletImport from './class/wallet-import';
import Biometric from './class/biometrics';
import WidgetCommunication from './blue_modules/WidgetCommunication';
import changeNavigationBarColor from 'react-native-navigation-bar-color';
const A = require('./blue_modules/analytics');
2021-09-09 21:36:35 +02:00
const currency = require('./blue_modules/currency');
const eventEmitter = new NativeEventEmitter(NativeModules.EventEmitter);
const ClipboardContentType = Object.freeze({
BITCOIN: 'BITCOIN',
LIGHTNING: 'LIGHTNING',
});
if (Platform.OS === 'android') {
if (UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
}
2020-10-11 09:07:22 +02:00
const App = () => {
2020-12-30 03:13:56 +01:00
const { walletsInitialized, wallets, addWallet, saveToDisk, fetchAndSaveWalletTransactions, refreshAllWalletTransactions } = useContext(
BlueStorageContext,
);
2020-10-11 09:07:22 +02:00
const appState = useRef(AppState.currentState);
const [isClipboardContentModalVisible, setIsClipboardContentModalVisible] = useState(false);
const [clipboardContentType, setClipboardContentType] = useState();
const clipboardContent = useRef();
2020-10-11 09:07:22 +02:00
const colorScheme = useColorScheme();
const stylesHook = StyleSheet.create({
modalContent: {
backgroundColor: colorScheme === 'dark' ? BlueDarkTheme.colors.elevated : BlueDefaultTheme.colors.elevated,
},
});
2021-01-01 20:15:40 +01:00
const onNotificationReceived = async notification => {
2020-12-27 18:04:32 +01:00
const payload = Object.assign({}, notification, notification.data);
if (notification.data && notification.data.data) Object.assign(payload, notification.data.data);
2021-01-01 20:15:40 +01:00
payload.foreground = true;
await Notifications.addNotification(payload);
// if user is staring at the app when he receives the notification we process it instantly
// so app refetches related wallet
if (payload.foreground) await processPushNotifications();
};
2020-12-27 01:10:54 +01:00
const openSettings = () => {
NavigationService.dispatch(
CommonActions.navigate({
name: 'Settings',
}),
);
};
2020-10-11 09:07:22 +02:00
useEffect(() => {
if (walletsInitialized) {
addListeners();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [walletsInitialized]);
useEffect(() => {
2020-10-11 09:07:22 +02:00
return () => {
Linking.removeEventListener('url', handleOpenURL);
AppState.removeEventListener('change', handleAppStateChange);
eventEmitter.removeAllListeners('onNotificationReceived');
2020-12-27 01:10:54 +01:00
eventEmitter.removeAllListeners('openSettings');
2020-10-11 09:07:22 +02:00
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
2020-06-21 02:16:05 +02:00
2020-10-11 09:07:22 +02:00
useEffect(() => {
if (colorScheme) {
BlueCurrentTheme.updateColorScheme();
if (colorScheme === 'light') {
changeNavigationBarColor(BlueDefaultTheme.colors.background, true, true);
} else {
changeNavigationBarColor(BlueDarkTheme.colors.buttonBackgroundColor, false, true);
}
2020-07-15 19:32:59 +02:00
}
2020-10-11 09:07:22 +02:00
}, [colorScheme]);
2020-07-15 19:32:59 +02:00
2020-10-11 09:07:22 +02:00
const addListeners = () => {
Linking.addEventListener('url', handleOpenURL);
AppState.addEventListener('change', handleAppStateChange);
DeviceEventEmitter.addListener('quickActionShortcut', walletQuickActions);
DeviceQuickActions.popInitialAction().then(popInitialAction);
2020-10-11 09:07:22 +02:00
handleAppStateChange(undefined);
2021-01-01 20:15:40 +01:00
/*
When a notification on iOS is shown while the app is on foreground;
On willPresent on AppDelegate.m
*/
eventEmitter.addListener('onNotificationReceived', onNotificationReceived);
2020-12-27 01:10:54 +01:00
eventEmitter.addListener('openSettings', openSettings);
2020-06-21 02:16:05 +02:00
};
2020-10-11 09:07:22 +02:00
const popInitialAction = async data => {
if (data) {
const wallet = wallets.find(wallet => wallet.getID() === data.userInfo.url.split('wallet/')[1]);
NavigationService.dispatch(
2020-05-27 13:12:17 +02:00
CommonActions.navigate({
name: 'WalletTransactions',
2020-05-28 15:57:01 +02:00
key: `WalletTransactions-${wallet.getID()}`,
params: {
walletID: wallet.getID(),
walletType: wallet.type,
},
}),
);
} else {
const url = await Linking.getInitialURL();
if (url) {
2019-12-28 01:53:34 +01:00
if (DeeplinkSchemaMatch.hasSchema(url)) {
2020-10-11 09:07:22 +02:00
handleOpenURL({ url });
}
} else {
const isViewAllWalletsEnabled = await OnAppLaunch.isViewAllWalletsEnabled();
if (!isViewAllWalletsEnabled) {
const selectedDefaultWallet = await OnAppLaunch.getSelectedDefaultWallet();
const wallet = wallets.find(wallet => wallet.getID() === selectedDefaultWallet.getID());
if (wallet) {
NavigationService.dispatch(
2020-05-27 13:12:17 +02:00
CommonActions.navigate({
name: 'WalletTransactions',
key: `WalletTransactions-${wallet.getID()}`,
params: {
walletID: wallet.getID(),
walletType: wallet.type,
},
}),
);
}
}
}
}
};
2020-10-11 09:07:22 +02:00
const walletQuickActions = data => {
const wallet = wallets.find(wallet => wallet.getID() === data.userInfo.url.split('wallet/')[1]);
NavigationService.dispatch(
2020-05-27 13:12:17 +02:00
CommonActions.navigate({
name: 'WalletTransactions',
key: `WalletTransactions-${wallet.getID()}`,
params: {
walletID: wallet.getID(),
walletType: wallet.type,
},
}),
);
};
/**
* Processes push notifications stored in AsyncStorage. Might navigate to some screen.
*
* @returns {Promise<boolean>} returns TRUE if notification was processed _and acted_ upon, i.e. navigation happened
* @private
*/
2020-10-11 09:07:22 +02:00
const processPushNotifications = async () => {
2021-09-07 21:30:07 +02:00
if (!walletsInitialized) {
console.log('not processing push notifications because wallets are not initialized');
return;
}
await new Promise(resolve => setTimeout(resolve, 200));
// sleep needed as sometimes unsuspend is faster than notification module actually saves notifications to async storage
const notifications2process = await Notifications.getStoredNotifications();
2021-01-01 20:15:40 +01:00
await Notifications.clearStoredNotifications();
Notifications.setApplicationIconBadgeNumber(0);
2020-12-29 23:54:56 +01:00
const deliveredNotifications = await Notifications.getDeliveredNotifications();
2021-01-01 20:15:40 +01:00
setTimeout(() => Notifications.removeAllDeliveredNotifications(), 5000); // so notification bubble wont disappear too fast
for (const payload of notifications2process) {
2021-01-02 00:39:26 +01:00
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;
}
if (wallet) {
const walletID = wallet.getID();
fetchAndSaveWalletTransactions(walletID);
if (wasTapped) {
2021-08-31 15:54:29 +02:00
if (payload.type !== 3 || wallet.chain === Chain.OFFCHAIN) {
NavigationService.dispatch(
CommonActions.navigate({
name: 'WalletTransactions',
key: `WalletTransactions-${wallet.getID()}`,
params: {
walletID,
walletType: wallet.type,
},
}),
);
} else {
NavigationService.navigate('ReceiveDetailsRoot', {
screen: 'ReceiveDetails',
params: {
walletID,
2021-08-31 15:54:29 +02:00
address: payload.address,
},
2021-08-31 15:54:29 +02:00
});
}
2021-01-01 20:15:40 +01:00
return true;
}
} else {
2021-01-01 20:15:40 +01:00
console.log('could not find wallet while processing push notification, NOP');
}
2021-01-01 20:15:40 +01:00
} // end foreach notifications loop
2021-01-01 20:15:40 +01:00
if (deliveredNotifications.length > 0) {
// notification object is missing userInfo. We know we received a notification but don't have sufficient
// data to refresh 1 wallet. let's refresh all.
2020-12-30 03:13:56 +01:00
refreshAllWalletTransactions();
}
2021-01-01 20:15:40 +01:00
// if we are here - we did not act upon any push
return false;
2020-10-11 09:07:22 +02:00
};
2020-10-11 09:07:22 +02:00
const handleAppStateChange = async nextAppState => {
2021-09-09 21:36:35 +02:00
if (wallets.length === 0) return;
if ((appState.current.match(/background/) && nextAppState === 'active') || nextAppState === undefined) {
setTimeout(() => A(A.ENUM.APP_UNSUSPENDED), 2000);
currency.updateExchangeRate();
const processed = await processPushNotifications();
if (processed) return;
const clipboard = await BlueClipboard.getClipboardContent();
const isAddressFromStoredWallet = wallets.some(wallet => {
if (wallet.chain === Chain.ONCHAIN) {
// checking address validity is faster than unwrapping hierarchy only to compare it to garbage
return wallet.isAddressValid && wallet.isAddressValid(clipboard) && wallet.weOwnAddress(clipboard);
} else {
return wallet.isInvoiceGeneratedByWallet(clipboard) || wallet.weOwnAddress(clipboard);
2019-03-05 02:04:40 +01:00
}
2021-09-09 21:36:35 +02:00
});
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)
) {
if (isBitcoinAddress) {
setClipboardContentType(ClipboardContentType.BITCOIN);
} else if (isLightningInvoice || isLNURL) {
setClipboardContentType(ClipboardContentType.LIGHTNING);
} else if (isBothBitcoinAndLightning) {
setClipboardContentType(ClipboardContentType.BITCOIN);
}
setIsClipboardContentModalVisible(true);
}
2021-09-09 21:36:35 +02:00
clipboardContent.current = clipboard;
}
if (nextAppState) {
appState.current = nextAppState;
}
};
2020-10-11 09:07:22 +02:00
const handleOpenURL = event => {
DeeplinkSchemaMatch.navigationRouteFor(event, value => NavigationService.navigate(...value), { wallets, addWallet, saveToDisk });
};
2020-10-11 09:07:22 +02:00
const hideClipboardContentModal = () => {
setIsClipboardContentModalVisible(false);
};
2020-10-11 09:07:22 +02:00
const renderClipboardContentModal = () => {
return (
<BottomModal
2019-05-03 14:36:11 +02:00
onModalShow={() => ReactNativeHapticFeedback.trigger('impactLight', { ignoreAndroidSystemSettings: false })}
2020-10-11 09:07:22 +02:00
isVisible={isClipboardContentModalVisible}
onClose={hideClipboardContentModal}
>
2021-02-25 02:56:06 +01:00
<KeyboardAvoidingView enabled={!Platform.isPad} behavior={Platform.OS === 'ios' ? 'position' : null}>
2020-10-11 09:07:22 +02:00
<View style={[styles.modalContent, stylesHook.modalContent]}>
<BlueTextCentered>
{clipboardContentType === ClipboardContentType.BITCOIN && loc.wallets.clipboard_bitcoin}
{clipboardContentType === ClipboardContentType.LIGHTNING && loc.wallets.clipboard_lightning}
</BlueTextCentered>
<View style={styles.modelContentButtonLayout}>
2020-10-11 09:07:22 +02:00
<SecondButton noMinWidth title={loc._.cancel} onPress={hideClipboardContentModal} />
<View style={styles.space} />
<BlueButton
noMinWidth
title={loc._.ok}
2020-10-11 09:07:22 +02:00
onPress={async () => {
setIsClipboardContentModalVisible(false);
const clipboard = await BlueClipboard.getClipboardContent();
2021-09-17 05:28:33 +02:00
setTimeout(() => handleOpenURL({ url: clipboard }), 650);
}}
/>
</View>
</View>
</KeyboardAvoidingView>
</BottomModal>
);
};
2020-10-11 09:07:22 +02:00
return (
<SafeAreaProvider>
<View style={styles.root}>
2021-03-12 14:19:51 +01:00
<StatusBar barStyle={colorScheme === 'dark' ? 'light-content' : 'dark-content'} backgroundColor="transparent" translucent />
2020-10-11 09:07:22 +02:00
<NavigationContainer ref={navigationRef} theme={colorScheme === 'dark' ? BlueDarkTheme : BlueDefaultTheme}>
<InitRoot />
<Notifications onProcessNotifications={processPushNotifications} />
{renderClipboardContentModal()}
2020-10-11 09:07:22 +02:00
</NavigationContainer>
2021-08-03 16:50:07 +02:00
{walletsInitialized && !isDesktop && <WatchConnectivity />}
2020-10-11 09:07:22 +02:00
</View>
<DeviceQuickActions />
<WalletImport />
<Biometric />
2020-11-02 14:11:28 +01:00
<WidgetCommunication />
2020-10-11 09:07:22 +02:00
</SafeAreaProvider>
);
};
const styles = StyleSheet.create({
root: {
flex: 1,
},
space: {
marginHorizontal: 8,
},
modalContent: {
padding: 22,
justifyContent: 'center',
alignItems: 'center',
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
borderColor: 'rgba(0, 0, 0, 0.1)',
minHeight: 200,
height: 200,
},
modelContentButtonLayout: {
flexDirection: 'row',
margin: 16,
justifyContent: 'space-between',
alignItems: 'flex-end',
},
});
2020-10-11 09:07:22 +02:00
export default App;