From b537944d4e9b56336601b2c24604ded318323646 Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Sun, 12 May 2024 16:45:37 -0400 Subject: [PATCH 01/64] REF: Place network calls in InteractionManager --- blue_modules/storage-context.tsx | 104 ++++++++++++++++--------------- 1 file changed, 54 insertions(+), 50 deletions(-) diff --git a/blue_modules/storage-context.tsx b/blue_modules/storage-context.tsx index 5b2caf0b2..6f79b3183 100644 --- a/blue_modules/storage-context.tsx +++ b/blue_modules/storage-context.tsx @@ -106,61 +106,65 @@ export const BlueStorageProvider = ({ children }: { children: React.ReactNode }) }; const refreshAllWalletTransactions = async (lastSnappedTo?: number, showUpdateStatusIndicator: boolean = true) => { - let noErr = true; - try { - await BlueElectrum.waitTillConnected(); - if (showUpdateStatusIndicator) { - setWalletTransactionUpdateStatus(WalletTransactionsStatus.ALL); + InteractionManager.runAfterInteractions(async () => { + let noErr = true; + try { + await BlueElectrum.waitTillConnected(); + if (showUpdateStatusIndicator) { + setWalletTransactionUpdateStatus(WalletTransactionsStatus.ALL); + } + const paymentCodesStart = Date.now(); + await BlueApp.fetchSenderPaymentCodes(lastSnappedTo); + const paymentCodesEnd = Date.now(); + console.log('fetch payment codes took', (paymentCodesEnd - paymentCodesStart) / 1000, 'sec'); + const balanceStart = +new Date(); + await fetchWalletBalances(lastSnappedTo); + const balanceEnd = +new Date(); + console.log('fetch balance took', (balanceEnd - balanceStart) / 1000, 'sec'); + const start = +new Date(); + await fetchWalletTransactions(lastSnappedTo); + const end = +new Date(); + console.log('fetch tx took', (end - start) / 1000, 'sec'); + } catch (err) { + noErr = false; + console.warn(err); + } finally { + setWalletTransactionUpdateStatus(WalletTransactionsStatus.NONE); } - const paymentCodesStart = Date.now(); - await BlueApp.fetchSenderPaymentCodes(lastSnappedTo); - const paymentCodesEnd = Date.now(); - console.log('fetch payment codes took', (paymentCodesEnd - paymentCodesStart) / 1000, 'sec'); - const balanceStart = +new Date(); - await fetchWalletBalances(lastSnappedTo); - const balanceEnd = +new Date(); - console.log('fetch balance took', (balanceEnd - balanceStart) / 1000, 'sec'); - const start = +new Date(); - await fetchWalletTransactions(lastSnappedTo); - const end = +new Date(); - console.log('fetch tx took', (end - start) / 1000, 'sec'); - } catch (err) { - noErr = false; - console.warn(err); - } finally { - setWalletTransactionUpdateStatus(WalletTransactionsStatus.NONE); - } - if (noErr) await saveToDisk(); // caching + if (noErr) await saveToDisk(); // caching + }); }; const fetchAndSaveWalletTransactions = async (walletID: string) => { - const index = wallets.findIndex(wallet => wallet.getID() === walletID); - let noErr = true; - try { - // 5sec debounce: - if (+new Date() - _lastTimeTriedToRefetchWallet[walletID] < 5000) { - console.log('re-fetch wallet happens too fast; NOP'); - return; - } - _lastTimeTriedToRefetchWallet[walletID] = +new Date(); + InteractionManager.runAfterInteractions(async () => { + const index = wallets.findIndex(wallet => wallet.getID() === walletID); + let noErr = true; + try { + // 5sec debounce: + if (+new Date() - _lastTimeTriedToRefetchWallet[walletID] < 5000) { + console.log('re-fetch wallet happens too fast; NOP'); + return; + } + _lastTimeTriedToRefetchWallet[walletID] = +new Date(); - await BlueElectrum.waitTillConnected(); - setWalletTransactionUpdateStatus(walletID); - const balanceStart = +new Date(); - await fetchWalletBalances(index); - const balanceEnd = +new Date(); - console.log('fetch balance took', (balanceEnd - balanceStart) / 1000, 'sec'); - const start = +new Date(); - await fetchWalletTransactions(index); - const end = +new Date(); - console.log('fetch tx took', (end - start) / 1000, 'sec'); - } catch (err) { - noErr = false; - console.warn(err); - } finally { - setWalletTransactionUpdateStatus(WalletTransactionsStatus.NONE); - } - if (noErr) await saveToDisk(); // caching + await BlueElectrum.waitTillConnected(); + setWalletTransactionUpdateStatus(walletID); + const balanceStart = +new Date(); + await fetchWalletBalances(index); + const balanceEnd = +new Date(); + console.log('fetch balance took', (balanceEnd - balanceStart) / 1000, 'sec'); + const start = +new Date(); + await fetchWalletTransactions(index); + const end = +new Date(); + console.log('fetch tx took', (end - start) / 1000, 'sec'); + } catch (err) { + noErr = false; + console.warn(err); + } finally { + setWalletTransactionUpdateStatus(WalletTransactionsStatus.NONE); + } + if (noErr) await saveToDisk(); // caching + }); }; const addWallet = (wallet: TWallet) => { From 3b7e074d868111feed33f9eb2746cd5ee370687f Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Mon, 13 May 2024 12:43:57 -0400 Subject: [PATCH 02/64] REF: App.js to tsx --- App.tsx | 52 ++++++++++ App.js => components/CompanionDelegates.tsx | 108 ++++++-------------- navigation/DetailViewScreensStack.tsx | 2 +- Navigation.tsx => navigation/index.tsx | 12 +-- 4 files changed, 89 insertions(+), 85 deletions(-) create mode 100644 App.tsx rename App.js => components/CompanionDelegates.tsx (74%) rename Navigation.tsx => navigation/index.tsx (78%) diff --git a/App.tsx b/App.tsx new file mode 100644 index 000000000..6b5e654c5 --- /dev/null +++ b/App.tsx @@ -0,0 +1,52 @@ +import 'react-native-gesture-handler'; // should be on top +import React, { Suspense, lazy, useEffect } from 'react'; +import { NativeModules, Platform, UIManager, useColorScheme, LogBox } from 'react-native'; +import { NavigationContainer } from '@react-navigation/native'; +import { SafeAreaProvider } from 'react-native-safe-area-context'; +import { navigationRef } from './NavigationService'; +import { BlueDefaultTheme, BlueDarkTheme } from './components/themes'; +import { NavigationProvider } from './components/NavigationProvider'; +import MainRoot from './navigation'; +import { useStorage } from './blue_modules/storage-context'; +import Biometric from './class/biometrics'; +const CompanionDelegates = lazy(() => import('./components/CompanionDelegates')); +const { SplashScreen } = NativeModules; + +LogBox.ignoreLogs(['Require cycle:', 'Battery state `unknown` and monitoring disabled, this is normal for simulators and tvOS.']); + +if (Platform.OS === 'android') { + if (UIManager.setLayoutAnimationEnabledExperimental) { + UIManager.setLayoutAnimationEnabledExperimental(true); + } +} + +const App = () => { + const { walletsInitialized } = useStorage(); + const colorScheme = useColorScheme(); + + useEffect(() => { + if (Platform.OS === 'ios') { + // Call hide to setup the listener on the native side + SplashScreen?.addObserver(); + } + }, []); + + return ( + + + + + + + {/* {walletsInitialized && ( + + + + )} */} + + + + ); +}; + +export default App; diff --git a/App.js b/components/CompanionDelegates.tsx similarity index 74% rename from App.js rename to components/CompanionDelegates.tsx index a87105306..090a6a5a2 100644 --- a/App.js +++ b/components/CompanionDelegates.tsx @@ -1,43 +1,25 @@ import 'react-native-gesture-handler'; // should be on top -import React, { useContext, useEffect, useRef } from 'react'; -import { - AppState, - NativeModules, - NativeEventEmitter, - Linking, - Platform, - StyleSheet, - UIManager, - useColorScheme, - View, - LogBox, -} from 'react-native'; -import { NavigationContainer, CommonActions } from '@react-navigation/native'; -import { SafeAreaProvider } from 'react-native-safe-area-context'; -import { navigationRef } from './NavigationService'; -import * as NavigationService from './NavigationService'; -import { Chain } from './models/bitcoinUnits'; -import DeeplinkSchemaMatch from './class/deeplink-schema-match'; -import loc from './loc'; -import { BlueDefaultTheme, BlueDarkTheme } from './components/themes'; -import BlueClipboard from './blue_modules/clipboard'; -import { BlueStorageContext } from './blue_modules/storage-context'; -import WatchConnectivity from './WatchConnectivity'; -import Notifications from './blue_modules/notifications'; -import Biometric from './class/biometrics'; -import WidgetCommunication from './components/WidgetCommunication'; -import ActionSheet from './screen/ActionSheet'; -import triggerHapticFeedback, { HapticFeedbackTypes } from './blue_modules/hapticFeedback'; -import MenuElements from './components/MenuElements'; -import { updateExchangeRate } from './blue_modules/currency'; -import { NavigationProvider } from './components/NavigationProvider'; -import A from './blue_modules/analytics'; -import HandOffComponentListener from './components/HandOffComponentListener'; -import DeviceQuickActions from './components/DeviceQuickActions'; -import MainRoot from './Navigation'; +import React, { useEffect, useRef } from 'react'; +import { AppState, NativeModules, NativeEventEmitter, Linking, Platform, UIManager, LogBox } from 'react-native'; +import { CommonActions } from '@react-navigation/native'; +import * as NavigationService from '../NavigationService'; +import { Chain } from '../models/bitcoinUnits'; +import DeeplinkSchemaMatch from '../class/deeplink-schema-match'; +import loc from '../loc'; +import BlueClipboard from '../blue_modules/clipboard'; +import WatchConnectivity from '../WatchConnectivity'; +import Notifications from '../blue_modules/notifications'; +import WidgetCommunication from '../components/WidgetCommunication'; +import ActionSheet from '../screen/ActionSheet'; +import triggerHapticFeedback, { HapticFeedbackTypes } from '../blue_modules/hapticFeedback'; +import MenuElements from '../components/MenuElements'; +import { updateExchangeRate } from '../blue_modules/currency'; +import A from '../blue_modules/analytics'; +import HandOffComponentListener from '../components/HandOffComponentListener'; +import DeviceQuickActions from '../components/DeviceQuickActions'; +import { useStorage } from '../blue_modules/storage-context'; const eventEmitter = Platform.OS === 'ios' ? new NativeEventEmitter(NativeModules.EventEmitter) : undefined; -const { SplashScreen } = NativeModules; LogBox.ignoreLogs(['Require cycle:', 'Battery state `unknown` and monitoring disabled, this is normal for simulators and tvOS.']); @@ -52,19 +34,10 @@ if (Platform.OS === 'android') { } } -const App = () => { - const { - walletsInitialized, - wallets, - addWallet, - saveToDisk, - fetchAndSaveWalletTransactions, - refreshAllWalletTransactions, - setSharedCosigner, - } = useContext(BlueStorageContext); +const CompanionDelegates = () => { + const { wallets, addWallet, saveToDisk, fetchAndSaveWalletTransactions, refreshAllWalletTransactions, setSharedCosigner } = useStorage(); const appState = useRef(AppState.currentState); const clipboardContent = useRef(); - const colorScheme = useColorScheme(); const onNotificationReceived = async notification => { const payload = Object.assign({}, notification, notification.data); @@ -258,37 +231,16 @@ const App = () => { }); }; - useEffect(() => { - if (Platform.OS === 'ios') { - // Call hide to setup the listener on the native side - SplashScreen?.addObserver(); - } - }, []); - return ( - - - - - - - - - - - - - - - - + <> + + + + + + + ); }; -const styles = StyleSheet.create({ - root: { - flex: 1, - }, -}); - -export default App; +export default CompanionDelegates; diff --git a/navigation/DetailViewScreensStack.tsx b/navigation/DetailViewScreensStack.tsx index 6bbdb3cd2..51308c0c2 100644 --- a/navigation/DetailViewScreensStack.tsx +++ b/navigation/DetailViewScreensStack.tsx @@ -75,7 +75,7 @@ import { NavigationDefaultOptionsForDesktop, NavigationFormModalOptions, StatusBarLightOptions, -} from '../Navigation'; +} from './'; const DetailViewRoot = createNativeStackNavigator(); const DetailViewStackScreensStack = () => { diff --git a/Navigation.tsx b/navigation/index.tsx similarity index 78% rename from Navigation.tsx rename to navigation/index.tsx index 8f98128d5..49a07bbc3 100644 --- a/Navigation.tsx +++ b/navigation/index.tsx @@ -1,12 +1,12 @@ import React, { Suspense, lazy } from 'react'; import { NativeStackNavigationOptions, createNativeStackNavigator } from '@react-navigation/native-stack'; -import { useStorage } from './blue_modules/storage-context'; -import UnlockWith from './screen/UnlockWith'; -import { LazyLoadingIndicator } from './navigation/LazyLoadingIndicator'; -import { isHandset } from './blue_modules/environment'; +import { useStorage } from '../blue_modules/storage-context'; +import UnlockWith from '../screen/UnlockWith'; +import { LazyLoadingIndicator } from './LazyLoadingIndicator'; +import { isHandset } from '../blue_modules/environment'; -const DetailViewScreensStack = lazy(() => import('./navigation/DetailViewScreensStack')); -const DrawerRoot = lazy(() => import('./navigation/DrawerRoot')); +const DetailViewScreensStack = lazy(() => import('./DetailViewScreensStack')); +const DrawerRoot = lazy(() => import('./DrawerRoot')); export const NavigationDefaultOptions: NativeStackNavigationOptions = { headerShown: false, From 7b09b3552297b299433cee8f74b5619eaeac70a4 Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Mon, 13 May 2024 13:08:47 -0400 Subject: [PATCH 03/64] REF: Single source of truth for useIsLargeScreen --- components/Context/LargeScreenProvider.tsx | 43 ++++++++++++++++++++ hooks/useIsLargeScreen.ts | 47 ++++------------------ index.js => index.tsx | 13 +++--- 3 files changed, 59 insertions(+), 44 deletions(-) create mode 100644 components/Context/LargeScreenProvider.tsx rename index.js => index.tsx (74%) diff --git a/components/Context/LargeScreenProvider.tsx b/components/Context/LargeScreenProvider.tsx new file mode 100644 index 000000000..5bab2d89d --- /dev/null +++ b/components/Context/LargeScreenProvider.tsx @@ -0,0 +1,43 @@ +import React, { createContext, useState, useEffect, useMemo, ReactNode } from 'react'; +import { Dimensions } from 'react-native'; +import { isTablet } from 'react-native-device-info'; +import { isDesktop } from '../../blue_modules/environment'; + +interface ILargeScreenContext { + isLargeScreen: boolean; +} + +export const LargeScreenContext = createContext(undefined); + +interface LargeScreenProviderProps { + children: ReactNode; +} + +export const LargeScreenProvider: React.FC = ({ children }) => { + const [windowWidth, setWindowWidth] = useState(Dimensions.get('window').width); + const screenWidth: number = useMemo(() => Dimensions.get('screen').width, []); + + useEffect(() => { + const updateScreenUsage = (): void => { + const newWindowWidth = Dimensions.get('window').width; + if (newWindowWidth !== windowWidth) { + setWindowWidth(newWindowWidth); + } + }; + + const subscription = Dimensions.addEventListener('change', updateScreenUsage); + return () => subscription.remove(); + }, [windowWidth]); + + const isLargeScreen: boolean = useMemo(() => { + const isRunningOnTablet = isTablet(); + const halfScreenWidth = windowWidth >= screenWidth / 2; + const condition = (isRunningOnTablet && halfScreenWidth) || isDesktop; + console.debug( + `LargeScreenProvider.isLargeScreen: width: ${windowWidth}, Screen width: ${screenWidth}, Is tablet: ${isTablet()}, Is large screen: ${condition}, isDesktkop: ${isDesktop}`, + ); + return condition; + }, [windowWidth, screenWidth]); + + return {children}; +}; diff --git a/hooks/useIsLargeScreen.ts b/hooks/useIsLargeScreen.ts index 8c706cade..14ec0cd2d 100644 --- a/hooks/useIsLargeScreen.ts +++ b/hooks/useIsLargeScreen.ts @@ -1,41 +1,10 @@ -import { useState, useEffect, useMemo } from 'react'; -import { Dimensions } from 'react-native'; -import { isTablet } from 'react-native-device-info'; -import { isDesktop } from '../blue_modules/environment'; +import { useContext } from 'react'; +import { LargeScreenContext } from '../components/Context/LargeScreenProvider'; -// Custom hook to determine if the screen is large -export const useIsLargeScreen = () => { - const [windowWidth, setWindowWidth] = useState(Dimensions.get('window').width); - const screenWidth = useMemo(() => Dimensions.get('screen').width, []); - - useEffect(() => { - const updateScreenUsage = () => { - const newWindowWidth = Dimensions.get('window').width; - if (newWindowWidth !== windowWidth) { - console.debug(`Window width changed: ${newWindowWidth}`); - setWindowWidth(newWindowWidth); - } - }; - - // Add event listener for dimension changes - const subscription = Dimensions.addEventListener('change', updateScreenUsage); - - // Cleanup function to remove the event listener - return () => { - subscription.remove(); - }; - }, [windowWidth]); - - // Determine if the window width is at least half of the screen width - const isLargeScreen = useMemo(() => { - const isRunningOnTablet = isTablet(); - const halfScreenWidth = windowWidth >= screenWidth / 2; - const condition = (isRunningOnTablet && halfScreenWidth) || isDesktop; - console.debug( - `Window width: ${windowWidth}, Screen width: ${screenWidth}, Is tablet: ${isTablet()}, Is large screen: ${condition}, isDesktkop: ${isDesktop}`, - ); - return condition; - }, [windowWidth, screenWidth]); - - return isLargeScreen; +export const useIsLargeScreen = (): boolean => { + const context = useContext(LargeScreenContext); + if (context === undefined) { + throw new Error('useIsLargeScreen must be used within a LargeScreenProvider'); + } + return context.isLargeScreen; }; diff --git a/index.js b/index.tsx similarity index 74% rename from index.js rename to index.tsx index 0783e0c0c..8055bca79 100644 --- a/index.js +++ b/index.tsx @@ -8,6 +8,7 @@ import { BlueStorageProvider } from './blue_modules/storage-context'; import A from './blue_modules/analytics'; import { SettingsProvider } from './components/Context/SettingsContext'; import { restoreSavedPreferredFiatCurrencyAndExchangeFromStorage } from './blue_modules/currency'; +import { LargeScreenProvider } from './components/Context/LargeScreenProvider'; if (!Error.captureStackTrace) { // captureStackTrace is only available when debugging @@ -21,11 +22,13 @@ const BlueAppComponent = () => { }, []); return ( - - - - - + + + + + + + ); }; From 254ed0297db362ae333a5897a2623f431bf7578a Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Mon, 13 May 2024 13:12:37 -0400 Subject: [PATCH 04/64] Update DetailViewScreensStack.tsx --- navigation/DetailViewScreensStack.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/navigation/DetailViewScreensStack.tsx b/navigation/DetailViewScreensStack.tsx index 6bbdb3cd2..101de9583 100644 --- a/navigation/DetailViewScreensStack.tsx +++ b/navigation/DetailViewScreensStack.tsx @@ -104,7 +104,7 @@ const DetailViewStackScreensStack = () => { title: '', headerBackTitle: loc.wallets.list_title, navigationBarColor: theme.colors.navigationBarColor, - headerShown: true, + headerShown: !isDesktop, headerStyle: { backgroundColor: theme.colors.customHeader, }, From 51738e462850b67da9c0e229784e3eda80305f77 Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Mon, 13 May 2024 19:01:57 -0400 Subject: [PATCH 05/64] OPS: Revert --- index.tsx => index.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename index.tsx => index.js (100%) diff --git a/index.tsx b/index.js similarity index 100% rename from index.tsx rename to index.js From c337d8dbbc557c75fdecf0b02c2e65629f785beb Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Tue, 14 May 2024 13:17:03 -0400 Subject: [PATCH 06/64] ADD: CompanionDelegates --- App.tsx | 5 +-- components/CompanionDelegates.tsx | 56 ++++++++++++++------------- navigation/DetailViewScreensStack.tsx | 7 +--- scripts/find-unused-loc.js | 4 +- 4 files changed, 35 insertions(+), 37 deletions(-) diff --git a/App.tsx b/App.tsx index 6b5e654c5..905e0a42f 100644 --- a/App.tsx +++ b/App.tsx @@ -37,12 +37,11 @@ const App = () => { - - {/* {walletsInitialized && ( + {walletsInitialized && ( - )} */} + )} diff --git a/components/CompanionDelegates.tsx b/components/CompanionDelegates.tsx index 090a6a5a2..5aa507091 100644 --- a/components/CompanionDelegates.tsx +++ b/components/CompanionDelegates.tsx @@ -1,8 +1,8 @@ import 'react-native-gesture-handler'; // should be on top import React, { useEffect, useRef } from 'react'; -import { AppState, NativeModules, NativeEventEmitter, Linking, Platform, UIManager, LogBox } from 'react-native'; +import { AppState, NativeModules, NativeEventEmitter, Linking, Platform, UIManager, LogBox, AppStateStatus } from 'react-native'; import { CommonActions } from '@react-navigation/native'; -import * as NavigationService from '../NavigationService'; +import { navigationRef } from '../NavigationService'; import { Chain } from '../models/bitcoinUnits'; import DeeplinkSchemaMatch from '../class/deeplink-schema-match'; import loc from '../loc'; @@ -18,6 +18,7 @@ import A from '../blue_modules/analytics'; import HandOffComponentListener from '../components/HandOffComponentListener'; import DeviceQuickActions from '../components/DeviceQuickActions'; import { useStorage } from '../blue_modules/storage-context'; +import { LightningCustodianWallet } from '../class'; const eventEmitter = Platform.OS === 'ios' ? new NativeEventEmitter(NativeModules.EventEmitter) : undefined; @@ -36,17 +37,20 @@ if (Platform.OS === 'android') { const CompanionDelegates = () => { const { wallets, addWallet, saveToDisk, fetchAndSaveWalletTransactions, refreshAllWalletTransactions, setSharedCosigner } = useStorage(); - const appState = useRef(AppState.currentState); - const clipboardContent = useRef(); + const appState = useRef(AppState.currentState); + const clipboardContent = useRef(); - const onNotificationReceived = async notification => { + const onNotificationReceived = 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: Notfication type is not defined; payload.foreground = true; + // @ts-ignore: Notfication type is not defined 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 + // @ts-ignore: Notfication type is not defined if (payload.foreground) await processPushNotifications(); }; @@ -65,18 +69,16 @@ const CompanionDelegates = () => { }; useEffect(() => { - if (walletsInitialized) { - const subscriptions = addListeners(); + const subscriptions = addListeners(); - // Cleanup function - return () => { - subscriptions.urlSubscription?.remove(); - subscriptions.appStateSubscription?.remove(); - subscriptions.notificationSubscription?.remove(); - }; - } + // Cleanup function + return () => { + subscriptions.urlSubscription?.remove(); + subscriptions.appStateSubscription?.remove(); + subscriptions.notificationSubscription?.remove(); + }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [walletsInitialized]); // Re-run when walletsInitialized changes + }, []); // Re-run when walletsInitialized changes /** * Processes push notifications stored in AsyncStorage. Might navigate to some screen. @@ -85,17 +87,19 @@ const CompanionDelegates = () => { * @private */ const processPushNotifications = async () => { - 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 + // @ts-ignore: Notfication type is not defined + const notifications2process = await Notifications.getStoredNotifications(); + // @ts-ignore: Notfication type is not defined await Notifications.clearStoredNotifications(); + // @ts-ignore: Notfication type is not defined Notifications.setApplicationIconBadgeNumber(0); + // @ts-ignore: Notfication type is not defined const deliveredNotifications = await Notifications.getDeliveredNotifications(); + // @ts-ignore: Notfication type is not defined setTimeout(() => Notifications.removeAllDeliveredNotifications(), 5000); // so notification bubble wont disappear too fast for (const payload of notifications2process) { @@ -119,7 +123,7 @@ const CompanionDelegates = () => { fetchAndSaveWalletTransactions(walletID); if (wasTapped) { if (payload.type !== 3 || wallet.chain === Chain.OFFCHAIN) { - NavigationService.dispatch( + navigationRef.dispatch( CommonActions.navigate({ name: 'WalletTransactions', params: { @@ -129,7 +133,7 @@ const CompanionDelegates = () => { }), ); } else { - NavigationService.navigate('ReceiveDetailsRoot', { + navigationRef.navigate('ReceiveDetailsRoot', { screen: 'ReceiveDetails', params: { walletID, @@ -155,7 +159,7 @@ const CompanionDelegates = () => { return false; }; - const handleAppStateChange = async nextAppState => { + const handleAppStateChange = 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); @@ -168,7 +172,7 @@ const CompanionDelegates = () => { // 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); + return (wallet as LightningCustodianWallet).isInvoiceGeneratedByWallet(clipboard) || wallet.weOwnAddress(clipboard); } }); const isBitcoinAddress = DeeplinkSchemaMatch.isBitcoinAddress(clipboard); @@ -197,8 +201,8 @@ const CompanionDelegates = () => { } }; - const handleOpenURL = event => { - DeeplinkSchemaMatch.navigationRouteFor(event, value => NavigationService.navigate(...value), { + const handleOpenURL = (event: { url: string }) => { + DeeplinkSchemaMatch.navigationRouteFor(event, value => navigationRef.navigate(...value), { wallets, addWallet, saveToDisk, @@ -206,7 +210,7 @@ const CompanionDelegates = () => { }); }; - const showClipboardAlert = ({ contentType }) => { + const showClipboardAlert = ({ contentType }: { contentType: undefined | string }) => { triggerHapticFeedback(HapticFeedbackTypes.ImpactLight); BlueClipboard() .getClipboardContent() diff --git a/navigation/DetailViewScreensStack.tsx b/navigation/DetailViewScreensStack.tsx index 51308c0c2..62fefd869 100644 --- a/navigation/DetailViewScreensStack.tsx +++ b/navigation/DetailViewScreensStack.tsx @@ -70,12 +70,7 @@ import { import { Icon } from 'react-native-elements'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { I18nManager, Platform, TouchableOpacity } from 'react-native'; -import { - NavigationDefaultOptions, - NavigationDefaultOptionsForDesktop, - NavigationFormModalOptions, - StatusBarLightOptions, -} from './'; +import { NavigationDefaultOptions, NavigationDefaultOptionsForDesktop, NavigationFormModalOptions, StatusBarLightOptions } from './'; const DetailViewRoot = createNativeStackNavigator(); const DetailViewStackScreensStack = () => { diff --git a/scripts/find-unused-loc.js b/scripts/find-unused-loc.js index 3f35e37ed..1ad5fa54d 100644 --- a/scripts/find-unused-loc.js +++ b/scripts/find-unused-loc.js @@ -3,7 +3,7 @@ const path = require('path'); const mainLocFile = './loc/en.json'; const dirsToInterate = ['components', 'screen', 'blue_modules', 'class', 'hooks', 'helpers', 'navigation']; -const addFiles = ['BlueComponents.js', 'App.js', 'Navigation.tsx']; +const addFiles = ['BlueComponents.js', 'App.tsx', 'navigation/index.tsx']; const allowedLocPrefixes = ['loc.lnurl_auth', 'loc.units']; const allLocKeysHashmap = {}; // loc key -> used or not @@ -32,7 +32,7 @@ for (const dir of dirsToInterate) { for (const filename of addFiles) { allDirFiles.push(path.resolve(filename)); } -allDirFiles.push(path.resolve('App.js')); +allDirFiles.push(path.resolve('App.tsx')); // got all source files From cf9ea0edca318c29c3d7660897a85a2db27e61e8 Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Tue, 14 May 2024 13:36:31 -0400 Subject: [PATCH 07/64] Update CompanionDelegates.tsx --- components/CompanionDelegates.tsx | 285 +++++++++++++++--------------- 1 file changed, 141 insertions(+), 144 deletions(-) diff --git a/components/CompanionDelegates.tsx b/components/CompanionDelegates.tsx index 5aa507091..98525be69 100644 --- a/components/CompanionDelegates.tsx +++ b/components/CompanionDelegates.tsx @@ -1,5 +1,5 @@ import 'react-native-gesture-handler'; // should be on top -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useRef, useCallback, lazy, Suspense } from 'react'; import { AppState, NativeModules, NativeEventEmitter, Linking, Platform, UIManager, LogBox, AppStateStatus } from 'react-native'; import { CommonActions } from '@react-navigation/native'; import { navigationRef } from '../NavigationService'; @@ -7,19 +7,21 @@ import { Chain } from '../models/bitcoinUnits'; import DeeplinkSchemaMatch from '../class/deeplink-schema-match'; import loc from '../loc'; import BlueClipboard from '../blue_modules/clipboard'; -import WatchConnectivity from '../WatchConnectivity'; -import Notifications from '../blue_modules/notifications'; -import WidgetCommunication from '../components/WidgetCommunication'; -import ActionSheet from '../screen/ActionSheet'; import triggerHapticFeedback, { HapticFeedbackTypes } from '../blue_modules/hapticFeedback'; -import MenuElements from '../components/MenuElements'; import { updateExchangeRate } from '../blue_modules/currency'; import A from '../blue_modules/analytics'; -import HandOffComponentListener from '../components/HandOffComponentListener'; -import DeviceQuickActions from '../components/DeviceQuickActions'; import { useStorage } from '../blue_modules/storage-context'; import { LightningCustodianWallet } from '../class'; +import ActionSheet from '../screen/ActionSheet'; +import Notifications from '../blue_modules/notifications'; +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; LogBox.ignoreLogs(['Require cycle:', 'Battery state `unknown` and monitoring disabled, this is normal for simulators and tvOS.']); @@ -40,67 +42,18 @@ const CompanionDelegates = () => { const appState = useRef(AppState.currentState); const clipboardContent = useRef(); - const onNotificationReceived = 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: Notfication type is not defined; - payload.foreground = true; - - // @ts-ignore: Notfication type is not defined - 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 - // @ts-ignore: Notfication type is not defined - if (payload.foreground) await processPushNotifications(); - }; - - const addListeners = () => { - const urlSubscription = Linking.addEventListener('url', handleOpenURL); - const appStateSubscription = AppState.addEventListener('change', handleAppStateChange); - - const notificationSubscription = eventEmitter?.addListener('onNotificationReceived', onNotificationReceived); - - // Store subscriptions in a ref or state to remove them later - return { - urlSubscription, - appStateSubscription, - notificationSubscription, - }; - }; - - useEffect(() => { - const subscriptions = addListeners(); - - // Cleanup function - return () => { - subscriptions.urlSubscription?.remove(); - subscriptions.appStateSubscription?.remove(); - subscriptions.notificationSubscription?.remove(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); // Re-run when walletsInitialized changes - - /** - * Processes push notifications stored in AsyncStorage. Might navigate to some screen. - * - * @returns {Promise} returns TRUE if notification was processed _and acted_ upon, i.e. navigation happened - * @private - */ - const processPushNotifications = async () => { + const processPushNotifications = useCallback(async () => { await new Promise(resolve => setTimeout(resolve, 200)); - // sleep needed as sometimes unsuspend is faster than notification module actually saves notifications to async storage - // @ts-ignore: Notfication type is not defined - + // @ts-ignore: Notifications type is not defined const notifications2process = await Notifications.getStoredNotifications(); - - // @ts-ignore: Notfication type is not defined + // @ts-ignore: Notifications type is not defined await Notifications.clearStoredNotifications(); - // @ts-ignore: Notfication type is not defined + // @ts-ignore: Notifications type is not defined Notifications.setApplicationIconBadgeNumber(0); - // @ts-ignore: Notfication type is not defined + // @ts-ignore: Notifications type is not defined const deliveredNotifications = await Notifications.getDeliveredNotifications(); - // @ts-ignore: Notfication type is not defined - setTimeout(() => Notifications.removeAllDeliveredNotifications(), 5000); // so notification bubble wont disappear too fast + // @ts-ignore: Notifications type is not defined + setTimeout(() => Notifications.removeAllDeliveredNotifications(), 5000); for (const payload of notifications2process) { const wasTapped = payload.foreground === false || (payload.foreground === true && payload.userInteraction); @@ -147,102 +100,146 @@ const CompanionDelegates = () => { } else { console.log('could not find wallet while processing push notification, NOP'); } - } // end foreach notifications loop + } 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. refreshAllWalletTransactions(); } - // if we are here - we did not act upon any push return false; - }; + }, [fetchAndSaveWalletTransactions, refreshAllWalletTransactions, wallets]); - const handleAppStateChange = 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; - 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 as LightningCustodianWallet).isInvoiceGeneratedByWallet(clipboard) || wallet.weOwnAddress(clipboard); - } + const handleOpenURL = useCallback( + (event: { url: string }) => { + DeeplinkSchemaMatch.navigationRouteFor(event, value => navigationRef.navigate(...value), { + wallets, + addWallet, + saveToDisk, + setSharedCosigner, }); - 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; + }, + [addWallet, saveToDisk, setSharedCosigner, wallets], + ); + + const showClipboardAlert = useCallback( + ({ contentType }: { contentType: undefined | string }) => { + triggerHapticFeedback(HapticFeedbackTypes.ImpactLight); + BlueClipboard() + .getClipboardContent() + .then(clipboard => { + 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; + } + }, + ); + }); + }, + [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; + const clipboard = await BlueClipboard().getClipboardContent(); + 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 }); } - showClipboardAlert({ contentType }); + clipboardContent.current = clipboard; } - clipboardContent.current = clipboard; - } - if (nextAppState) { - appState.current = nextAppState; - } - }; + if (nextAppState) { + appState.current = nextAppState; + } + }, + [processPushNotifications, showClipboardAlert, wallets], + ); - const handleOpenURL = (event: { url: string }) => { - DeeplinkSchemaMatch.navigationRouteFor(event, value => navigationRef.navigate(...value), { - wallets, - addWallet, - saveToDisk, - setSharedCosigner, - }); - }; + 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; - const showClipboardAlert = ({ contentType }: { contentType: undefined | string }) => { - triggerHapticFeedback(HapticFeedbackTypes.ImpactLight); - BlueClipboard() - .getClipboardContent() - .then(clipboard => { - 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: // Cancel - break; - case 1: - handleOpenURL({ url: clipboard }); - break; - } - }, - ); - }); - }; + // @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]); + + useEffect(() => { + const subscriptions = addListeners(); + + return () => { + subscriptions.urlSubscription?.remove(); + subscriptions.appStateSubscription?.remove(); + subscriptions.notificationSubscription?.remove(); + }; + }, [addListeners]); return ( <> - - - - - + + + + + + + ); }; From 37a8704fecce3b8f7d26c483493a3dfed4baa03e Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Tue, 14 May 2024 17:03:47 -0400 Subject: [PATCH 08/64] REF: App entry --- App.tsx | 32 ++++++++++-------------------- MasterView.tsx | 33 +++++++++++++++++++++++++++++++ components/CompanionDelegates.tsx | 4 +--- index.js | 21 ++++++++++---------- 4 files changed, 54 insertions(+), 36 deletions(-) create mode 100644 MasterView.tsx diff --git a/App.tsx b/App.tsx index 905e0a42f..0772aed08 100644 --- a/App.tsx +++ b/App.tsx @@ -1,27 +1,17 @@ import 'react-native-gesture-handler'; // should be on top -import React, { Suspense, lazy, useEffect } from 'react'; -import { NativeModules, Platform, UIManager, useColorScheme, LogBox } from 'react-native'; +import React, { useEffect } from 'react'; +import { NativeModules, Platform, useColorScheme } from 'react-native'; import { NavigationContainer } from '@react-navigation/native'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { navigationRef } from './NavigationService'; import { BlueDefaultTheme, BlueDarkTheme } from './components/themes'; import { NavigationProvider } from './components/NavigationProvider'; -import MainRoot from './navigation'; -import { useStorage } from './blue_modules/storage-context'; -import Biometric from './class/biometrics'; -const CompanionDelegates = lazy(() => import('./components/CompanionDelegates')); +import { BlueStorageProvider } from './blue_modules/storage-context'; +import MasterView from './MasterView'; +import { SettingsProvider } from './components/Context/SettingsContext'; const { SplashScreen } = NativeModules; -LogBox.ignoreLogs(['Require cycle:', 'Battery state `unknown` and monitoring disabled, this is normal for simulators and tvOS.']); - -if (Platform.OS === 'android') { - if (UIManager.setLayoutAnimationEnabledExperimental) { - UIManager.setLayoutAnimationEnabledExperimental(true); - } -} - const App = () => { - const { walletsInitialized } = useStorage(); const colorScheme = useColorScheme(); useEffect(() => { @@ -35,13 +25,11 @@ const App = () => { - - - {walletsInitialized && ( - - - - )} + + + + + diff --git a/MasterView.tsx b/MasterView.tsx new file mode 100644 index 000000000..f12468d37 --- /dev/null +++ b/MasterView.tsx @@ -0,0 +1,33 @@ +import 'react-native-gesture-handler'; // should be on top +import React, { Suspense, lazy, useEffect } from 'react'; +import { NativeModules, Platform } from 'react-native'; +import MainRoot from './navigation'; +import { useStorage } from './blue_modules/storage-context'; +import Biometric from './class/biometrics'; +const CompanionDelegates = lazy(() => import('./components/CompanionDelegates')); +const { SplashScreen } = NativeModules; + +const MasterView = () => { + const { walletsInitialized } = useStorage(); + + useEffect(() => { + if (Platform.OS === 'ios') { + // Call hide to setup the listener on the native side + SplashScreen?.addObserver(); + } + }, []); + + return ( + <> + + + {walletsInitialized && ( + + + + )} + + ); +}; + +export default MasterView; diff --git a/components/CompanionDelegates.tsx b/components/CompanionDelegates.tsx index 98525be69..c58f7057e 100644 --- a/components/CompanionDelegates.tsx +++ b/components/CompanionDelegates.tsx @@ -1,6 +1,6 @@ import 'react-native-gesture-handler'; // should be on top import React, { useEffect, useRef, useCallback, lazy, Suspense } from 'react'; -import { AppState, NativeModules, NativeEventEmitter, Linking, Platform, UIManager, LogBox, AppStateStatus } from 'react-native'; +import { AppState, NativeModules, NativeEventEmitter, Linking, Platform, UIManager, AppStateStatus } from 'react-native'; import { CommonActions } from '@react-navigation/native'; import { navigationRef } from '../NavigationService'; import { Chain } from '../models/bitcoinUnits'; @@ -24,8 +24,6 @@ const WatchConnectivity = lazy(() => import('../WatchConnectivity')); // @ts-ignore: NativeModules.EventEmitter is not typed const eventEmitter = Platform.OS === 'ios' ? new NativeEventEmitter(NativeModules.EventEmitter) : undefined; -LogBox.ignoreLogs(['Require cycle:', 'Battery state `unknown` and monitoring disabled, this is normal for simulators and tvOS.']); - const ClipboardContentType = Object.freeze({ BITCOIN: 'BITCOIN', LIGHTNING: 'LIGHTNING', diff --git a/index.js b/index.js index 0783e0c0c..0affb0f4c 100644 --- a/index.js +++ b/index.js @@ -1,12 +1,9 @@ import './shim.js'; import React, { useEffect } from 'react'; -import { AppRegistry } from 'react-native'; - +import { AppRegistry, LogBox, Platform, UIManager } from 'react-native'; import App from './App'; -import { BlueStorageProvider } from './blue_modules/storage-context'; import A from './blue_modules/analytics'; -import { SettingsProvider } from './components/Context/SettingsContext'; import { restoreSavedPreferredFiatCurrencyAndExchangeFromStorage } from './blue_modules/currency'; if (!Error.captureStackTrace) { @@ -14,19 +11,21 @@ if (!Error.captureStackTrace) { Error.captureStackTrace = () => {}; } +LogBox.ignoreLogs(['Require cycle:', 'Battery state `unknown` and monitoring disabled, this is normal for simulators and tvOS.']); + +if (Platform.OS === 'android') { + if (UIManager.setLayoutAnimationEnabledExperimental) { + UIManager.setLayoutAnimationEnabledExperimental(true); + } +} + const BlueAppComponent = () => { useEffect(() => { restoreSavedPreferredFiatCurrencyAndExchangeFromStorage(); A(A.ENUM.INIT); }, []); - return ( - - - - - - ); + return ; }; AppRegistry.registerComponent('BlueWallet', () => BlueAppComponent); From f7b3056360f0912e4d590b4949cc399f781f6143 Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Tue, 14 May 2024 19:02:08 -0400 Subject: [PATCH 09/64] FIX: Use existing file --- blue_modules/environment.ts | 5 +++-- components/Context/LargeScreenProvider.tsx | 8 +++----- screen/send/Broadcast.tsx | 5 +++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/blue_modules/environment.ts b/blue_modules/environment.ts index c6e5db789..1b6f6fd32 100644 --- a/blue_modules/environment.ts +++ b/blue_modules/environment.ts @@ -1,6 +1,7 @@ import { isTablet, getDeviceType } from 'react-native-device-info'; const isDesktop: boolean = getDeviceType() === 'Desktop'; +const isHandset: boolean = getDeviceType() === 'Handset'; +const isTabletDevice: boolean = isTablet(); -export const isHandset: boolean = getDeviceType() === 'Handset'; -export { isDesktop, isTablet }; +export { isDesktop, isTabletDevice, isHandset }; diff --git a/components/Context/LargeScreenProvider.tsx b/components/Context/LargeScreenProvider.tsx index 5bab2d89d..a831f4e09 100644 --- a/components/Context/LargeScreenProvider.tsx +++ b/components/Context/LargeScreenProvider.tsx @@ -1,7 +1,6 @@ import React, { createContext, useState, useEffect, useMemo, ReactNode } from 'react'; import { Dimensions } from 'react-native'; -import { isTablet } from 'react-native-device-info'; -import { isDesktop } from '../../blue_modules/environment'; +import { isDesktop, isTabletDevice } from '../../blue_modules/environment'; interface ILargeScreenContext { isLargeScreen: boolean; @@ -30,11 +29,10 @@ export const LargeScreenProvider: React.FC = ({ childr }, [windowWidth]); const isLargeScreen: boolean = useMemo(() => { - const isRunningOnTablet = isTablet(); const halfScreenWidth = windowWidth >= screenWidth / 2; - const condition = (isRunningOnTablet && halfScreenWidth) || isDesktop; + const condition = (isTabletDevice && halfScreenWidth) || isDesktop; console.debug( - `LargeScreenProvider.isLargeScreen: width: ${windowWidth}, Screen width: ${screenWidth}, Is tablet: ${isTablet()}, Is large screen: ${condition}, isDesktkop: ${isDesktop}`, + `LargeScreenProvider.isLargeScreen: width: ${windowWidth}, Screen width: ${screenWidth}, Is tablet: ${isTabletDevice}, Is large screen: ${condition}, isDesktkop: ${isDesktop}`, ); return condition; }, [windowWidth, screenWidth]); diff --git a/screen/send/Broadcast.tsx b/screen/send/Broadcast.tsx index 33d5358f0..774b6cc1c 100644 --- a/screen/send/Broadcast.tsx +++ b/screen/send/Broadcast.tsx @@ -21,7 +21,7 @@ import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/h import SafeArea from '../../components/SafeArea'; import presentAlert from '../../components/Alert'; import { scanQrHelper } from '../../helpers/scan-qr'; -import { isTablet } from 'react-native-device-info'; +import { isTabletDevice } from '../../blue_modules/environment'; const BROADCAST_RESULT = Object.freeze({ none: 'Input transaction hex', @@ -34,6 +34,7 @@ interface SuccessScreenProps { tx: string; } + const Broadcast: React.FC = () => { const { name } = useRoute(); const { navigate } = useNavigation(); @@ -117,7 +118,7 @@ const Broadcast: React.FC = () => { return ( - + {BROADCAST_RESULT.success !== broadcastResult && ( From 7df5c3721e5992dae75ee4d9524eb2dd679c17fb Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Tue, 14 May 2024 19:50:13 -0400 Subject: [PATCH 10/64] FIX: CI --- blue_modules/environment.ts | 3 +-- components/Context/LargeScreenProvider.tsx | 5 ++++- screen/send/Broadcast.tsx | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/blue_modules/environment.ts b/blue_modules/environment.ts index 1b6f6fd32..febd4c83f 100644 --- a/blue_modules/environment.ts +++ b/blue_modules/environment.ts @@ -2,6 +2,5 @@ import { isTablet, getDeviceType } from 'react-native-device-info'; const isDesktop: boolean = getDeviceType() === 'Desktop'; const isHandset: boolean = getDeviceType() === 'Handset'; -const isTabletDevice: boolean = isTablet(); -export { isDesktop, isTabletDevice, isHandset }; +export { isDesktop, isTablet, isHandset }; diff --git a/components/Context/LargeScreenProvider.tsx b/components/Context/LargeScreenProvider.tsx index a831f4e09..838b3af88 100644 --- a/components/Context/LargeScreenProvider.tsx +++ b/components/Context/LargeScreenProvider.tsx @@ -1,11 +1,14 @@ import React, { createContext, useState, useEffect, useMemo, ReactNode } from 'react'; import { Dimensions } from 'react-native'; -import { isDesktop, isTabletDevice } from '../../blue_modules/environment'; +import { isDesktop, isTablet } from '../../blue_modules/environment'; interface ILargeScreenContext { isLargeScreen: boolean; } + +const isTabletDevice: boolean = isTablet() + export const LargeScreenContext = createContext(undefined); interface LargeScreenProviderProps { diff --git a/screen/send/Broadcast.tsx b/screen/send/Broadcast.tsx index 774b6cc1c..d6e23f928 100644 --- a/screen/send/Broadcast.tsx +++ b/screen/send/Broadcast.tsx @@ -21,8 +21,9 @@ import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/h import SafeArea from '../../components/SafeArea'; import presentAlert from '../../components/Alert'; import { scanQrHelper } from '../../helpers/scan-qr'; -import { isTabletDevice } from '../../blue_modules/environment'; +import { isTablet } from '../../blue_modules/environment'; +const isTabletDevice = isTablet(); const BROADCAST_RESULT = Object.freeze({ none: 'Input transaction hex', pending: 'pending', @@ -34,7 +35,6 @@ interface SuccessScreenProps { tx: string; } - const Broadcast: React.FC = () => { const { name } = useRoute(); const { navigate } = useNavigation(); From fc73266b2fd14a1807a6341a3a2f8d0e8b59b11c Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Wed, 15 May 2024 07:43:13 -0400 Subject: [PATCH 11/64] Update App.tsx --- App.tsx | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/App.tsx b/App.tsx index 0772aed08..0fa875f95 100644 --- a/App.tsx +++ b/App.tsx @@ -9,6 +9,7 @@ import { NavigationProvider } from './components/NavigationProvider'; import { BlueStorageProvider } from './blue_modules/storage-context'; import MasterView from './MasterView'; import { SettingsProvider } from './components/Context/SettingsContext'; +import { LargeScreenProvider } from './components/Context/LargeScreenProvider'; const { SplashScreen } = NativeModules; const App = () => { @@ -22,17 +23,19 @@ const App = () => { }, []); return ( - - - - - - - - - - - + + + + + + + + + + + + + ); }; From 7b3b48e3b5b74f6232de4a8dec20d97ae6d77f69 Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Wed, 15 May 2024 08:05:42 -0400 Subject: [PATCH 12/64] REF: isTablet --- blue_modules/environment.ts | 3 ++- components/Context/LargeScreenProvider.tsx | 7 ++----- screen/send/Broadcast.tsx | 3 +-- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/blue_modules/environment.ts b/blue_modules/environment.ts index febd4c83f..987f92c9b 100644 --- a/blue_modules/environment.ts +++ b/blue_modules/environment.ts @@ -1,5 +1,6 @@ -import { isTablet, getDeviceType } from 'react-native-device-info'; +import { isTablet as checkIsTablet, getDeviceType } from 'react-native-device-info'; +const isTablet: boolean = checkIsTablet(); const isDesktop: boolean = getDeviceType() === 'Desktop'; const isHandset: boolean = getDeviceType() === 'Handset'; diff --git a/components/Context/LargeScreenProvider.tsx b/components/Context/LargeScreenProvider.tsx index 838b3af88..f39fef626 100644 --- a/components/Context/LargeScreenProvider.tsx +++ b/components/Context/LargeScreenProvider.tsx @@ -6,9 +6,6 @@ interface ILargeScreenContext { isLargeScreen: boolean; } - -const isTabletDevice: boolean = isTablet() - export const LargeScreenContext = createContext(undefined); interface LargeScreenProviderProps { @@ -33,9 +30,9 @@ export const LargeScreenProvider: React.FC = ({ childr const isLargeScreen: boolean = useMemo(() => { const halfScreenWidth = windowWidth >= screenWidth / 2; - const condition = (isTabletDevice && halfScreenWidth) || isDesktop; + const condition = (isTablet && halfScreenWidth) || isDesktop; console.debug( - `LargeScreenProvider.isLargeScreen: width: ${windowWidth}, Screen width: ${screenWidth}, Is tablet: ${isTabletDevice}, Is large screen: ${condition}, isDesktkop: ${isDesktop}`, + `LargeScreenProvider.isLargeScreen: width: ${windowWidth}, Screen width: ${screenWidth}, Is tablet: ${isTablet}, Is large screen: ${condition}, isDesktkop: ${isDesktop}`, ); return condition; }, [windowWidth, screenWidth]); diff --git a/screen/send/Broadcast.tsx b/screen/send/Broadcast.tsx index d6e23f928..83be8c227 100644 --- a/screen/send/Broadcast.tsx +++ b/screen/send/Broadcast.tsx @@ -23,7 +23,6 @@ import presentAlert from '../../components/Alert'; import { scanQrHelper } from '../../helpers/scan-qr'; import { isTablet } from '../../blue_modules/environment'; -const isTabletDevice = isTablet(); const BROADCAST_RESULT = Object.freeze({ none: 'Input transaction hex', pending: 'pending', @@ -118,7 +117,7 @@ const Broadcast: React.FC = () => { return ( - + {BROADCAST_RESULT.success !== broadcastResult && ( From bb575ee633d586796fde7388983ecefa6a07d538 Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Wed, 15 May 2024 10:08:22 -0400 Subject: [PATCH 13/64] Update setup.js --- tests/setup.js | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/setup.js b/tests/setup.js index b2803823f..fc9b4c650 100644 --- a/tests/setup.js +++ b/tests/setup.js @@ -59,6 +59,7 @@ jest.mock('react-native-device-info', () => { getDeviceType: jest.fn().mockReturnValue(false), hasGmsSync: jest.fn().mockReturnValue(true), hasHmsSync: jest.fn().mockReturnValue(false), + isTablet: jest.fn().mockReturnValue(false), }; }); From 1589dfa1956a527c22e9448b18840b86fd82f969 Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Wed, 15 May 2024 13:35:42 -0400 Subject: [PATCH 14/64] DEL: Artificial Splash delay --- android/app/src/main/AndroidManifest.xml | 13 +++------- .../bluewallet/bluewallet/SplashActivity.java | 26 ------------------- .../app/src/main/res/layout/splash_screen.xml | 13 ---------- blue_modules/storage-context.tsx | 15 ++++++----- 4 files changed, 12 insertions(+), 55 deletions(-) delete mode 100644 android/app/src/main/java/io/bluewallet/bluewallet/SplashActivity.java delete mode 100644 android/app/src/main/res/layout/splash_screen.xml diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 385824b24..04a9554ff 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -64,16 +64,7 @@ - - - - - - - - + + + diff --git a/android/app/src/main/java/io/bluewallet/bluewallet/SplashActivity.java b/android/app/src/main/java/io/bluewallet/bluewallet/SplashActivity.java deleted file mode 100644 index 161055463..000000000 --- a/android/app/src/main/java/io/bluewallet/bluewallet/SplashActivity.java +++ /dev/null @@ -1,26 +0,0 @@ -package io.bluewallet.bluewallet; // Replace with your package name - -import android.content.Intent; -import android.os.Bundle; -import android.os.Handler; -import androidx.appcompat.app.AppCompatActivity; - -public class SplashActivity extends AppCompatActivity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.splash_screen); // Replace with your layout name - - int SPLASH_DISPLAY_LENGTH = 1000; // Splash screen duration in milliseconds - - new Handler().postDelayed(new Runnable() { - @Override - public void run() { - Intent mainIntent = new Intent(SplashActivity.this, MainActivity.class); - SplashActivity.this.startActivity(mainIntent); - SplashActivity.this.finish(); - } - }, SPLASH_DISPLAY_LENGTH); - } -} diff --git a/android/app/src/main/res/layout/splash_screen.xml b/android/app/src/main/res/layout/splash_screen.xml deleted file mode 100644 index c90f097b0..000000000 --- a/android/app/src/main/res/layout/splash_screen.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - diff --git a/blue_modules/storage-context.tsx b/blue_modules/storage-context.tsx index 6f79b3183..3bed3789b 100644 --- a/blue_modules/storage-context.tsx +++ b/blue_modules/storage-context.tsx @@ -73,12 +73,15 @@ export const BlueStorageProvider = ({ children }: { children: React.ReactNode }) const [reloadTransactionsMenuActionFunction, setReloadTransactionsMenuActionFunction] = useState<() => void>(() => {}); useEffect(() => { - setWallets(BlueApp.getWallets()); - - BlueElectrum.isDisabled().then(setIsElectrumDisabled); - if (walletsInitialized) { - BlueElectrum.connectMain(); - } + (async () => { + const isElectrumDisabledValue = await BlueElectrum.isDisabled(); + setIsElectrumDisabled(isElectrumDisabledValue); + if (walletsInitialized && wallets.length > 0 && !isElectrumDisabledValue) { + setWallets(BlueApp.getWallets()); + BlueElectrum.connectMain(); + } + })(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [walletsInitialized]); const saveToDisk = async (force: boolean = false) => { From 667dba4836466907e13888d3a158ff3fbf74872d Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Wed, 15 May 2024 15:53:08 -0400 Subject: [PATCH 15/64] Update AndroidManifest.xml --- android/app/src/main/AndroidManifest.xml | 151 ++++++++++++----------- 1 file changed, 77 insertions(+), 74 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 04a9554ff..7376d7788 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -7,50 +7,52 @@ android:required="false" /> - - - + + + - + + android:name=".MainApplication" + android:label="@string/app_name" + android:icon="@mipmap/ic_launcher" + android:roundIcon="@mipmap/ic_launcher_round" + android:allowBackup="false" + android:largeHeap="true" + android:extractNativeLibs="true" + android:usesCleartextTraffic="true" + android:supportsRtl="true" + android:theme="@style/AppTheme" + android:networkSecurityConfig="@xml/network_security_config"> - - - - - - - - - + + + + + - @@ -58,64 +60,65 @@ + android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationListenerService" + android:exported="false"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - + From 138a46cb371d5908115c54c68743fe2bce59d719 Mon Sep 17 00:00:00 2001 From: overtorment Date: Wed, 15 May 2024 22:46:54 +0100 Subject: [PATCH 16/64] ADD: BIP47 notification transaction --- Navigation.tsx | 6 +- blue_modules/BlueElectrum.ts | 2 +- class/contact-list.ts | 23 ++ class/wallets/abstract-hd-electrum-wallet.ts | 19 +- class/wallets/types.ts | 2 +- loc/en.json | 3 +- screen/wallets/paymentCodesList.tsx | 262 +++++++++++++++++-- tests/integration/bip47.test.ts | 58 +++- tests/unit/contact-list.test.ts | 19 ++ 9 files changed, 351 insertions(+), 43 deletions(-) create mode 100644 class/contact-list.ts create mode 100644 tests/unit/contact-list.test.ts diff --git a/Navigation.tsx b/Navigation.tsx index 7f10794b5..c4721e0bb 100644 --- a/Navigation.tsx +++ b/Navigation.tsx @@ -479,11 +479,7 @@ const PaymentCodeStackRoot = () => { return ( - + ); }; diff --git a/blue_modules/BlueElectrum.ts b/blue_modules/BlueElectrum.ts index 65de31d05..2a2524c01 100644 --- a/blue_modules/BlueElectrum.ts +++ b/blue_modules/BlueElectrum.ts @@ -52,7 +52,7 @@ type ElectrumTransaction = { }; }[]; blockhash: string; - confirmations?: number; + confirmations: number; time: number; blocktime: number; }; diff --git a/class/contact-list.ts b/class/contact-list.ts new file mode 100644 index 000000000..d90360f63 --- /dev/null +++ b/class/contact-list.ts @@ -0,0 +1,23 @@ +import BIP47Factory from '@spsina/bip47'; +import { TWallet } from './wallets/types'; +import ecc from '../blue_modules/noble_ecc'; + +export class ContactList { + private _wallet: TWallet; + + constructor(wallet: TWallet) { + if (!wallet.allowBIP47()) throw new Error('BIP47 is not allowed for the wallet'); + if (!wallet.isBIP47Enabled()) throw new Error('BIP47 is not enabled'); + + this._wallet = wallet; + } + + isPaymentCodeValid(pc: string): boolean { + try { + BIP47Factory(ecc).fromPaymentCode(pc); + return true; + } catch (_) { + return false; + } + } +} diff --git a/class/wallets/abstract-hd-electrum-wallet.ts b/class/wallets/abstract-hd-electrum-wallet.ts index 48822b341..8b05c35cc 100644 --- a/class/wallets/abstract-hd-electrum-wallet.ts +++ b/class/wallets/abstract-hd-electrum-wallet.ts @@ -1542,22 +1542,21 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet { } /** - * this method goes over all our txs and checks if we sent a notification tx in the past to the given PC + * find and return _existing_ notification transaction for the given payment code + * (i.e. if it exists - we notified in the past and dont need to notify again) */ - needToNotifyBIP47(receiverPaymentCode: string): boolean { + getBIP47NotificationTransaction(receiverPaymentCode: string): Transaction | undefined { const publicBip47 = BIP47Factory(ecc).fromPaymentCode(receiverPaymentCode); const remoteNotificationAddress = publicBip47.getNotificationAddress(); for (const tx of this.getTransactions()) { for (const output of tx.outputs) { - if (output.scriptPubKey?.addresses?.includes(remoteNotificationAddress)) return false; + if (output.scriptPubKey?.addresses?.includes(remoteNotificationAddress)) return tx; // ^^^ if in the past we sent a tx to his notification address - most likely that was a proper notification // transaction with OP_RETURN. // but not gona verify it here, will just trust it } } - - return true; } /** @@ -1694,6 +1693,10 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet { return bip47Local.getNotificationAddress(); } + /** + * check our notification address, and decypher all payment codes people notified us + * about (so they can pay us) + */ async fetchBIP47SenderPaymentCodes(): Promise { const bip47_instance = this.getBIP47FromSeed(); const address = bip47_instance.getNotificationAddress(); @@ -1748,10 +1751,16 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet { } } + /** + * payment codes of people who can pay us + */ getBIP47SenderPaymentCodes(): string[] { return this._receive_payment_codes; } + /** + * payment codes of people whom we can pay + */ getBIP47ReceiverPaymentCodes(): string[] { return this._send_payment_codes; } diff --git a/class/wallets/types.ts b/class/wallets/types.ts index e693e553c..3a2fb9fe1 100644 --- a/class/wallets/types.ts +++ b/class/wallets/types.ts @@ -101,7 +101,7 @@ export type Transaction = { inputs: TransactionInput[]; outputs: TransactionOutput[]; blockhash: string; - confirmations?: number; + confirmations: number; time: number; blocktime: number; received?: number; diff --git a/loc/en.json b/loc/en.json index 92f51e6cc..ce7061523 100644 --- a/loc/en.json +++ b/loc/en.json @@ -615,9 +615,8 @@ }, "bip47": { "payment_code": "Payment Code", - "payment_codes_list": "Payment Codes List", + "contacts": "Contacts", "who_can_pay_me": "Who can pay me:", - "whom_can_i_pay": "Whom can I pay:", "purpose": "Reusable and shareable code (BIP47)", "not_found": "Payment code not found" } diff --git a/screen/wallets/paymentCodesList.tsx b/screen/wallets/paymentCodesList.tsx index 15ac0b86c..81d7f7557 100644 --- a/screen/wallets/paymentCodesList.tsx +++ b/screen/wallets/paymentCodesList.tsx @@ -1,23 +1,98 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack'; -import React, { useContext, useEffect, useState } from 'react'; +import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import { SectionList, StyleSheet, Text, View } from 'react-native'; import { PaymentCodeStackParamList } from '../../Navigation'; import { BlueStorageContext } from '../../blue_modules/storage-context'; -import loc from '../../loc'; -import CopyTextToClipboard from '../../components/CopyTextToClipboard'; +import loc, { formatBalance } from '../../loc'; import { AbstractHDElectrumWallet } from '../../class/wallets/abstract-hd-electrum-wallet'; +import ToolTipMenu from '../../components/TooltipMenu'; +import { useTheme } from '../../components/themes'; +import createHash from 'create-hash'; +import Button from '../../components/Button'; +import prompt from '../../helpers/prompt'; +import { ContactList } from '../../class/contact-list'; +import assert from 'assert'; +import { HDSegwitBech32Wallet } from '../../class'; +import Clipboard from '@react-native-clipboard/clipboard'; +import * as BlueElectrum from '../../blue_modules/BlueElectrum'; +import confirm from '../../helpers/confirm'; +import { BitcoinUnit } from '../../models/bitcoinUnits'; +import { satoshiToLocalCurrency } from '../../blue_modules/currency'; +import { BlueLoading } from '../../BlueComponents'; interface DataSection { title: string; data: string[]; } +const actionIcons = { + Eye: { + iconType: 'SYSTEM', + iconValue: 'eye', + }, + EyeSlash: { + iconType: 'SYSTEM', + iconValue: 'eye.slash', + }, + Clipboard: { + iconType: 'SYSTEM', + iconValue: 'doc.on.doc', + }, + Link: { + iconType: 'SYSTEM', + iconValue: 'link', + }, + Note: { + iconType: 'SYSTEM', + iconValue: 'note.text', + }, +}; + +interface IActionKey { + id: Actions; + text: string; + icon: any; +} + +enum Actions { + pay, + rename, + copyToClipboard, +} + +const actionKeys: IActionKey[] = [ + { + id: Actions.pay, + text: 'Pay this contact', + icon: actionIcons.Clipboard, + }, + { + id: Actions.rename, + text: 'Rename contact', + icon: actionIcons.Clipboard, + }, + { + id: Actions.copyToClipboard, + text: 'Copy PaymentCode', + icon: actionIcons.Clipboard, + }, +]; + type Props = NativeStackScreenProps; +function onlyUnique(value: any, index: number, self: any[]) { + return self.indexOf(value) === index; +} + export default function PaymentCodesList({ route }: Props) { const { walletID } = route.params; - const { wallets } = useContext(BlueStorageContext); + const { wallets, txMetadata, counterpartyMetadata, saveToDisk } = useContext(BlueStorageContext); + const [reload, setReload] = useState(0); const [data, setData] = useState([]); + const menuRef = useRef(); + const { colors } = useTheme(); + const [isLoading, setIsLoading] = useState(false); + const [loadingText, setLoadingText] = useState('Loading...'); useEffect(() => { if (!walletID) return; @@ -27,35 +102,166 @@ export default function PaymentCodesList({ route }: Props) { const newData: DataSection[] = [ { - title: loc.bip47.who_can_pay_me, - data: foundWallet.getBIP47SenderPaymentCodes(), - }, - { - title: loc.bip47.whom_can_i_pay, - data: foundWallet.getBIP47ReceiverPaymentCodes(), + title: '', + data: foundWallet.getBIP47SenderPaymentCodes().concat(foundWallet.getBIP47ReceiverPaymentCodes()).filter(onlyUnique), }, ]; setData(newData); - }, [walletID, wallets]); + }, [walletID, wallets, reload]); + + const toolTipActions = useMemo(() => { + return actionKeys; + }, []); + + const shortenContactName = (name: string): string => { + if (name.length < 20) return name; + return name.substr(0, 10) + '...' + name.substr(name.length - 10, 10); + }; + + const onToolTipPress = async (id: any, pc: string) => { + if (id === Actions.copyToClipboard) { + Clipboard.setString(pc); + alert('Copied'); + } + + if (id === Actions.rename) { + const newName = await prompt('Rename', 'Provide new name for this contact', false, 'plain-text'); + if (!newName) return; + + counterpartyMetadata[pc] = { label: newName }; + setReload(Math.random()); + } + }; + + const onPress = useCallback(async () => { + // @ts-ignore: idk how to fix + menuRef?.current?.dismissMenu?.(); + }, []); + + const renderItem = (pc: string) => { + const color = createHash('sha256').update(pc).digest().toString('hex').substring(0, 6); + + const displayName = shortenContactName(counterpartyMetadata?.[pc]?.label ?? pc); + + return ( + + onToolTipPress(item, pc)} + onPress={onPress} + isButton={true} + isMenuPrimaryAction={true} + > + + + + {displayName} + + + + + + ); + }; + + const onAddContactPress = async () => { + try { + const foundWallet = wallets.find(w => w.getID() === walletID) as unknown as HDSegwitBech32Wallet; + // foundWallet._send_payment_codes = []; // fixme debug + assert(foundWallet); + + const newPc = await prompt('Add Contact', 'Contact Payment Code', false, 'plain-text'); + if (!newPc) return; + const cl = new ContactList(foundWallet); + + if (!cl.isPaymentCodeValid(newPc)) { + alert('Invalid Payment Code'); + return; + } + + setIsLoading(true); + + const notificationTx = foundWallet.getBIP47NotificationTransaction(newPc); + + if (notificationTx && notificationTx.confirmations > 0) { + // we previously sent notification transaction to him, so just need to add him to internals + foundWallet.addBIP47Receiver(newPc); + await foundWallet.syncBip47ReceiversAddresses(newPc); // so we can unwrap and save all his possible addresses + // (for a case if already have txs with him, we will now be able to label them on tx list) + await saveToDisk(); + setReload(Math.random()); + return; + } + + if (notificationTx && notificationTx.confirmations === 0) { + // for a rare case when we just sent the confirmation tx and it havent confirmed yet + alert('Notification transaction is not confirmed yet, please wait'); + return; + } + + // need to send notif tx: + + setLoadingText('Fetching UTXO...'); + await foundWallet.fetchUtxo(); + setLoadingText('Fetching fees...'); + const fees = await BlueElectrum.estimateFees(); + setLoadingText('Fetching change address...'); + const changeAddress = await foundWallet.getChangeAddressAsync(); + setLoadingText('Crafting notification transaction...'); + const { tx, fee } = foundWallet.createBip47NotificationTransaction(foundWallet.getUtxo(), newPc, fees.fast, changeAddress); + + if (!tx) { + alert('Failed to create on-chain transaction'); + return; + } + + setLoadingText(''); + if ( + await confirm( + 'On-chain transaction needed', + `${loc.send.create_fee}: ${formatBalance(fee, BitcoinUnit.BTC)} (${satoshiToLocalCurrency(fee)}). `, + ) + ) { + setLoadingText('Broadcasting...'); + try { + await foundWallet.broadcastTx(tx.toHex()); + foundWallet.addBIP47Receiver(newPc); + alert('Notification transaction sent. Please wait for it to confirm'); + txMetadata[tx.getId()] = { memo: 'Notification transaction' }; + setReload(Math.random()); + await new Promise(resolve => setTimeout(resolve, 5000)); // tx propagate on backend so our fetch will actually get the new tx + } catch (_) {} + setLoadingText('Fetching transactions...'); + await foundWallet.fetchTransactions(); + } + } catch (error: any) { + alert(error.message); + } finally { + setIsLoading(false); + } + }; + + if (isLoading) { + return ( + + + {loadingText} + + ); + } return ( {!walletID ? ( Internal error ) : ( - - item + index} - renderItem={({ item }) => ( - - - - )} - renderSectionHeader={({ section: { title } }) => {title}} - /> + + item + index} renderItem={({ item }) => renderItem(item)} /> )} + +