diff --git a/App.js b/App.js deleted file mode 100644 index a87105306..000000000 --- a/App.js +++ /dev/null @@ -1,294 +0,0 @@ -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'; - -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.']); - -const ClipboardContentType = Object.freeze({ - BITCOIN: 'BITCOIN', - LIGHTNING: 'LIGHTNING', -}); - -if (Platform.OS === 'android') { - if (UIManager.setLayoutAnimationEnabledExperimental) { - UIManager.setLayoutAnimationEnabledExperimental(true); - } -} - -const App = () => { - const { - walletsInitialized, - wallets, - addWallet, - saveToDisk, - fetchAndSaveWalletTransactions, - refreshAllWalletTransactions, - setSharedCosigner, - } = useContext(BlueStorageContext); - const appState = useRef(AppState.currentState); - const clipboardContent = useRef(); - const colorScheme = useColorScheme(); - - const onNotificationReceived = async notification => { - const payload = Object.assign({}, notification, notification.data); - if (notification.data && notification.data.data) Object.assign(payload, notification.data.data); - 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(); - }; - - 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(() => { - if (walletsInitialized) { - const subscriptions = addListeners(); - - // 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 - - /** - * 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 () => { - 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(); - - await Notifications.clearStoredNotifications(); - Notifications.setApplicationIconBadgeNumber(0); - const deliveredNotifications = await Notifications.getDeliveredNotifications(); - setTimeout(() => Notifications.removeAllDeliveredNotifications(), 5000); // so notification bubble wont disappear too fast - - 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; - } - - if (wallet) { - const walletID = wallet.getID(); - fetchAndSaveWalletTransactions(walletID); - if (wasTapped) { - if (payload.type !== 3 || wallet.chain === Chain.OFFCHAIN) { - NavigationService.dispatch( - CommonActions.navigate({ - name: 'WalletTransactions', - params: { - walletID, - walletType: wallet.type, - }, - }), - ); - } else { - NavigationService.navigate('ReceiveDetailsRoot', { - screen: 'ReceiveDetails', - params: { - walletID, - address: payload.address, - }, - }); - } - - return true; - } - } 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; - }; - - const handleAppStateChange = async nextAppState => { - 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.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 }); - } - clipboardContent.current = clipboard; - } - if (nextAppState) { - appState.current = nextAppState; - } - }; - - const handleOpenURL = event => { - DeeplinkSchemaMatch.navigationRouteFor(event, value => NavigationService.navigate(...value), { - wallets, - addWallet, - saveToDisk, - setSharedCosigner, - }); - }; - - const showClipboardAlert = ({ contentType }) => { - 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; - } - }, - ); - }); - }; - - 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; diff --git a/App.tsx b/App.tsx new file mode 100644 index 000000000..a105764d7 --- /dev/null +++ b/App.tsx @@ -0,0 +1,34 @@ +import 'react-native-gesture-handler'; // should be on top +import React from 'react'; +import { 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 { BlueStorageProvider } from './blue_modules/storage-context'; +import { SettingsProvider } from './components/Context/SettingsContext'; +import { LargeScreenProvider } from './components/Context/LargeScreenProvider'; +import MasterView from './navigation/MasterView'; + +const App = () => { + const colorScheme = useColorScheme(); + + return ( + + + + + + + + + + + + + + ); +}; + +export default App; diff --git a/MasterView.tsx b/MasterView.tsx new file mode 100644 index 000000000..9f2614ef2 --- /dev/null +++ b/MasterView.tsx @@ -0,0 +1,22 @@ +import 'react-native-gesture-handler'; // should be on top +import React, { Suspense, lazy } from 'react'; +import MainRoot from './navigation'; +import { useStorage } from './blue_modules/storage-context'; +const CompanionDelegates = lazy(() => import('./components/CompanionDelegates')); + +const MasterView = () => { + const { walletsInitialized } = useStorage(); + + return ( + <> + + {walletsInitialized && ( + + + + )} + + ); +}; + +export default MasterView; diff --git a/android/app/build.gradle b/android/app/build.gradle index 6fac33df4..89be09784 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -79,7 +79,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode 1 - versionName "6.6.6" + versionName "6.6.7" testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 385824b24..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,71 +60,65 @@ + android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationListenerService" + android:exported="false"> - + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - + 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/android/build.gradle b/android/build.gradle index cbc794e23..5a0fac4db 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -83,9 +83,3 @@ subprojects { } } - -subprojects { subproject -> - if(project['name'] == 'react-native-widget-center') { - project.configurations { compile { } } - } -} diff --git a/blue_modules/BlueElectrum.ts b/blue_modules/BlueElectrum.ts index 65de31d05..67dd7d646 100644 --- a/blue_modules/BlueElectrum.ts +++ b/blue_modules/BlueElectrum.ts @@ -7,7 +7,6 @@ import Realm from 'realm'; import { LegacyWallet, SegwitBech32Wallet, SegwitP2SHWallet, TaprootWallet } from '../class'; import presentAlert from '../components/Alert'; import loc from '../loc'; -import { reloadAllTimelines } from '../components/WidgetCommunication'; import RNFS from 'react-native-fs'; const ElectrumClient = require('electrum-client'); @@ -52,7 +51,7 @@ type ElectrumTransaction = { }; }[]; blockhash: string; - confirmations?: number; + confirmations: number; time: number; blocktime: number; }; @@ -212,8 +211,6 @@ export async function connectMain(): Promise { await DefaultPreference.set(ELECTRUM_TCP_PORT, usingPeer.tcp ?? ''); await DefaultPreference.set(ELECTRUM_SSL_PORT, usingPeer.ssl ?? ''); } - - reloadAllTimelines(); } catch (e) { // Must be running on Android console.log(e); @@ -340,7 +337,6 @@ const presentNetworkErrorAlert = async (usingPeer?: Peer) => { await DefaultPreference.clear(ELECTRUM_HOST); await DefaultPreference.clear(ELECTRUM_SSL_PORT); await DefaultPreference.clear(ELECTRUM_TCP_PORT); - reloadAllTimelines(); } catch (e) { // Must be running on Android console.log(e); diff --git a/blue_modules/WidgetCommunication.ts b/blue_modules/WidgetCommunication.ts deleted file mode 100644 index f93d9c129..000000000 --- a/blue_modules/WidgetCommunication.ts +++ /dev/null @@ -1,9 +0,0 @@ -function WidgetCommunication() { - return null; -} - -WidgetCommunication.isBalanceDisplayAllowed = () => {}; -WidgetCommunication.setBalanceDisplayAllowed = () => {}; -WidgetCommunication.reloadAllTimelines = () => {}; - -export default WidgetCommunication; diff --git a/blue_modules/currency.ts b/blue_modules/currency.ts index 4f626a856..c954231bb 100644 --- a/blue_modules/currency.ts +++ b/blue_modules/currency.ts @@ -3,7 +3,6 @@ import DefaultPreference from 'react-native-default-preference'; import * as RNLocalize from 'react-native-localize'; import BigNumber from 'bignumber.js'; import { FiatUnit, FiatUnitType, getFiatRate } from '../models/fiatUnit'; -import { reloadAllTimelines } from '../components/WidgetCommunication'; const PREFERRED_CURRENCY_STORAGE_KEY = 'preferredCurrency'; const PREFERRED_CURRENCY_LOCALE_STORAGE_KEY = 'preferredCurrencyLocale'; @@ -32,8 +31,6 @@ async function setPreferredCurrency(item: FiatUnitType): Promise { await DefaultPreference.setName(GROUP_IO_BLUEWALLET); await DefaultPreference.set(PREFERRED_CURRENCY_STORAGE_KEY, item.endPointKey); await DefaultPreference.set(PREFERRED_CURRENCY_LOCALE_STORAGE_KEY, item.locale.replace('-', '_')); - // @ts-ignore: Convert to TSX later - reloadAllTimelines(); } async function getPreferredCurrency(): Promise { diff --git a/blue_modules/environment.ts b/blue_modules/environment.ts index c6e5db789..987f92c9b 100644 --- a/blue_modules/environment.ts +++ b/blue_modules/environment.ts @@ -1,6 +1,7 @@ -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'; -export const isHandset: boolean = getDeviceType() === 'Handset'; -export { isDesktop, isTablet }; +export { isDesktop, isTablet, isHandset }; diff --git a/blue_modules/start-and-decrypt.ts b/blue_modules/start-and-decrypt.ts index 3be918f51..0145b60ae 100644 --- a/blue_modules/start-and-decrypt.ts +++ b/blue_modules/start-and-decrypt.ts @@ -1,8 +1,8 @@ import { Platform } from 'react-native'; -import Biometric from '../class/biometrics'; import prompt from '../helpers/prompt'; import loc from '../loc'; import { BlueApp as BlueAppClass } from '../class/'; +import { showKeychainWipeAlert } from '../hooks/useBiometrics'; const BlueApp = BlueAppClass.getInstance(); // If attempt reaches 10, a wipe keychain option will be provided to the user. @@ -55,7 +55,7 @@ export const startAndDecrypt = async (retry?: boolean): Promise => { return startAndDecrypt(true); } else { unlockAttempt = 0; - Biometric.showKeychainWipeAlert(); + showKeychainWipeAlert(); // We want to return false to let the UnlockWith screen that it is NOT ok to proceed. return false; } diff --git a/class/biometrics.ts b/class/biometrics.ts deleted file mode 100644 index d22e9093d..000000000 --- a/class/biometrics.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { useContext } from 'react'; -import { Alert, Platform } from 'react-native'; -import ReactNativeBiometrics, { BiometryTypes as RNBiometryTypes } from 'react-native-biometrics'; -import PasscodeAuth from 'react-native-passcode-auth'; -import RNSecureKeyStore, { ACCESSIBLE } from 'react-native-secure-key-store'; -import loc from '../loc'; -import * as NavigationService from '../NavigationService'; -import { BlueStorageContext } from '../blue_modules/storage-context'; -import presentAlert from '../components/Alert'; - -const STORAGEKEY = 'Biometrics'; - -const rnBiometrics = new ReactNativeBiometrics({ allowDeviceCredentials: true }); - -// Define a function type with properties -type DescribableFunction = { - (): null; // Call signature - FaceID: 'Face ID'; - TouchID: 'Touch ID'; - Biometrics: 'Biometrics'; - isBiometricUseCapableAndEnabled: () => Promise; - isDeviceBiometricCapable: () => Promise; - setBiometricUseEnabled: (arg: boolean) => Promise; - biometricType: () => Promise; - isBiometricUseEnabled: () => Promise; - unlockWithBiometrics: () => Promise; - showKeychainWipeAlert: () => void; -}; - -// Bastard component/module. All properties are added in runtime -const Biometric = function () { - const { getItem, setItem } = useContext(BlueStorageContext); - Biometric.FaceID = 'Face ID'; - Biometric.TouchID = 'Touch ID'; - Biometric.Biometrics = 'Biometrics'; - - Biometric.isDeviceBiometricCapable = async () => { - try { - const { available } = await rnBiometrics.isSensorAvailable(); - return available; - } catch (e) { - console.log('Biometrics isDeviceBiometricCapable failed'); - console.log(e); - Biometric.setBiometricUseEnabled(false); - } - return false; - }; - - Biometric.biometricType = async () => { - try { - const { available, biometryType } = await rnBiometrics.isSensorAvailable(); - if (!available) { - return undefined; - } - - return biometryType; - } catch (e) { - console.log('Biometrics biometricType failed'); - console.log(e); - return undefined; // Explicitly return false in case of an error - } - }; - - Biometric.isBiometricUseEnabled = async () => { - try { - const enabledBiometrics = await getItem(STORAGEKEY); - return !!enabledBiometrics; - } catch (_) {} - - return false; - }; - - Biometric.isBiometricUseCapableAndEnabled = async () => { - const isBiometricUseEnabled = await Biometric.isBiometricUseEnabled(); - const isDeviceBiometricCapable = await Biometric.isDeviceBiometricCapable(); - return isBiometricUseEnabled && isDeviceBiometricCapable; - }; - - Biometric.setBiometricUseEnabled = async value => { - await setItem(STORAGEKEY, value === true ? '1' : ''); - }; - - Biometric.unlockWithBiometrics = async () => { - const isDeviceBiometricCapable = await Biometric.isDeviceBiometricCapable(); - if (isDeviceBiometricCapable) { - return new Promise(resolve => { - rnBiometrics - .simplePrompt({ promptMessage: loc.settings.biom_conf_identity }) - .then((result: { success: any }) => { - if (result.success) { - resolve(true); - } else { - console.log('Biometrics authentication failed'); - resolve(false); - } - }) - .catch((error: Error) => { - console.log('Biometrics authentication error'); - presentAlert({ message: error.message }); - resolve(false); - }); - }); - } - return false; - }; - - const clearKeychain = async () => { - try { - console.log('Wiping keychain'); - console.log('Wiping key: data'); - await RNSecureKeyStore.set('data', JSON.stringify({ data: { wallets: [] } }), { - accessible: ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY, - }); - console.log('Wiped key: data'); - console.log('Wiping key: data_encrypted'); - await RNSecureKeyStore.set('data_encrypted', '', { accessible: ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY }); - console.log('Wiped key: data_encrypted'); - console.log('Wiping key: STORAGEKEY'); - await RNSecureKeyStore.set(STORAGEKEY, '', { accessible: ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY }); - console.log('Wiped key: STORAGEKEY'); - NavigationService.reset(); - } catch (error: any) { - console.warn(error); - presentAlert({ message: error.message }); - } - }; - - const requestDevicePasscode = async () => { - let isDevicePasscodeSupported: boolean | undefined = false; - try { - isDevicePasscodeSupported = await PasscodeAuth.isSupported(); - if (isDevicePasscodeSupported) { - const isAuthenticated = await PasscodeAuth.authenticate(); - if (isAuthenticated) { - Alert.alert( - loc.settings.encrypt_tstorage, - loc.settings.biom_remove_decrypt, - [ - { text: loc._.cancel, style: 'cancel' }, - { - text: loc._.ok, - style: 'destructive', - onPress: async () => await clearKeychain(), - }, - ], - { cancelable: false }, - ); - } - } - } catch { - isDevicePasscodeSupported = undefined; - } - if (isDevicePasscodeSupported === false) { - presentAlert({ message: loc.settings.biom_no_passcode }); - } - }; - - Biometric.showKeychainWipeAlert = () => { - if (Platform.OS === 'ios') { - Alert.alert( - loc.settings.encrypt_tstorage, - loc.settings.biom_10times, - [ - { - text: loc._.cancel, - onPress: () => { - console.log('Cancel Pressed'); - }, - style: 'cancel', - }, - { - text: loc._.ok, - onPress: () => requestDevicePasscode(), - style: 'default', - }, - ], - { cancelable: false }, - ); - } - }; - - return null; -} as DescribableFunction; - -export default Biometric; -export { RNBiometryTypes as BiometricType }; 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/components/CompanionDelegates.tsx b/components/CompanionDelegates.tsx new file mode 100644 index 000000000..ef74630f6 --- /dev/null +++ b/components/CompanionDelegates.tsx @@ -0,0 +1,245 @@ +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, AppStateStatus } from 'react-native'; +import { CommonActions } from '@react-navigation/native'; +import { navigationRef } 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 triggerHapticFeedback, { HapticFeedbackTypes } from '../blue_modules/hapticFeedback'; +import { updateExchangeRate } from '../blue_modules/currency'; +import A from '../blue_modules/analytics'; +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; + +const ClipboardContentType = Object.freeze({ + BITCOIN: 'BITCOIN', + LIGHTNING: 'LIGHTNING', +}); + +if (Platform.OS === 'android') { + if (UIManager.setLayoutAnimationEnabledExperimental) { + UIManager.setLayoutAnimationEnabledExperimental(true); + } +} + +const CompanionDelegates = () => { + const { wallets, addWallet, saveToDisk, fetchAndSaveWalletTransactions, refreshAllWalletTransactions, setSharedCosigner } = useStorage(); + const appState = useRef(AppState.currentState); + const clipboardContent = useRef(); + + const processPushNotifications = useCallback(async () => { + await new Promise(resolve => setTimeout(resolve, 200)); + // @ts-ignore: Notifications type is not defined + const notifications2process = await Notifications.getStoredNotifications(); + // @ts-ignore: Notifications type is not defined + await Notifications.clearStoredNotifications(); + // @ts-ignore: Notifications type is not defined + Notifications.setApplicationIconBadgeNumber(0); + // @ts-ignore: Notifications type is not defined + const deliveredNotifications = await Notifications.getDeliveredNotifications(); + // @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); + + 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) { + 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'); + } + } + + if (deliveredNotifications.length > 0) { + refreshAllWalletTransactions(); + } + + return false; + }, [fetchAndSaveWalletTransactions, refreshAllWalletTransactions, wallets]); + + const handleOpenURL = useCallback( + (event: { url: string }) => { + DeeplinkSchemaMatch.navigationRouteFor(event, value => navigationRef.navigate(...value), { + wallets, + addWallet, + saveToDisk, + setSharedCosigner, + }); + }, + [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 }); + } + clipboardContent.current = clipboard; + } + if (nextAppState) { + appState.current = nextAppState; + } + }, + [processPushNotifications, showClipboardAlert, wallets], + ); + + const onNotificationReceived = useCallback( + async (notification: { data: { data: any } }) => { + const payload = Object.assign({}, notification, notification.data); + if (notification.data && notification.data.data) Object.assign(payload, notification.data.data); + // @ts-ignore: Notifications type is not defined + payload.foreground = true; + + // @ts-ignore: Notifications type is not defined + await Notifications.addNotification(payload); + // @ts-ignore: Notifications type is not defined + if (payload.foreground) await processPushNotifications(); + }, + [processPushNotifications], + ); + + const addListeners = useCallback(() => { + const urlSubscription = Linking.addEventListener('url', handleOpenURL); + const appStateSubscription = AppState.addEventListener('change', handleAppStateChange); + const notificationSubscription = eventEmitter?.addListener('onNotificationReceived', onNotificationReceived); + + return { + urlSubscription, + appStateSubscription, + notificationSubscription, + }; + }, [handleOpenURL, handleAppStateChange, onNotificationReceived]); + + useEffect(() => { + const subscriptions = addListeners(); + + return () => { + subscriptions.urlSubscription?.remove(); + subscriptions.appStateSubscription?.remove(); + subscriptions.notificationSubscription?.remove(); + }; + }, [addListeners]); + + return ( + <> + + + + + + + + + + ); +}; + +export default CompanionDelegates; diff --git a/components/Context/LargeScreenProvider.tsx b/components/Context/LargeScreenProvider.tsx new file mode 100644 index 000000000..f39fef626 --- /dev/null +++ b/components/Context/LargeScreenProvider.tsx @@ -0,0 +1,41 @@ +import React, { createContext, useState, useEffect, useMemo, ReactNode } from 'react'; +import { Dimensions } from 'react-native'; +import { isDesktop, isTablet } 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 halfScreenWidth = windowWidth >= screenWidth / 2; + const condition = (isTablet && 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/components/MenuElements.ios.tsx b/components/MenuElements.ios.tsx index 7c936e5d0..e1b887564 100644 --- a/components/MenuElements.ios.tsx +++ b/components/MenuElements.ios.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useContext, useEffect } from 'react'; +import { useCallback, useContext, useEffect } from 'react'; import { NativeEventEmitter, NativeModules, Platform } from 'react-native'; import * as NavigationService from '../NavigationService'; import { CommonActions } from '@react-navigation/native'; @@ -65,7 +65,7 @@ const MenuElements = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [walletsInitialized]); - return <>; + return null; }; export default MenuElements; diff --git a/components/QRCodeComponent.tsx b/components/QRCodeComponent.tsx index abbc5c1cd..c11b55ba7 100644 --- a/components/QRCodeComponent.tsx +++ b/components/QRCodeComponent.tsx @@ -7,6 +7,7 @@ import loc from '../loc'; import Clipboard from '@react-native-clipboard/clipboard'; import { useTheme } from './themes'; import { ActionIcons } from '../typings/ActionIcons'; +import { Action } from './types'; interface QRCodeComponentProps { value: string; @@ -18,22 +19,6 @@ interface QRCodeComponentProps { onError?: () => void; } -interface ActionType { - Share: 'share'; - Copy: 'copy'; -} - -interface Action { - id: string; - text: string; - icon: ActionIcons; -} - -const actionKeys: ActionType = { - Share: 'share', - Copy: 'copy', -}; - const actionIcons: { [key: string]: ActionIcons } = { Share: { iconType: 'SYSTEM', @@ -45,6 +30,22 @@ const actionIcons: { [key: string]: ActionIcons } = { }, }; +const actionKeys = { + Share: 'share', + Copy: 'copy', +}; + +const menuActions: Action[] = + Platform.OS === 'ios' || Platform.OS === 'macos' + ? [ + { + id: actionKeys.Copy, + text: loc.transactions.details_copy, + icon: actionIcons.Copy, + }, + ] + : [{ id: actionKeys.Share, text: loc.receive.details_share, icon: actionIcons.Share }]; + const QRCodeComponent: React.FC = ({ value = '', isLogoRendered = true, @@ -75,23 +76,6 @@ const QRCodeComponent: React.FC = ({ } }; - const menuActions = (): Action[] => { - const actions: Action[] = []; - if (Platform.OS === 'ios' || Platform.OS === 'macos') { - actions.push({ - id: actionKeys.Copy, - text: loc.transactions.details_copy, - icon: actionIcons.Copy, - }); - } - actions.push({ - id: actionKeys.Share, - text: loc.receive.details_share, - icon: actionIcons.Share, - }); - return actions; - }; - const renderQRCode = ( = ({ accessibilityLabel={loc.receive.qrcode_for_the_address} > {isMenuAvailable ? ( - + {renderQRCode} ) : ( diff --git a/components/SaveFileButton.tsx b/components/SaveFileButton.tsx index f417f6344..98607b0f1 100644 --- a/components/SaveFileButton.tsx +++ b/components/SaveFileButton.tsx @@ -1,9 +1,10 @@ import React, { ReactNode } from 'react'; import { StyleProp, ViewStyle } from 'react-native'; -import ToolTipMenu from './TooltipMenu'; import loc from '../loc'; import { ActionIcons } from '../typings/ActionIcons'; import * as fs from '../blue_modules/fs'; +import { Action } from './types'; +import ToolTipMenu from './TooltipMenu'; interface SaveFileButtonProps { fileName: string; @@ -11,7 +12,7 @@ interface SaveFileButtonProps { children?: ReactNode; style?: StyleProp; afterOnPress?: () => void; - beforeOnPress?: () => Promise; // Changed this line + beforeOnPress?: () => Promise; onMenuWillHide?: () => void; onMenuWillShow?: () => void; } @@ -26,30 +27,24 @@ const SaveFileButton: React.FC = ({ onMenuWillHide, onMenuWillShow, }) => { - const actions = [ - { id: 'save', text: loc._.save, icon: actionIcons.Save }, - { id: 'share', text: loc.receive.details_share, icon: actionIcons.Share }, - ]; - const handlePressMenuItem = async (actionId: string) => { if (beforeOnPress) { - await beforeOnPress(); // Now properly awaiting a function that returns a promise + await beforeOnPress(); } const action = actions.find(a => a.id === actionId); if (action?.id === 'save') { await fs.writeFileAndExport(fileName, fileContent, false).finally(() => { - afterOnPress?.(); // Safely call afterOnPress if it exists + afterOnPress?.(); }); } else if (action?.id === 'share') { await fs.writeFileAndExport(fileName, fileContent, true).finally(() => { - afterOnPress?.(); // Safely call afterOnPress if it exists + afterOnPress?.(); }); } }; return ( - // @ts-ignore: Tooltip must be refactored to use TSX} = ({ isMenuPrimaryAction actions={actions} onPressMenuItem={handlePressMenuItem} - buttonStyle={style} + buttonStyle={style as ViewStyle} // Type assertion to match ViewStyle > {children} @@ -76,3 +71,7 @@ const actionIcons: { [key: string]: ActionIcons } = { iconValue: 'square.and.arrow.down', }, }; +const actions: Action[] = [ + { id: 'save', text: loc._.save, icon: actionIcons.Save }, + { id: 'share', text: loc.receive.details_share, icon: actionIcons.Share }, +]; diff --git a/components/TooltipMenu.android.js b/components/TooltipMenu.android.js deleted file mode 100644 index c9a1d3190..000000000 --- a/components/TooltipMenu.android.js +++ /dev/null @@ -1,64 +0,0 @@ -import React, { useRef, useEffect, forwardRef } from 'react'; -import PropTypes from 'prop-types'; -import { Pressable } from 'react-native'; -import showPopupMenu from '../blue_modules/showPopupMenu'; - -const BaseToolTipMenu = (props, ref) => { - const menuRef = useRef(); - const disabled = props.disabled ?? false; - const isMenuPrimaryAction = props.isMenuPrimaryAction ?? false; - const enableAndroidRipple = props.enableAndroidRipple ?? true; - const buttonStyle = props.buttonStyle ?? {}; - const handleToolTipSelection = selection => { - props.onPressMenuItem(selection.id); - }; - - useEffect(() => { - if (ref && ref.current) { - ref.current.dismissMenu = dismissMenu; - } - }, [ref]); - - const dismissMenu = () => { - console.log('dismissMenu Not implemented'); - }; - - const showMenu = () => { - const menu = []; - for (const actions of props.actions) { - if (Array.isArray(actions)) { - for (const actionToMap of actions) { - menu.push({ id: actionToMap.id, label: actionToMap.text }); - } - } else { - menu.push({ id: actions.id, label: actions.text }); - } - } - - showPopupMenu(menu, handleToolTipSelection, menuRef.current); - }; - - return ( - - {props.children} - - ); -}; - -const ToolTipMenu = forwardRef(BaseToolTipMenu); - -export default ToolTipMenu; -ToolTipMenu.propTypes = { - actions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]).isRequired, - children: PropTypes.node, - onPressMenuItem: PropTypes.func.isRequired, - isMenuPrimaryAction: PropTypes.bool, - onPress: PropTypes.func, - disabled: PropTypes.bool, -}; diff --git a/components/TooltipMenu.android.tsx b/components/TooltipMenu.android.tsx new file mode 100644 index 000000000..00ab41999 --- /dev/null +++ b/components/TooltipMenu.android.tsx @@ -0,0 +1,76 @@ +import React, { useRef, useEffect, forwardRef, Ref, useMemo, useCallback } from 'react'; +import { Pressable, View } from 'react-native'; +import showPopupMenu, { OnPopupMenuItemSelect, PopupMenuItem } from '../blue_modules/showPopupMenu.android'; +import { ToolTipMenuProps } from './types'; + +const dismissMenu = () => { + console.log('dismissMenu Not implemented'); +}; +const BaseToolTipMenu = (props: ToolTipMenuProps, ref: Ref<{ dismissMenu?: () => void }>) => { + const menuRef = useRef(null); + const { + actions, + children, + onPressMenuItem, + isMenuPrimaryAction = false, + buttonStyle = {}, + enableAndroidRipple = true, + disabled = false, + onPress, + ...restProps + } = props; + + const handleToolTipSelection = useCallback( + (selection: PopupMenuItem) => { + if (selection.id) { + onPressMenuItem(selection.id); + } + }, + [onPressMenuItem], + ); + + useEffect(() => { + // @ts-ignore: fix later + if (ref && ref.current) { + // @ts-ignore: fix later + ref.current.dismissMenu = dismissMenu; + } + }, [ref]); + + const menuItems = useMemo(() => { + const menu: { id: string; label: string }[] = []; + actions.forEach(action => { + if (Array.isArray(action)) { + action.forEach(actionToMap => { + menu.push({ id: actionToMap.id.toString(), label: actionToMap.text }); + }); + } else { + menu.push({ id: action.id.toString(), label: action.text }); + } + }); + return menu; + }, [actions]); + + const showMenu = useCallback(() => { + if (menuRef.current) { + showPopupMenu(menuItems, handleToolTipSelection, menuRef.current); + } + }, [menuItems, handleToolTipSelection]); + + return ( + + {children} + + ); +}; + +const ToolTipMenu = forwardRef(BaseToolTipMenu); + +export default ToolTipMenu; diff --git a/components/TooltipMenu.ios.js b/components/TooltipMenu.ios.js deleted file mode 100644 index 2325f37f8..000000000 --- a/components/TooltipMenu.ios.js +++ /dev/null @@ -1,140 +0,0 @@ -import React, { forwardRef } from 'react'; -import { ContextMenuView, ContextMenuButton } from 'react-native-ios-context-menu'; -import PropTypes from 'prop-types'; -import { TouchableOpacity } from 'react-native'; - -const BaseToolTipMenu = (props, ref) => { - const menuItemMapped = ({ action, menuOptions }) => { - const item = { - actionKey: action.id, - actionTitle: action.text, - icon: action.icon, - menuOptions, - menuTitle: action.menuTitle, - }; - item.menuState = action.menuStateOn ? 'on' : 'off'; - - if (action.disabled) { - item.menuAttributes = ['disabled']; - } - return item; - }; - - const menuItems = props.actions.map(action => { - if (Array.isArray(action)) { - const mapped = []; - for (const actionToMap of action) { - mapped.push(menuItemMapped({ action: actionToMap })); - } - const submenu = { - menuOptions: ['displayInline'], - menuItems: mapped, - menuTitle: '', - }; - return submenu; - } else { - return menuItemMapped({ action }); - } - }); - const menuTitle = props.title ?? ''; - const isButton = !!props.isButton; - const isMenuPrimaryAction = props.isMenuPrimaryAction ? props.isMenuPrimaryAction : false; - const renderPreview = props.renderPreview ?? undefined; - const disabled = props.disabled ?? false; - const onPress = props.onPress ?? undefined; - const onMenuWillShow = props.onMenuWillShow ?? undefined; - const onMenuWillHide = props.onMenuWillHide ?? undefined; - - const buttonStyle = props.buttonStyle; - return isButton ? ( - - { - props.onPressMenuItem(nativeEvent.actionKey); - }} - isMenuPrimaryAction={isMenuPrimaryAction} - menuConfig={{ - menuTitle, - menuItems, - }} - > - {props.children} - - - ) : props.onPress ? ( - { - props.onPressMenuItem(nativeEvent.actionKey); - }} - useActionSheetFallback={false} - menuConfig={{ - menuTitle, - menuItems, - }} - {...(renderPreview - ? { - previewConfig: { - previewType: 'CUSTOM', - backgroundColor: 'white', - }, - renderPreview, - } - : {})} - > - - {props.children} - - - ) : ( - { - props.onPressMenuItem(nativeEvent.actionKey); - }} - lazyPreview - shouldEnableAggressiveCleanup - useActionSheetFallback={false} - menuConfig={{ - menuTitle, - menuItems, - }} - {...(renderPreview - ? { - previewConfig: { - previewType: 'CUSTOM', - backgroundColor: 'white', - }, - renderPreview, - } - : {})} - > - {props.children} - - ); -}; - -const ToolTipMenu = forwardRef(BaseToolTipMenu); - -export default ToolTipMenu; -ToolTipMenu.propTypes = { - actions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]).isRequired, - title: PropTypes.string, - children: PropTypes.node, - onPressMenuItem: PropTypes.func.isRequired, - isMenuPrimaryAction: PropTypes.bool, - isButton: PropTypes.bool, - renderPreview: PropTypes.func, - onPress: PropTypes.func, - previewValue: PropTypes.string, - disabled: PropTypes.bool, -}; diff --git a/components/TooltipMenu.ios.tsx b/components/TooltipMenu.ios.tsx new file mode 100644 index 000000000..44eacedae --- /dev/null +++ b/components/TooltipMenu.ios.tsx @@ -0,0 +1,122 @@ +import React, { forwardRef, Ref, useMemo, useCallback } from 'react'; +import { ContextMenuView, ContextMenuButton, RenderItem } from 'react-native-ios-context-menu'; +import { TouchableOpacity } from 'react-native'; +import { ToolTipMenuProps, Action } from './types'; + +const BaseToolTipMenu = (props: ToolTipMenuProps, ref: Ref) => { + const { + title = '', + isButton = false, + isMenuPrimaryAction = false, + renderPreview, + disabled = false, + onPress, + onMenuWillShow, + onMenuWillHide, + buttonStyle, + onPressMenuItem, + } = props; + + const menuItemMapped = useCallback(({ action, menuOptions }: { action: Action; menuOptions?: string[] }) => { + const item: any = { + actionKey: action.id.toString(), + actionTitle: action.text, + icon: action.icon, + menuOptions, + menuTitle: action.menuTitle, + }; + item.menuState = action.menuStateOn ? 'on' : 'off'; + + if (action.disabled) { + item.menuAttributes = ['disabled']; + } + return item; + }, []); + + const menuItems = useMemo( + () => + props.actions.map(action => { + if (Array.isArray(action)) { + const mapped = action.map(actionToMap => menuItemMapped({ action: actionToMap })); + return { + menuOptions: ['displayInline'], + menuItems: mapped, + menuTitle: '', + }; + } else { + return menuItemMapped({ action }); + } + }), + [props.actions, menuItemMapped], + ); + + const handlePressMenuItem = useCallback( + ({ nativeEvent }: { nativeEvent: { actionKey: string } }) => { + onPressMenuItem(nativeEvent.actionKey); + }, + [onPressMenuItem], + ); + + const renderContextMenuButton = () => ( + + {props.children} + + ); + + const renderContextMenuView = () => ( + + {onPress ? ( + + {props.children} + + ) : ( + props.children + )} + + ); + + return isMenuPrimaryAction && onPress ? ( + + {renderContextMenuButton()} + + ) : isButton ? ( + renderContextMenuButton() + ) : ( + renderContextMenuView() + ); +}; + +const ToolTipMenu = forwardRef(BaseToolTipMenu); + +export default ToolTipMenu; diff --git a/components/TooltipMenu.js b/components/TooltipMenu.js deleted file mode 100644 index ca6307f1b..000000000 --- a/components/TooltipMenu.js +++ /dev/null @@ -1,9 +0,0 @@ -import { forwardRef } from 'react'; - -const BaseToolTipMenu = (props, _ref) => { - return props.children; -}; - -const ToolTipMenu = forwardRef(BaseToolTipMenu); - -export default ToolTipMenu; diff --git a/components/TooltipMenu.tsx b/components/TooltipMenu.tsx new file mode 100644 index 000000000..26180bfac --- /dev/null +++ b/components/TooltipMenu.tsx @@ -0,0 +1,11 @@ +import { forwardRef, Ref } from 'react'; +import { ToolTipMenuProps } from './types'; + +const BaseToolTipMenu = (props: ToolTipMenuProps, ref: Ref) => { + console.debug('ToolTipMenu.tsx ref:', ref); + return props.children; +}; + +const ToolTipMenu = forwardRef(BaseToolTipMenu); + +export default ToolTipMenu; diff --git a/components/TransactionListItem.tsx b/components/TransactionListItem.tsx index db42bca60..b22cfee8d 100644 --- a/components/TransactionListItem.tsx +++ b/components/TransactionListItem.tsx @@ -20,6 +20,7 @@ import { useTheme } from './themes'; import ListItem from './ListItem'; import { useSettings } from './Context/SettingsContext'; import { LightningTransaction, Transaction } from '../class/wallets/types'; +import { Action } from './types'; interface TransactionListItemProps { itemPriceUnit: BitcoinUnit; @@ -287,9 +288,9 @@ export const TransactionListItem: React.FC = React.mem handleOnViewOnBlockExplorer, ], ); + const toolTipActions = useMemo((): Action[] | Action[][] => { + const actions: (Action | Action[])[] = []; - const toolTipActions = useMemo(() => { - const actions = []; if (rowTitle !== loc.lnd.expired) { actions.push({ id: actionKeys.CopyAmount, @@ -305,6 +306,7 @@ export const TransactionListItem: React.FC = React.mem icon: actionIcons.Clipboard, }); } + if (item.hash) { actions.push( { @@ -337,10 +339,9 @@ export const TransactionListItem: React.FC = React.mem ]); } - return actions; + return actions as Action[] | Action[][]; // eslint-disable-next-line react-hooks/exhaustive-deps }, [item.hash, subtitle, rowTitle, subtitleNumberOfLines, txMetadata]); - return ( diff --git a/components/WalletsCarousel.js b/components/WalletsCarousel.js index 8bf1f9159..4c7c716cd 100644 --- a/components/WalletsCarousel.js +++ b/components/WalletsCarousel.js @@ -154,7 +154,7 @@ const iStyles = StyleSheet.create({ }, }); -export const WalletCarouselItem = ({ item, _, onPress, handleLongPress, isSelectedWallet, customStyle }) => { +export const WalletCarouselItem = React.memo(({ item, _, onPress, handleLongPress, isSelectedWallet, customStyle }) => { const scaleValue = new Animated.Value(1.0); const { colors } = useTheme(); const { walletTransactionUpdateStatus } = useContext(BlueStorageContext); @@ -251,7 +251,7 @@ export const WalletCarouselItem = ({ item, _, onPress, handleLongPress, isSelect ); -}; +}); WalletCarouselItem.propTypes = { item: PropTypes.any, @@ -292,7 +292,7 @@ const WalletsCarousel = forwardRef((props, ref) => { ), // eslint-disable-next-line react-hooks/exhaustive-deps - [horizontal, selectedWallet, handleLongPress, onPress, preferredFiatCurrency, language], + [horizontal, selectedWallet, preferredFiatCurrency, language], ); const flatListRef = useRef(); diff --git a/WatchConnectivity.ios.js b/components/WatchConnectivity.ios.js similarity index 96% rename from WatchConnectivity.ios.js rename to components/WatchConnectivity.ios.js index 98a268201..39a9e3940 100644 --- a/WatchConnectivity.ios.js +++ b/components/WatchConnectivity.ios.js @@ -7,13 +7,13 @@ import { transferCurrentComplicationUserInfo, transferUserInfo, } from 'react-native-watch-connectivity'; -import { Chain } from './models/bitcoinUnits'; -import loc, { formatBalance, transactionTimeToReadable } from './loc'; -import { BlueStorageContext } from './blue_modules/storage-context'; -import Notifications from './blue_modules/notifications'; -import { FiatUnit } from './models/fiatUnit'; -import { MultisigHDWallet } from './class'; -import { useSettings } from './components/Context/SettingsContext'; +import { Chain } from '../models/bitcoinUnits'; +import loc, { formatBalance, transactionTimeToReadable } from '../loc'; +import { BlueStorageContext } from '../blue_modules/storage-context'; +import Notifications from '../blue_modules/notifications'; +import { FiatUnit } from '../models/fiatUnit'; +import { MultisigHDWallet } from '../class'; +import { useSettings } from './Context/SettingsContext'; function WatchConnectivity() { const { walletsInitialized, wallets, fetchWalletTransactions, saveToDisk, txMetadata } = useContext(BlueStorageContext); diff --git a/WatchConnectivity.js b/components/WatchConnectivity.js similarity index 100% rename from WatchConnectivity.js rename to components/WatchConnectivity.js diff --git a/components/WidgetCommunication.ios.tsx b/components/WidgetCommunication.ios.tsx index fa95b198b..3738e3b36 100644 --- a/components/WidgetCommunication.ios.tsx +++ b/components/WidgetCommunication.ios.tsx @@ -1,7 +1,5 @@ import React, { useContext, useEffect } from 'react'; import DefaultPreference from 'react-native-default-preference'; -// @ts-ignore: no type definitions -import RNWidgetCenter from 'react-native-widget-center'; import { BlueStorageContext } from '../blue_modules/storage-context'; import { TWallet } from '../class/wallets/types'; import { useSettings } from './Context/SettingsContext'; @@ -13,10 +11,6 @@ enum WidgetCommunicationKeys { LatestTransactionIsUnconfirmed = 'WidgetCommunicationLatestTransactionIsUnconfirmed', } -export const reloadAllTimelines = (): void => { - RNWidgetCenter.reloadAllTimelines(); -}; - export const isBalanceDisplayAllowed = async (): Promise => { try { await DefaultPreference.setName('group.io.bluewallet.bluewallet'); @@ -34,7 +28,6 @@ export const setBalanceDisplayAllowed = async (value: boolean): Promise => } else { await DefaultPreference.clear(WidgetCommunicationKeys.DisplayBalanceAllowed); } - reloadAllTimelines(); }; export const syncWidgetBalanceWithWallets = async (wallets: TWallet[], walletsInitialized: boolean): Promise => { @@ -42,7 +35,6 @@ export const syncWidgetBalanceWithWallets = async (wallets: TWallet[], walletsIn const { allWalletsBalance, latestTransactionTime } = await allWalletsBalanceAndTransactionTime(wallets, walletsInitialized); await DefaultPreference.set(WidgetCommunicationKeys.AllWalletsSatoshiBalance, String(allWalletsBalance)); await DefaultPreference.set(WidgetCommunicationKeys.AllWalletsLatestTransactionTime, String(latestTransactionTime)); - reloadAllTimelines(); }; const allWalletsBalanceAndTransactionTime = async ( diff --git a/components/WidgetCommunication.tsx b/components/WidgetCommunication.tsx index 0afb6dbc4..87d7e9dad 100644 --- a/components/WidgetCommunication.tsx +++ b/components/WidgetCommunication.tsx @@ -7,8 +7,6 @@ export const isBalanceDisplayAllowed = async (): Promise => { export const setBalanceDisplayAllowed = async (value: boolean): Promise => {}; -export const reloadAllTimelines = (): void => {}; - export const syncWidgetBalanceWithWallets = async (_wallets: TWallet[], _walletsInitialized: boolean): Promise => {}; const WidgetCommunication: React.FC = () => { diff --git a/components/addresses/AddressItem.tsx b/components/addresses/AddressItem.tsx index a6ce981f7..06e738367 100644 --- a/components/addresses/AddressItem.tsx +++ b/components/addresses/AddressItem.tsx @@ -1,8 +1,7 @@ -import React, { useContext, useRef } from 'react'; +import React, { useMemo, useRef } from 'react'; import { StyleSheet, Text, View } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { ListItem } from 'react-native-elements'; -import PropTypes from 'prop-types'; import { AddressTypeBadge } from './AddressTypeBadge'; import loc, { formatBalance } from '../../loc'; import TooltipMenu from '../TooltipMenu'; @@ -10,12 +9,13 @@ import Clipboard from '@react-native-clipboard/clipboard'; import Share from 'react-native-share'; import { useTheme } from '../themes'; import { BitcoinUnit } from '../../models/bitcoinUnits'; -import { BlueStorageContext } from '../../blue_modules/storage-context'; -import Biometric from '../../class/biometrics'; +import { useStorage } from '../../blue_modules/storage-context'; import presentAlert from '../Alert'; import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback'; import QRCodeComponent from '../QRCodeComponent'; import confirm from '../../helpers/confirm'; +import { useBiometrics } from '../../hooks/useBiometrics'; +import { Action } from '../types'; interface AddressItemProps { // todo: fix `any` after addresses.js is converted to the church of holy typescript @@ -26,8 +26,9 @@ interface AddressItemProps { } const AddressItem = ({ item, balanceUnit, walletID, allowSignVerifyMessage }: AddressItemProps) => { - const { wallets } = useContext(BlueStorageContext); + const { wallets } = useStorage(); const { colors } = useTheme(); + const { isBiometricUseCapableAndEnabled, unlockWithBiometrics } = useBiometrics(); const hasTransactions = item.transactions > 0; @@ -79,6 +80,8 @@ const AddressItem = ({ item, balanceUnit, walletID, allowSignVerifyMessage }: Ad }); }; + const menuActions = useMemo(() => getAvailableActions({ allowSignVerifyMessage }), [allowSignVerifyMessage]); + const balance = formatBalance(item.balance, balanceUnit, true); const handleCopyPress = () => { @@ -118,8 +121,8 @@ const AddressItem = ({ item, balanceUnit, walletID, allowSignVerifyMessage }: Ad navigateToSignVerify(); } else if (id === AddressItem.actionKeys.ExportPrivateKey) { if (await confirm(loc.addresses.sensitive_private_key)) { - if (await Biometric.isBiometricUseCapableAndEnabled()) { - if (!(await Biometric.unlockWithBiometrics())) { + if (await isBiometricUseCapableAndEnabled()) { + if (!(await unlockWithBiometrics())) { return; } } @@ -129,39 +132,6 @@ const AddressItem = ({ item, balanceUnit, walletID, allowSignVerifyMessage }: Ad } }; - const getAvailableActions = () => { - const actions = [ - { - id: AddressItem.actionKeys.CopyToClipboard, - text: loc.transactions.details_copy, - icon: AddressItem.actionIcons.Clipboard, - }, - { - id: AddressItem.actionKeys.Share, - text: loc.receive.details_share, - icon: AddressItem.actionIcons.Share, - }, - ]; - - if (allowSignVerifyMessage) { - actions.push({ - id: AddressItem.actionKeys.SignVerify, - text: loc.addresses.sign_title, - icon: AddressItem.actionIcons.Signature, - }); - } - - if (allowSignVerifyMessage) { - actions.push({ - id: AddressItem.actionKeys.ExportPrivateKey, - text: loc.addresses.copy_private_key, - icon: AddressItem.actionIcons.ExportPrivateKey, - }); - } - - return actions; - }; - const renderPreview = () => { return ; }; @@ -171,7 +141,7 @@ const AddressItem = ({ item, balanceUnit, walletID, allowSignVerifyMessage }: Ad { + const actions = [ + { + id: AddressItem.actionKeys.CopyToClipboard, + text: loc.transactions.details_copy, + icon: AddressItem.actionIcons.Clipboard, + }, + { + id: AddressItem.actionKeys.Share, + text: loc.receive.details_share, + icon: AddressItem.actionIcons.Share, + }, + ]; + + if (allowSignVerifyMessage) { + actions.push({ + id: AddressItem.actionKeys.SignVerify, + text: loc.addresses.sign_title, + icon: AddressItem.actionIcons.Signature, + }); + } + + if (allowSignVerifyMessage) { + actions.push({ + id: AddressItem.actionKeys.ExportPrivateKey, + text: loc.addresses.copy_private_key, + icon: AddressItem.actionIcons.ExportPrivateKey, + }); + } + + return actions; }; + export { AddressItem }; diff --git a/components/types.ts b/components/types.ts new file mode 100644 index 000000000..d4a7c623f --- /dev/null +++ b/components/types.ts @@ -0,0 +1,30 @@ +import { ViewStyle } from 'react-native'; + +export interface Action { + id: string | number; + text: string; + icon: { + iconType: string; + iconValue: string; + }; + menuTitle?: string; + menuStateOn?: boolean; + disabled?: boolean; +} + +export interface ToolTipMenuProps { + actions: Action[] | Action[][]; + children: React.ReactNode; + enableAndroidRipple?: boolean; + onPressMenuItem: (id: string) => void; + title?: string; + isMenuPrimaryAction?: boolean; + isButton?: boolean; + renderPreview?: () => React.ReactNode; + onPress?: () => void; + previewValue?: string; + disabled?: boolean; + buttonStyle?: ViewStyle; + onMenuWillShow?: () => void; + onMenuWillHide?: () => void; +} diff --git a/hooks/useBiometrics.ts b/hooks/useBiometrics.ts new file mode 100644 index 000000000..4a60714b9 --- /dev/null +++ b/hooks/useBiometrics.ts @@ -0,0 +1,195 @@ +import { useState, useEffect } from 'react'; +import { Alert, Platform } from 'react-native'; +import ReactNativeBiometrics, { BiometryTypes as RNBiometryTypes } from 'react-native-biometrics'; +import PasscodeAuth from 'react-native-passcode-auth'; +import RNSecureKeyStore, { ACCESSIBLE } from 'react-native-secure-key-store'; +import loc from '../loc'; +import * as NavigationService from '../NavigationService'; +import { useStorage } from '../blue_modules/storage-context'; +import presentAlert from '../components/Alert'; + +const STORAGEKEY = 'Biometrics'; +const rnBiometrics = new ReactNativeBiometrics({ allowDeviceCredentials: true }); + +const FaceID = 'Face ID'; +const TouchID = 'Touch ID'; +const Biometrics = 'Biometrics'; + +const clearKeychain = async () => { + try { + console.log('Wiping keychain'); + console.log('Wiping key: data'); + await RNSecureKeyStore.set('data', JSON.stringify({ data: { wallets: [] } }), { + accessible: ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY, + }); + console.log('Wiped key: data'); + console.log('Wiping key: data_encrypted'); + await RNSecureKeyStore.set('data_encrypted', '', { accessible: ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY }); + console.log('Wiped key: data_encrypted'); + console.log('Wiping key: STORAGEKEY'); + await RNSecureKeyStore.set(STORAGEKEY, '', { accessible: ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY }); + console.log('Wiped key: STORAGEKEY'); + NavigationService.reset(); + } catch (error: any) { + console.warn(error); + presentAlert({ message: error.message }); + } +}; + +const requestDevicePasscode = async () => { + let isDevicePasscodeSupported: boolean | undefined = false; + try { + isDevicePasscodeSupported = await PasscodeAuth.isSupported(); + if (isDevicePasscodeSupported) { + const isAuthenticated = await PasscodeAuth.authenticate(); + if (isAuthenticated) { + Alert.alert( + loc.settings.encrypt_tstorage, + loc.settings.biom_remove_decrypt, + [ + { text: loc._.cancel, style: 'cancel' }, + { + text: loc._.ok, + style: 'destructive', + onPress: async () => await clearKeychain(), + }, + ], + { cancelable: false }, + ); + } + } + } catch { + isDevicePasscodeSupported = undefined; + } + if (isDevicePasscodeSupported === false) { + presentAlert({ message: loc.settings.biom_no_passcode }); + } +}; + +const showKeychainWipeAlert = () => { + if (Platform.OS === 'ios') { + Alert.alert( + loc.settings.encrypt_tstorage, + loc.settings.biom_10times, + [ + { + text: loc._.cancel, + onPress: () => { + console.log('Cancel Pressed'); + }, + style: 'cancel', + }, + { + text: loc._.ok, + onPress: () => requestDevicePasscode(), + style: 'default', + }, + ], + { cancelable: false }, + ); + } +}; + +const useBiometrics = () => { + const { getItem, setItem } = useStorage(); + const [biometricEnabled, setBiometricEnabled] = useState(false); + const [deviceBiometricType, setDeviceBiometricType] = useState<'TouchID' | 'FaceID' | 'Biometrics' | undefined>(undefined); + + useEffect(() => { + const fetchBiometricEnabledStatus = async () => { + const enabled = await isBiometricUseEnabled(); + setBiometricEnabled(enabled); + + const biometricType = await type(); + setDeviceBiometricType(biometricType); + }; + + fetchBiometricEnabledStatus(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const isDeviceBiometricCapable = async () => { + try { + const { available } = await rnBiometrics.isSensorAvailable(); + return available; + } catch (e) { + console.log('Biometrics isDeviceBiometricCapable failed'); + console.log(e); + setBiometricUseEnabled(false); + } + return false; + }; + + const type = async () => { + try { + const { available, biometryType } = await rnBiometrics.isSensorAvailable(); + if (!available) { + return undefined; + } + + return biometryType; + } catch (e) { + console.log('Biometrics biometricType failed'); + console.log(e); + return undefined; + } + }; + + const isBiometricUseEnabled = async () => { + try { + const enabledBiometrics = await getItem(STORAGEKEY); + return !!enabledBiometrics; + } catch (_) {} + + return false; + }; + + const isBiometricUseCapableAndEnabled = async () => { + const isEnabled = await isBiometricUseEnabled(); + const isCapable = await isDeviceBiometricCapable(); + return isEnabled && isCapable; + }; + + const setBiometricUseEnabled = async (value: boolean) => { + await setItem(STORAGEKEY, value === true ? '1' : ''); + setBiometricEnabled(value); + }; + + const unlockWithBiometrics = async () => { + const isCapable = await isDeviceBiometricCapable(); + if (isCapable) { + return new Promise(resolve => { + rnBiometrics + .simplePrompt({ promptMessage: loc.settings.biom_conf_identity }) + .then((result: { success: any }) => { + if (result.success) { + resolve(true); + } else { + console.log('Biometrics authentication failed'); + resolve(false); + } + }) + .catch((error: Error) => { + console.log('Biometrics authentication error'); + presentAlert({ message: error.message }); + resolve(false); + }); + }); + } + return false; + }; + + return { + isDeviceBiometricCapable, + deviceBiometricType, + isBiometricUseEnabled, + isBiometricUseCapableAndEnabled, + setBiometricUseEnabled, + unlockWithBiometrics, + clearKeychain, + requestDevicePasscode, + biometricEnabled, + }; +}; + +export { FaceID, TouchID, Biometrics, RNBiometryTypes as BiometricType, useBiometrics, showKeychainWipeAlert }; diff --git a/hooks/useExtendedNavigation.ts b/hooks/useExtendedNavigation.ts index 6eb612c8d..885f84bac 100644 --- a/hooks/useExtendedNavigation.ts +++ b/hooks/useExtendedNavigation.ts @@ -1,9 +1,8 @@ import { useNavigation, NavigationProp, ParamListBase } from '@react-navigation/native'; -import Biometric from '../class/biometrics'; import { navigationRef } from '../NavigationService'; -import { BlueStorageContext } from '../blue_modules/storage-context'; -import { useContext } from 'react'; +import { useStorage } from '../blue_modules/storage-context'; import { presentWalletExportReminder } from '../helpers/presentWalletExportReminder'; +import { useBiometrics } from './useBiometrics'; // List of screens that require biometrics @@ -15,7 +14,8 @@ const requiresWalletExportIsSaved = ['ReceiveDetailsRoot', 'WalletAddresses']; export const useExtendedNavigation = (): NavigationProp => { const originalNavigation = useNavigation>(); - const { wallets, saveToDisk } = useContext(BlueStorageContext); + const { wallets, saveToDisk } = useStorage(); + const { isBiometricUseEnabled, unlockWithBiometrics } = useBiometrics(); const enhancedNavigate: NavigationProp['navigate'] = (screenOrOptions: any, params?: any) => { let screenName: string; @@ -42,12 +42,12 @@ export const useExtendedNavigation = (): NavigationProp => { (async () => { if (isRequiresBiometrics) { - const isBiometricsEnabled = await Biometric.isBiometricUseEnabled(); + const isBiometricsEnabled = await isBiometricUseEnabled(); if (isBiometricsEnabled) { - const isAuthenticated = await Biometric.unlockWithBiometrics(); + const isAuthenticated = await unlockWithBiometrics(); if (isAuthenticated) { proceedWithNavigation(); - return; // Ensure the function exits if this path is taken + return; } else { console.error('Biometric authentication failed'); // Decide if navigation should proceed or not after failed authentication 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.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); diff --git a/ios/BlueWallet-Bridging-Header.h b/ios/BlueWallet-Bridging-Header.h index 84805a30e..73636bdb6 100644 --- a/ios/BlueWallet-Bridging-Header.h +++ b/ios/BlueWallet-Bridging-Header.h @@ -2,4 +2,5 @@ // Use this file to import your target's public headers that you would like to expose to Swift. // +#import "AppDelegate.h" #import diff --git a/ios/BlueWallet.xcodeproj/project.pbxproj b/ios/BlueWallet.xcodeproj/project.pbxproj index 402662366..b92a61197 100644 --- a/ios/BlueWallet.xcodeproj/project.pbxproj +++ b/ios/BlueWallet.xcodeproj/project.pbxproj @@ -151,13 +151,14 @@ B4A29A3A2B55C990002A67DF /* BlueWalletWatch.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = B40D4E30225841EC00428FCC /* BlueWalletWatch.app */; platformFilter = ios; }; B4A29A3C2B55C990002A67DF /* Stickers.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 6D2A6461258BA92C0092292B /* Stickers.appex */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; B4A29A3D2B55C990002A67DF /* WidgetsExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 6DD4109C266CADF10087DE03 /* WidgetsExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - B4AB21072B61D8CA0080440C /* SplashScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4AB21062B61D8CA0080440C /* SplashScreen.swift */; }; - B4AB21092B61DC3F0080440C /* SplashScreen.m in Sources */ = {isa = PBXBuildFile; fileRef = B4AB21082B61DC3F0080440C /* SplashScreen.m */; }; B4AB225D2B02AD12001F4328 /* XMLParserDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4AB225C2B02AD12001F4328 /* XMLParserDelegate.swift */; }; B4AB225E2B02AD12001F4328 /* XMLParserDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4AB225C2B02AD12001F4328 /* XMLParserDelegate.swift */; }; + B4B1A4622BFA73110072E3BB /* WidgetHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B1A4612BFA73110072E3BB /* WidgetHelper.swift */; }; + B4B1A4642BFA73110072E3BB /* WidgetHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B1A4612BFA73110072E3BB /* WidgetHelper.swift */; }; + B4B1A4652BFA73110072E3BB /* WidgetHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B1A4612BFA73110072E3BB /* WidgetHelper.swift */; }; B4EE583C226703320003363C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B40D4E35225841ED00428FCC /* Assets.xcassets */; }; C59F90CE0D04D3E4BB39BC5D /* libPods-BlueWalletUITests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6F02C2F7CA3591E4E0B06EBA /* libPods-BlueWalletUITests.a */; }; - C978A716948AB7DEC5B6F677 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; }; + C978A716948AB7DEC5B6F677 /* (null) in Frameworks */ = {isa = PBXBuildFile; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -445,9 +446,8 @@ B49038D82B8FBAD300A8164A /* BlueWalletUITest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlueWalletUITest.swift; sourceTree = ""; }; B4A29A452B55C990002A67DF /* BlueWallet.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BlueWallet.app; sourceTree = BUILT_PRODUCTS_DIR; }; B4A29A462B55C990002A67DF /* BlueWallet-NoLDK.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "BlueWallet-NoLDK.plist"; sourceTree = ""; }; - B4AB21062B61D8CA0080440C /* SplashScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreen.swift; sourceTree = ""; }; - B4AB21082B61DC3F0080440C /* SplashScreen.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SplashScreen.m; sourceTree = ""; }; B4AB225C2B02AD12001F4328 /* XMLParserDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XMLParserDelegate.swift; sourceTree = ""; }; + B4B1A4612BFA73110072E3BB /* WidgetHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetHelper.swift; sourceTree = ""; }; B4D3235A177F4580BA52F2F9 /* libRNCSlider.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNCSlider.a; sourceTree = ""; }; B642AFB13483418CAB6FF25E /* libRCTQRCodeLocalImage.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRCTQRCodeLocalImage.a; sourceTree = ""; }; B68F8552DD4428F64B11DCFB /* Pods-BlueWallet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BlueWallet.debug.xcconfig"; path = "Target Support Files/Pods-BlueWallet/Pods-BlueWallet.debug.xcconfig"; sourceTree = ""; }; @@ -474,7 +474,7 @@ files = ( 782F075B5DD048449E2DECE9 /* libz.tbd in Frameworks */, 764B49B1420D4AEB8109BF62 /* libsqlite3.0.tbd in Frameworks */, - C978A716948AB7DEC5B6F677 /* BuildFile in Frameworks */, + C978A716948AB7DEC5B6F677 /* (null) in Frameworks */, 773E382FE62E836172AAB98B /* libPods-BlueWallet.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -709,6 +709,7 @@ 83CBB9F61A601CBA00E9B192 = { isa = PBXGroup; children = ( + B4B1A4612BFA73110072E3BB /* WidgetHelper.swift */, B44033C82BCC34AC00162242 /* Shared */, B41C2E552BB3DCB8000FE097 /* PrivacyInfo.xcprivacy */, B4549F2E2B80FEA1002E3153 /* ci_scripts */, @@ -865,8 +866,6 @@ B4AB21052B61D8890080440C /* SplashScreen */ = { isa = PBXGroup; children = ( - B4AB21062B61D8CA0080440C /* SplashScreen.swift */, - B4AB21082B61DC3F0080440C /* SplashScreen.m */, ); name = SplashScreen; sourceTree = ""; @@ -1116,7 +1115,7 @@ ); mainGroup = 83CBB9F61A601CBA00E9B192; packageReferences = ( - 6DFC806E24EA0B6C007B8700 /* XCRemoteSwiftPackageReference "EFQRCode.git" */, + 6DFC806E24EA0B6C007B8700 /* XCRemoteSwiftPackageReference "EFQRCode" */, B41B76832B66B2FF002C48D5 /* XCRemoteSwiftPackageReference "bugsnag-cocoa" */, ); productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; @@ -1502,9 +1501,7 @@ files = ( B44033E92BCC371A00162242 /* MarketData.swift in Sources */, B44033CA2BCC350A00162242 /* Currency.swift in Sources */, - B4AB21092B61DC3F0080440C /* SplashScreen.m in Sources */, B44033EE2BCC374500162242 /* Numeric+abbreviated.swift in Sources */, - B4AB21072B61D8CA0080440C /* SplashScreen.swift in Sources */, B44033DD2BCC36C300162242 /* LatestTransaction.swift in Sources */, 6D32C5C62596CE3A008C077C /* EventEmitter.m in Sources */, B44033FE2BCC37D700162242 /* MarketAPI.swift in Sources */, @@ -1516,6 +1513,7 @@ B44034072BCC38A000162242 /* FiatUnit.swift in Sources */, B44034002BCC37F800162242 /* Bundle+decode.swift in Sources */, B44033E22BCC36CB00162242 /* Placeholders.swift in Sources */, + B4B1A4622BFA73110072E3BB /* WidgetHelper.swift in Sources */, B44033DA2BCC369A00162242 /* Colors.swift in Sources */, B44033D32BCC368800162242 /* UserDefaultsGroupKey.swift in Sources */, 32B5A32A2334450100F8D608 /* Bridge.swift in Sources */, @@ -1536,6 +1534,7 @@ B40FC3FA29CCD1D00007EBAC /* SwiftTCPClient.swift in Sources */, 6DD410A1266CADF10087DE03 /* Widgets.swift in Sources */, 6DD410AC266CAE470087DE03 /* PriceWidget.swift in Sources */, + B4B1A4642BFA73110072E3BB /* WidgetHelper.swift in Sources */, B44033D52BCC368800162242 /* UserDefaultsGroupKey.swift in Sources */, 6DD410B2266CAF5C0087DE03 /* WalletInformationView.swift in Sources */, B44034022BCC37F800162242 /* Bundle+decode.swift in Sources */, @@ -1632,6 +1631,7 @@ B44033C72BCC332400162242 /* Balance.swift in Sources */, B44033FC2BCC379200162242 /* WidgetDataStore.swift in Sources */, B44033C22BCC32F800162242 /* BitcoinUnit.swift in Sources */, + B4B1A4652BFA73110072E3BB /* WidgetHelper.swift in Sources */, B44033F12BCC374500162242 /* Numeric+abbreviated.swift in Sources */, B44034032BCC37F800162242 /* Bundle+decode.swift in Sources */, B44033CD2BCC350A00162242 /* Currency.swift in Sources */, @@ -1744,7 +1744,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1703136666; + CURRENT_PROJECT_VERSION = 1703136669; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = A7W54YZ4WU; @@ -1769,7 +1769,7 @@ "$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(inherited)", ); - MARKETING_VERSION = 6.6.6; + MARKETING_VERSION = 6.6.7; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -1804,7 +1804,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1703136666; + CURRENT_PROJECT_VERSION = 1703136669; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = A7W54YZ4WU; @@ -1824,7 +1824,7 @@ "$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(inherited)", ); - MARKETING_VERSION = 6.6.6; + MARKETING_VERSION = 6.6.7; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -1860,7 +1860,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1703136666; + CURRENT_PROJECT_VERSION = 1703136669; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = ""; @@ -1873,7 +1873,7 @@ "$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(inherited)", ); - MARKETING_VERSION = 6.6.6; + MARKETING_VERSION = 6.6.7; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES; @@ -1903,7 +1903,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1703136666; + CURRENT_PROJECT_VERSION = 1703136669; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = ""; @@ -1916,7 +1916,7 @@ "$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(inherited)", ); - MARKETING_VERSION = 6.6.6; + MARKETING_VERSION = 6.6.7; MTL_FAST_MATH = YES; PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES; PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.Stickers; @@ -1947,7 +1947,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Mac Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1703136666; + CURRENT_PROJECT_VERSION = 1703136669; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = ""; @@ -1966,7 +1966,7 @@ "$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(inherited)", ); - MARKETING_VERSION = 6.6.6; + MARKETING_VERSION = 6.6.7; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES; @@ -2003,7 +2003,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Mac Developer"; CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1703136666; + CURRENT_PROJECT_VERSION = 1703136669; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = ""; @@ -2022,7 +2022,7 @@ "$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(inherited)", ); - MARKETING_VERSION = 6.6.6; + MARKETING_VERSION = 6.6.7; MTL_FAST_MATH = YES; PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES; PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.MarketWidget; @@ -2175,7 +2175,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1703136666; + CURRENT_PROJECT_VERSION = 1703136669; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = ""; @@ -2192,7 +2192,7 @@ "$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(inherited)", ); - MARKETING_VERSION = 6.6.6; + MARKETING_VERSION = 6.6.7; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES; @@ -2225,7 +2225,7 @@ "CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1703136666; + CURRENT_PROJECT_VERSION = 1703136669; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = ""; @@ -2242,7 +2242,7 @@ "$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(inherited)", ); - MARKETING_VERSION = 6.6.6; + MARKETING_VERSION = 6.6.7; MTL_FAST_MATH = YES; PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES; PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.watch.extension; @@ -2274,7 +2274,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1703136666; + CURRENT_PROJECT_VERSION = 1703136669; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = ""; @@ -2287,7 +2287,7 @@ "$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(inherited)", ); - MARKETING_VERSION = 6.6.6; + MARKETING_VERSION = 6.6.7; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES; @@ -2322,7 +2322,7 @@ "CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1703136666; + CURRENT_PROJECT_VERSION = 1703136669; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = ""; @@ -2335,7 +2335,7 @@ "$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(inherited)", ); - MARKETING_VERSION = 6.6.6; + MARKETING_VERSION = 6.6.7; MTL_FAST_MATH = YES; PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES; PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.watch; @@ -2366,7 +2366,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1703136666; + CURRENT_PROJECT_VERSION = 1703136669; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = A7W54YZ4WU; @@ -2417,7 +2417,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1703136666; + CURRENT_PROJECT_VERSION = 1703136669; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = A7W54YZ4WU; @@ -2456,7 +2456,7 @@ CODE_SIGN_ENTITLEMENTS = BlueWallet/BlueWallet.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1703136666; + CURRENT_PROJECT_VERSION = 1703136669; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = A7W54YZ4WU; ENABLE_BITCODE = NO; @@ -2480,7 +2480,7 @@ "$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(inherited)", ); - MARKETING_VERSION = 6.6.6; + MARKETING_VERSION = 6.6.7; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -2510,7 +2510,7 @@ CODE_SIGN_ENTITLEMENTS = BlueWallet/BlueWalletRelease.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1703136666; + CURRENT_PROJECT_VERSION = 1703136669; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = A7W54YZ4WU; ENABLE_BITCODE = NO; @@ -2529,7 +2529,7 @@ "$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(inherited)", ); - MARKETING_VERSION = 6.6.6; + MARKETING_VERSION = 6.6.7; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -2627,7 +2627,7 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 6DFC806E24EA0B6C007B8700 /* XCRemoteSwiftPackageReference "EFQRCode.git" */ = { + 6DFC806E24EA0B6C007B8700 /* XCRemoteSwiftPackageReference "EFQRCode" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/EFPrefix/EFQRCode.git"; requirement = { @@ -2648,7 +2648,7 @@ /* Begin XCSwiftPackageProductDependency section */ 6DFC806F24EA0B6C007B8700 /* EFQRCode */ = { isa = XCSwiftPackageProductDependency; - package = 6DFC806E24EA0B6C007B8700 /* XCRemoteSwiftPackageReference "EFQRCode.git" */; + package = 6DFC806E24EA0B6C007B8700 /* XCRemoteSwiftPackageReference "EFQRCode" */; productName = EFQRCode; }; B41B76842B66B2FF002C48D5 /* Bugsnag */ = { diff --git a/ios/BlueWallet/AppDelegate.mm b/ios/BlueWallet/AppDelegate.mm index 0785bc770..0a2cad0c7 100644 --- a/ios/BlueWallet/AppDelegate.mm +++ b/ios/BlueWallet/AppDelegate.mm @@ -9,10 +9,11 @@ #import "EventEmitter.h" #import #import +#import "BlueWallet-Swift.h" @interface AppDelegate() -@property (nonatomic, strong) UIView *launchScreenView; +@property (nonatomic, strong) NSUserDefaults *userDefaultsGroup; @end @@ -20,9 +21,10 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - -NSUserDefaults *group = [[NSUserDefaults alloc] initWithSuiteName:@"group.io.bluewallet.bluewallet"]; - NSString *isDoNotTrackEnabled = [group stringForKey:@"donottrack"]; + [self clearFilesIfNeeded]; + self.userDefaultsGroup = [[NSUserDefaults alloc] initWithSuiteName:@"group.io.bluewallet.bluewallet"]; + + NSString *isDoNotTrackEnabled = [self.userDefaultsGroup stringForKey:@"donottrack"]; if (![isDoNotTrackEnabled isEqualToString:@"1"]) { // Set the appType based on the current platform #if TARGET_OS_MACCATALYST @@ -41,8 +43,6 @@ NSUserDefaults *group = [[NSUserDefaults alloc] initWithSuiteName:@"group.io.blu [NSUserDefaults.standardUserDefaults setValue:@"" forKey:@"deviceUIDCopy"]; } - [self addSplashScreenView]; - self.moduleName = @"BlueWallet"; // You can add your custom initial props in the dictionary below. // They will be passed down to the ViewController used by React Native. @@ -52,26 +52,12 @@ NSUserDefaults *group = [[NSUserDefaults alloc] initWithSuiteName:@"group.io.blu UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; center.delegate = self; - + + [self setupUserDefaultsListener]; + return [super application:application didFinishLaunchingWithOptions:launchOptions]; } -- (void)addSplashScreenView -{ - // Get the rootView - RCTRootView *rootView = (RCTRootView *)self.window.rootViewController.view; - - // Capture the launch screen view - UIStoryboard *launchScreenStoryboard = [UIStoryboard storyboardWithName:@"LaunchScreen" bundle:nil]; - UIViewController *launchScreenVC = [launchScreenStoryboard instantiateInitialViewController]; - UIView *launchScreenView = launchScreenVC.view; - launchScreenView.frame = self.window.bounds; - [self.window addSubview:launchScreenView]; - - // Keep a reference to the launch screen view to remove it later - rootView.loadingView = launchScreenView; -} - - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge { #if DEBUG @@ -81,34 +67,66 @@ NSUserDefaults *group = [[NSUserDefaults alloc] initWithSuiteName:@"group.io.blu #endif } -- (void)observeValueForKeyPath:(NSString *) keyPath ofObject:(id) object change:(NSDictionary *) change context:(void *) context +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { - if([keyPath isEqual:@"deviceUID"] || [keyPath isEqual:@"deviceUIDCopy"]) - { - [self copyDeviceUID]; + if ([keyPath isEqualToString:@"deviceUID"] || [keyPath isEqualToString:@"deviceUIDCopy"]) { + [self copyDeviceUID]; + } + + NSArray *keys = @[ + @"WidgetCommunicationAllWalletsSatoshiBalance", + @"WidgetCommunicationAllWalletsLatestTransactionTime", + @"WidgetCommunicationDisplayBalanceAllowed", + @"WidgetCommunicationLatestTransactionIsUnconfirmed", + @"preferredCurrency", + @"preferredCurrencyLocale", + @"electrum_host", + @"electrum_tcp_port", + @"electrum_ssl_port" + ]; + + if ([keys containsObject:keyPath]) { + [WidgetHelper reloadAllWidgets]; } } - (void)copyDeviceUID { - [[NSUserDefaults standardUserDefaults] addObserver:self + [NSUserDefaults.standardUserDefaults addObserver:self forKeyPath:@"deviceUID" options:NSKeyValueObservingOptionNew context:NULL]; - [[NSUserDefaults standardUserDefaults] addObserver:self + [NSUserDefaults.standardUserDefaults addObserver:self forKeyPath:@"deviceUIDCopy" options:NSKeyValueObservingOptionNew context:NULL]; - NSString *deviceUID = [[NSUserDefaults standardUserDefaults] stringForKey:@"deviceUID"]; - if (deviceUID && deviceUID.length > 0) { - [NSUserDefaults.standardUserDefaults setValue:deviceUID forKey:@"deviceUIDCopy"]; - } + NSString *deviceUID = [NSUserDefaults.standardUserDefaults stringForKey:@"deviceUID"]; + if (deviceUID && deviceUID.length > 0) { + [NSUserDefaults.standardUserDefaults setValue:deviceUID forKey:@"deviceUIDCopy"]; + } +} + +- (void)setupUserDefaultsListener { + NSArray *keys = @[ + @"WidgetCommunicationAllWalletsSatoshiBalance", + @"WidgetCommunicationAllWalletsLatestTransactionTime", + @"WidgetCommunicationDisplayBalanceAllowed", + @"WidgetCommunicationLatestTransactionIsUnconfirmed", + @"preferredCurrency", + @"preferredCurrencyLocale", + @"electrum_host", + @"electrum_tcp_port", + @"electrum_ssl_port" + ]; + + for (NSString *key in keys) { + [self.userDefaultsGroup addObserver:self forKeyPath:key options:NSKeyValueObservingOptionNew context:NULL]; + } } - (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray> * _Nullable))restorationHandler { - NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.io.bluewallet.bluewallet"]; - [defaults setValue:@{@"activityType": userActivity.activityType, @"userInfo": userActivity.userInfo} forKey:@"onUserActivityOpen"]; + [self.userDefaultsGroup setValue:@{@"activityType": userActivity.activityType, @"userInfo": userActivity.userInfo} forKey:@"onUserActivityOpen"]; if (userActivity.activityType == NSUserActivityTypeBrowsingWeb) { return [RCTLinkingManager application:application continueUserActivity:userActivity @@ -129,8 +147,7 @@ NSUserDefaults *group = [[NSUserDefaults alloc] initWithSuiteName:@"group.io.blu } - (void)applicationWillTerminate:(UIApplication *)application { - NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.io.bluewallet.bluewallet"]; - [defaults removeObjectForKey:@"onUserActivityOpen"]; + [self.userDefaultsGroup removeObjectForKey:@"onUserActivityOpen"]; } - (void)application:(UIApplication *)application performActionForShortcutItem:(UIApplicationShortcutItem *)shortcutItem completionHandler:(void (^)(BOOL succeeded)) completionHandler { @@ -152,8 +169,8 @@ NSUserDefaults *group = [[NSUserDefaults alloc] initWithSuiteName:@"group.io.blu [builder removeMenuForIdentifier:UIMenuToolbar]; // File -> Add Wallet (Command + A) - UIKeyCommand *addWalletCommand = [UIKeyCommand keyCommandWithInput:@"A" - modifierFlags:UIKeyModifierCommand | UIKeyModifierShift + UIKeyCommand *addWalletCommand = [UIKeyCommand keyCommandWithInput:@"A" + modifierFlags:UIKeyModifierCommand | UIKeyModifierShift action:@selector(addWalletAction:)]; [addWalletCommand setTitle:@"Add Wallet"]; @@ -184,35 +201,29 @@ NSUserDefaults *group = [[NSUserDefaults alloc] initWithSuiteName:@"group.io.blu [builder insertSiblingMenu:settings afterMenuForIdentifier:UIMenuAbout]; } - - (void)openSettings:(UIKeyCommand *)keyCommand { [EventEmitter.sharedInstance openSettings]; } - (void)addWalletAction:(UIKeyCommand *)keyCommand { // Implement the functionality for adding a wallet - [EventEmitter.sharedInstance addWalletMenuAction]; - + [EventEmitter.sharedInstance addWalletMenuAction]; NSLog(@"Add Wallet action performed"); } - (void)importWalletAction:(UIKeyCommand *)keyCommand { // Implement the functionality for adding a wallet - [EventEmitter.sharedInstance importWalletMenuAction]; - + [EventEmitter.sharedInstance importWalletMenuAction]; NSLog(@"Import Wallet action performed"); } - (void)reloadTransactionsAction:(UIKeyCommand *)keyCommand { // Implement the functionality for adding a wallet - [EventEmitter.sharedInstance reloadTransactionsMenuAction]; - + [EventEmitter.sharedInstance reloadTransactionsMenuAction]; NSLog(@"Reload Transactions action performed"); } - - --(void)showHelp:(id)sender { +- (void)showHelp:(id)sender { [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://bluewallet.io/docs"] options:@{} completionHandler:nil]; } @@ -248,4 +259,63 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response [RNCPushNotificationIOS didReceiveNotificationResponse:response]; } +// Clear cache on app launch +- (void)clearFilesIfNeeded { + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + BOOL shouldClearFiles = [defaults boolForKey:@"clearFilesOnLaunch"]; + + if (shouldClearFiles) { + [self clearDocumentDirectory]; + [self clearCacheDirectory]; + [self clearTempDirectory]; + + // Reset the switch + [defaults setBool:NO forKey:@"clearFilesOnLaunch"]; + [defaults synchronize]; + + // Show an alert + UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Cache Cleared" + message:@"The document, cache, and temp directories have been cleared." + preferredStyle:UIAlertControllerStyleAlert]; + UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]; + [alert addAction:okAction]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [self.window.rootViewController presentViewController:alert animated:YES completion:nil]; + }); + } +} + +- (void)clearDocumentDirectory { + NSURL *documentsDirectory = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject]; + [self clearDirectoryAtURL:documentsDirectory]; +} + +- (void)clearCacheDirectory { + NSURL *cacheDirectory = [[[NSFileManager defaultManager] URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask] lastObject]; + [self clearDirectoryAtURL:cacheDirectory]; +} + +- (void)clearTempDirectory { + NSURL *tempDirectory = [NSURL fileURLWithPath:NSTemporaryDirectory() isDirectory:YES]; + [self clearDirectoryAtURL:tempDirectory]; +} + +- (void)clearDirectoryAtURL:(NSURL *)directoryURL { + NSError *error; + NSArray *contents = [[NSFileManager defaultManager] contentsOfDirectoryAtURL:directoryURL includingPropertiesForKeys:nil options:0 error:&error]; + + if (error) { + NSLog(@"Error reading contents of directory: %@", error.localizedDescription); + return; + } + + for (NSURL *fileURL in contents) { + [[NSFileManager defaultManager] removeItemAtURL:fileURL error:&error]; + if (error) { + NSLog(@"Error removing file: %@", error.localizedDescription); + } + } +} + @end diff --git a/ios/Podfile.lock b/ios/Podfile.lock index f5e9994ad..e93b60699 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,6 +1,6 @@ PODS: - boost (1.76.0) - - BugsnagReactNative (7.22.7): + - BugsnagReactNative (7.23.0): - React-Core - BVLinearGradient (2.8.3): - React-Core @@ -334,7 +334,7 @@ PODS: - React-Core - react-native-document-picker (9.1.1): - React-Core - - react-native-idle-timer (2.1.6): + - react-native-idle-timer (2.2.2): - React-Core - react-native-image-picker (7.1.2): - RCT-Folly (= 2021.07.22.00) @@ -352,8 +352,6 @@ PODS: - react-native-tcp-socket (6.0.6): - CocoaAsyncSocket - React-Core - - react-native-widget-center (0.0.9): - - React - React-NativeModulesApple (0.72.14): - hermes-engine - React-callinvoker @@ -466,7 +464,7 @@ PODS: - React-perflogger (= 0.72.14) - ReactNativeCameraKit (13.0.0): - React-Core - - RealmJS (12.8.0): + - RealmJS (12.8.1): - React - rn-ldk (0.8.4): - React-Core @@ -478,7 +476,7 @@ PODS: - React-Core - RNDefaultPreference (1.4.4): - React-Core - - RNDeviceInfo (10.13.2): + - RNDeviceInfo (10.14.0): - React-Core - RNFS (2.20.0): - React-Core @@ -509,9 +507,9 @@ PODS: - RCT-Folly (= 2021.07.22.00) - React-Core - React-RCTImage - - RNShare (10.2.0): + - RNShare (10.2.1): - React-Core - - RNSVG (13.14.0): + - RNSVG (13.14.1): - React-Core - RNVectorIcons (10.1.0): - RCT-Folly (= 2021.07.22.00) @@ -561,7 +559,6 @@ DEPENDENCIES: - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) - react-native-secure-key-store (from `../node_modules/react-native-secure-key-store`) - react-native-tcp-socket (from `../node_modules/react-native-tcp-socket`) - - react-native-widget-center (from `../node_modules/react-native-widget-center`) - React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) @@ -688,8 +685,6 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-secure-key-store" react-native-tcp-socket: :path: "../node_modules/react-native-tcp-socket" - react-native-widget-center: - :path: "../node_modules/react-native-widget-center" React-NativeModulesApple: :path: "../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios" React-perflogger: @@ -777,7 +772,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 7dcd2de282d72e344012f7d6564d024930a6a440 - BugsnagReactNative: 7cc5c927f6a0b00a8e3cc7157dab4cc94a4bc575 + BugsnagReactNative: 079e8ede687b76bd8b661acd55bc5c888af56dc7 BVLinearGradient: 880f91a7854faff2df62518f0281afb1c60d49a3 CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 @@ -809,7 +804,7 @@ SPEC CHECKSUMS: react-native-blue-crypto: 23f1558ad3d38d7a2edb7e2f6ed1bc520ed93e56 react-native-bw-file-access: b232fd1d902521ca046f3fc5990ab1465e1878d7 react-native-document-picker: 3599b238843369026201d2ef466df53f77ae0452 - react-native-idle-timer: f7f651542b39dce9b9473e4578cb64a255075f17 + react-native-idle-timer: ee2053f2cd458f6fef1db7bebe5098ca281cce07 react-native-image-picker: 1889c342e6a4ba089ff11ae0c3bf5cc30a3134d0 react-native-ios-context-menu: e529171ba760a1af7f2ef0729f5a7f4d226171c5 react-native-qrcode-local-image: 35ccb306e4265bc5545f813e54cc830b5d75bcfc @@ -817,7 +812,6 @@ SPEC CHECKSUMS: react-native-safe-area-context: dcab599c527c2d7de2d76507a523d20a0b83823d react-native-secure-key-store: 910e6df6bc33cb790aba6ee24bc7818df1fe5898 react-native-tcp-socket: e724380c910c2e704816ec817ed28f1342246ff7 - react-native-widget-center: 12dfba20a4fa995850b52cf0afecf734397f4b9c React-NativeModulesApple: 3107f777453f953906d9ba9dc5f8cbd91a6ef913 React-perflogger: daabc494c6328efc1784a4b49b8b74fca305d11c React-RCTActionSheet: 0e0e64a7cf6c07f1de73d1f0a92d26a70262b256 @@ -836,13 +830,13 @@ SPEC CHECKSUMS: React-utils: 22a77b05da25ce49c744faa82e73856dcae1734e ReactCommon: ff94462e007c568d8cdebc32e3c97af86ec93bb5 ReactNativeCameraKit: 9d46a5d7dd544ca64aa9c03c150d2348faf437eb - RealmJS: 3e6010ae878227830e947f40f996e13ccab4c8ba + RealmJS: 2c7fdb3991d7655fba5f88eb288f75eaf5cb9980 rn-ldk: 0d8749d98cc5ce67302a32831818c116b67f7643 RNCAsyncStorage: 826b603ae9c0f88b5ac4e956801f755109fa4d5c RNCClipboard: 0a720adef5ec193aa0e3de24c3977222c7e52a37 RNCPushNotificationIOS: 64218f3c776c03d7408284a819b2abfda1834bc8 RNDefaultPreference: 08bdb06cfa9188d5da97d4642dac745218d7fb31 - RNDeviceInfo: 42aadf1282ffa0a88dc38a504a7be145eb010dfa + RNDeviceInfo: 59344c19152c4b2b32283005f9737c5c64b42fba RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 982741f345785f2927e7b28f67dc83679cf3bfc8 RNHandoff: d3b0754cca3a6bcd9b25f544f733f7f033ccf5fa @@ -855,8 +849,8 @@ SPEC CHECKSUMS: RNReactNativeHapticFeedback: ec56a5f81c3941206fd85625fa669ffc7b4545f9 RNReanimated: d4f25b2a931c4f0b2bb12173a3096f02ea4cfb05 RNScreens: b8d370282cdeae9df85dd5eab20c88eb5181243b - RNShare: 554a91f5cfbe4adac4cfe3654826ee8b299fe365 - RNSVG: d00c8f91c3cbf6d476451313a18f04d220d4f396 + RNShare: 0fad69ae2d71de9d1f7b9a43acf876886a6cb99c + RNSVG: af3907ac5d4fa26a862b75a16d8f15bc74f2ceda RNVectorIcons: 32462e7c7e58fe457474fc79c4d7de3f0ef08d70 RNWatch: fd30ca40a5b5ef58dcbc195638e68219bc455236 SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 diff --git a/ios/Settings.bundle/Root.plist b/ios/Settings.bundle/Root.plist index ff555eead..0de1f5da9 100644 --- a/ios/Settings.bundle/Root.plist +++ b/ios/Settings.bundle/Root.plist @@ -12,16 +12,32 @@ FooterText Provide this Unique ID when reporting an issue Title - + Report Issue Type PSTextFieldSpecifier Title Unique ID - Key + Key deviceUIDCopy + + Type + PSGroupSpecifier + Title + Cache + + + Type + PSToggleSwitchSpecifier + Title + Clear Cache on Next Launch + Key + clearFilesOnLaunch + DefaultValue + + diff --git a/ios/SplashScreen.m b/ios/SplashScreen.m deleted file mode 100644 index 091048ffc..000000000 --- a/ios/SplashScreen.m +++ /dev/null @@ -1,14 +0,0 @@ -// -// SplashScreen.m -// BlueWallet -// -// Created by Marcos Rodriguez on 1/24/24. -// Copyright © 2024 BlueWallet. All rights reserved. -// - -#import - -@interface RCT_EXTERN_MODULE(SplashScreen, NSObject) -RCT_EXTERN_METHOD(addObserver) -RCT_EXTERN_METHOD(dismissSplashScreen) -@end diff --git a/ios/SplashScreen.swift b/ios/SplashScreen.swift deleted file mode 100644 index 6f70d3ca7..000000000 --- a/ios/SplashScreen.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// SplashScreen.swift -// BlueWallet -// -// Created by Marcos Rodriguez on 1/24/24. -// Copyright © 2024 BlueWallet. All rights reserved. -// - -import Foundation -import React - -@objc(SplashScreen) -class SplashScreen: NSObject, RCTBridgeModule { - static func moduleName() -> String! { - return "SplashScreen" - } - - static func requiresMainQueueSetup() -> Bool { - return true - } - - @objc - func addObserver() { - NotificationCenter.default.addObserver(self, selector: #selector(dismissSplashScreen), name: NSNotification.Name("HideSplashScreen"), object: nil) - } - - @objc - func dismissSplashScreen() { - DispatchQueue.main.async { - if let rootView = UIApplication.shared.delegate?.window??.rootViewController?.view as? RCTRootView { - rootView.loadingView?.removeFromSuperview() - rootView.loadingView = nil - } - NotificationCenter.default.removeObserver(self, name: NSNotification.Name("HideSplashScreen"), object: nil) - } - } - - -} diff --git a/ios/WidgetHelper.swift b/ios/WidgetHelper.swift new file mode 100644 index 000000000..37065bbda --- /dev/null +++ b/ios/WidgetHelper.swift @@ -0,0 +1,11 @@ +import WidgetKit + +@objc class WidgetHelper: NSObject { + @objc static func reloadAllWidgets() { + if #available(iOS 14.0, *) { + WidgetCenter.shared.reloadAllTimelines() + } else { + // Fallback on earlier versions + } + } +} diff --git a/loc/en.json b/loc/en.json index 92f51e6cc..d6797d274 100644 --- a/loc/en.json +++ b/loc/en.json @@ -277,6 +277,7 @@ "encrypt_title": "Security", "encrypt_tstorage": "Storage", "encrypt_use": "Use {type}", + "encrypted_feature_disabled": "This feature cannot be used with encrypted storage enabled.", "encrypt_use_expl": "{type} will be used to confirm your identity before making a transaction, unlocking, exporting, or deleting a wallet. {type} will not be used to unlock encrypted storage.", "biometrics_fail": "If {type} is not enabled, or fails to unlock, you can use your device passcode as an alternative.", "general": "General", @@ -615,10 +616,22 @@ }, "bip47": { "payment_code": "Payment Code", - "payment_codes_list": "Payment Codes List", - "who_can_pay_me": "Who can pay me:", - "whom_can_i_pay": "Whom can I pay:", + "contacts": "Contacts", "purpose": "Reusable and shareable code (BIP47)", + "pay_this_contact": "Pay this contact", + "rename_contact": "Rename contact", + "copy_payment_code": "Copy Payment Code", + "copied": "Copied", + "rename": "Rename", + "provide_name": "Provide new name for this contact", + "add_contact": "Add Contact", + "provide_payment_code": "Provide Payment Code", + "invalid_pc": "Invalid Payment Code", + "notification_tx_unconfirmed": "Notification transaction is not confirmed yet, please wait", + "failed_create_notif_tx": "Failed to create on-chain transaction", + "onchain_tx_needed": "On-chain transaction needed", + "notif_tx_sent" : "Notification transaction sent. Please wait for it to confirm", + "notif_tx": "Notification transaction", "not_found": "Payment code not found" } } diff --git a/loc/es_419.json b/loc/es_419.json index 77b433cc5..2b1319c5e 100644 --- a/loc/es_419.json +++ b/loc/es_419.json @@ -125,7 +125,8 @@ "maxSats": "La cantidad máxima es {max} sats", "maxSatsFull": "La cantidad máxima es {max} sats o {currency}", "minSats": "La cantidad mínima es {min} sats", - "minSatsFull": "La cantidad mínima es {min} sats o {currency}" + "minSatsFull": "La cantidad mínima es {min} sats o {currency}", + "qrcode_for_the_address": "Código QR para la dirección" }, "send": { "provided_address_is_invoice": "Esta dirección parece ser para una factura Lightning. Por favor, ve a tu billetera Lightning para realizar el pago de esta factura.", @@ -166,6 +167,7 @@ "details_next": "Siguiente", "details_no_signed_tx": "El archivo seleccionado no contiene una transacción que se pueda importar.", "details_note_placeholder": "Nota personal", + "counterparty_label_placeholder": "Editar nombre de contacto", "details_scan": "Escanear", "details_scan_hint": "Toca dos veces para escanear o importar un destino", "details_total_exceeds_balance": "La cantidad de envío excede el saldo disponible.", @@ -275,6 +277,7 @@ "encrypt_title": "Seguridad", "encrypt_tstorage": "Almacenamiento", "encrypt_use": "Usar {type}", + "encrypted_feature_disabled": "Esta función no se puede utilizar con el almacenamiento cifrado habilitado.", "encrypt_use_expl": "{type} se utilizará para confirmar tu identidad antes de realizar una transacción, desbloquear, exportar o eliminar una billetera. {type} no se utilizará para desbloquear el almacenamiento encriptado.", "biometrics_fail": "Si {type} no está activado o no se desbloquea, puedes utilizar el código de acceso de tu dispositivo como alternativa.", "general": "General", @@ -340,7 +343,6 @@ "cpfp_title": "Aumentar Comisión (CPFP)", "details_balance_hide": "Ocultar Balance", "details_balance_show": "Mostrar Balance", - "details_block": "Altura del Bloque", "details_copy": "Copiar", "details_copy_amount": "Importe de la copia", "details_copy_block_explorer_link": "Copiar el enlace del explorador de bloques", @@ -351,7 +353,7 @@ "details_outputs": "Salidas", "date": "Fecha", "details_received": "Recibido", - "transaction_note_saved": "La nota de transacción se ha guardado correctamente.", + "transaction_saved": "Guardado", "details_show_in_block_explorer": "Ver en el Explorador de Bloques", "details_title": "Transacción", "details_to": "Salida", @@ -372,6 +374,8 @@ "status_cancel": "Cancelar Transacción", "transactions_count": "Número de Transacciones", "txid": "ID de Transacción", + "from": "De: {counterparty}", + "to": "A: {counterparty}", "updating": "Actualizando..." }, "wallets": { @@ -612,9 +616,22 @@ }, "bip47": { "payment_code": "Código de pago", - "payment_codes_list": "Lista de códigos de pago", - "who_can_pay_me": "Quién puede pagarme:", + "contacts": "Contactos", "purpose": "Código reutilizable y compartible (BIP47)", + "pay_this_contact": "Paga a este contacto", + "rename_contact": "Renombrar contacto", + "copy_payment_code": "Copiar código de pago", + "copied": "Copiado", + "rename": "Cambiar nombre", + "provide_name": "Proporciona un nuevo nombre para este contacto", + "add_contact": "Agregar contacto", + "provide_payment_code": "Proporciona código de pago", + "invalid_pc": "Código de pago no válido", + "notification_tx_unconfirmed": "La transacción de notificación aún no está confirmada, espera", + "failed_create_notif_tx": "No se pudo crear una transacción en cadena", + "onchain_tx_needed": "Se necesita transacción en cadena", + "notif_tx_sent" : "Transacción de notificación enviada. Espera a que se confirme", + "notif_tx": "Transacción de notificación", "not_found": "Código de pago no encontrado" } } diff --git a/navigation/DetailViewScreensStack.tsx b/navigation/DetailViewScreensStack.tsx index 6bbdb3cd2..09424c3db 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 '../Navigation'; +import { NavigationDefaultOptions, NavigationDefaultOptionsForDesktop, NavigationFormModalOptions, StatusBarLightOptions } from './'; const DetailViewRoot = createNativeStackNavigator(); const DetailViewStackScreensStack = () => { @@ -104,7 +99,7 @@ const DetailViewStackScreensStack = () => { title: '', headerBackTitle: loc.wallets.list_title, navigationBarColor: theme.colors.navigationBarColor, - headerShown: true, + headerShown: !isDesktop, headerStyle: { backgroundColor: theme.colors.customHeader, }, diff --git a/navigation/LazyLoadPaymentCodeStack.tsx b/navigation/LazyLoadPaymentCodeStack.tsx index c0ddc0419..74a3bc0a3 100644 --- a/navigation/LazyLoadPaymentCodeStack.tsx +++ b/navigation/LazyLoadPaymentCodeStack.tsx @@ -1,8 +1,8 @@ import React, { lazy, Suspense } from 'react'; import { LazyLoadingIndicator } from './LazyLoadingIndicator'; -const PaymentCode = lazy(() => import('../screen/wallets/paymentCode')); -const PaymentCodesList = lazy(() => import('../screen/wallets/paymentCodesList')); +const PaymentCode = lazy(() => import('../screen/wallets/PaymentCode')); +const PaymentCodesList = lazy(() => import('../screen/wallets/PaymentCodesList')); export const PaymentCodeComponent = () => ( }> diff --git a/navigation/LazyLoadReorderWalletsStack.tsx b/navigation/LazyLoadReorderWalletsStack.tsx deleted file mode 100644 index 39f985456..000000000 --- a/navigation/LazyLoadReorderWalletsStack.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React, { lazy, Suspense } from 'react'; -import { LazyLoadingIndicator } from './LazyLoadingIndicator'; - -const ReorderWallets = lazy(() => import('../screen/wallets/reorderWallets')); - -export const ReorderWalletsComponent = () => ( - }> - - -); diff --git a/navigation/MasterView.tsx b/navigation/MasterView.tsx new file mode 100644 index 000000000..9713fa418 --- /dev/null +++ b/navigation/MasterView.tsx @@ -0,0 +1,22 @@ +import 'react-native-gesture-handler'; // should be on top +import React, { Suspense, lazy } from 'react'; +import MainRoot from '../navigation'; +import { useStorage } from '../blue_modules/storage-context'; +const CompanionDelegates = lazy(() => import('../components/CompanionDelegates')); + +const MasterView = () => { + const { walletsInitialized } = useStorage(); + + return ( + <> + + {walletsInitialized && ( + + + + )} + + ); +}; + +export default MasterView; diff --git a/navigation/PaymentCodeStack.tsx b/navigation/PaymentCodeStack.tsx index 0487df40e..f12078d05 100644 --- a/navigation/PaymentCodeStack.tsx +++ b/navigation/PaymentCodeStack.tsx @@ -2,6 +2,8 @@ import React from 'react'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { PaymentCodeComponent, PaymentCodesListComponent } from './LazyLoadPaymentCodeStack'; import loc from '../loc'; // Assuming 'loc' is used for localization +import navigationStyle from '../components/navigationStyle'; +import { useTheme } from '../components/themes'; export type PaymentCodeStackParamList = { PaymentCode: { paymentCode: string }; @@ -11,10 +13,20 @@ export type PaymentCodeStackParamList = { const Stack = createNativeStackNavigator(); const PaymentCodeStackRoot = () => { + const theme = useTheme(); + return ( - - + + ); }; diff --git a/navigation/ReorderWalletsStack.tsx b/navigation/ReorderWalletsStack.tsx index defbd53cb..076f4b363 100644 --- a/navigation/ReorderWalletsStack.tsx +++ b/navigation/ReorderWalletsStack.tsx @@ -1,9 +1,9 @@ import React from 'react'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; -import { ReorderWalletsComponent } from './LazyLoadReorderWalletsStack'; import { useTheme } from '../components/themes'; import navigationStyle from '../components/navigationStyle'; import loc from '../loc'; +import ReorderWallets from '../screen/wallets/reorderWallets'; const Stack = createNativeStackNavigator(); @@ -14,7 +14,7 @@ const ReorderWalletsStackRoot = () => { 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, diff --git a/package-lock.json b/package-lock.json index f0821c74e..47228ce7d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,17 @@ { "name": "bluewallet", - "version": "6.6.6", + "version": "6.6.7", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "bluewallet", - "version": "6.6.6", + "version": "6.6.7", "hasInstallScript": true, "license": "MIT", "dependencies": { "@babel/preset-env": "^7.20.0", - "@bugsnag/react-native": "7.22.7", + "@bugsnag/react-native": "7.23.0", "@bugsnag/source-maps": "2.3.3", "@keystonehq/bc-ur-registry": "0.6.4", "@ngraveio/bc-ur": "1.1.12", @@ -63,7 +63,7 @@ "react-native-camera-kit": "13.0.0", "react-native-crypto": "2.2.0", "react-native-default-preference": "1.4.4", - "react-native-device-info": "10.13.2", + "react-native-device-info": "10.14.0", "react-native-document-picker": "https://github.com/BlueWallet/react-native-document-picker#6033c4e1b0dd0a6760b5f5a5a2c3b2e5d07f2ae4", "react-native-draggable-flatlist": "github:BlueWallet/react-native-draggable-flatlist#ebfddc4", "react-native-elements": "3.4.3", @@ -71,7 +71,7 @@ "react-native-gesture-handler": "2.16.2", "react-native-handoff": "https://github.com/BlueWallet/react-native-handoff#31d005f93d31099d0e564590a3bbd052b8a02b39", "react-native-haptic-feedback": "2.2.0", - "react-native-idle-timer": "https://github.com/BlueWallet/react-native-idle-timer#8587876d68ab5920e79619726aeca9e672beaf2b", + "react-native-idle-timer": "https://github.com/BlueWallet/react-native-idle-timer#7300b637c465c86e8db874c442e687950111da40", "react-native-image-picker": "7.1.2", "react-native-ios-context-menu": "github:BlueWallet/react-native-ios-context-menu#v1.15.3", "react-native-keychain": "8.2.0", @@ -92,14 +92,13 @@ "react-native-safe-area-context": "4.10.1", "react-native-screens": "3.31.1", "react-native-secure-key-store": "https://github.com/BlueWallet/react-native-secure-key-store#2076b48", - "react-native-share": "10.2.0", - "react-native-svg": "13.14.0", + "react-native-share": "10.2.1", + "react-native-svg": "13.14.1", "react-native-tcp-socket": "6.0.6", "react-native-vector-icons": "10.1.0", "react-native-watch-connectivity": "1.1.0", - "react-native-widget-center": "https://github.com/BlueWallet/react-native-widget-center#a128c38", "readable-stream": "3.6.2", - "realm": "12.8.0", + "realm": "12.8.1", "rn-ldk": "github:BlueWallet/rn-ldk#v0.8.4", "rn-nodeify": "10.3.0", "scryptsy": "2.1.0", @@ -2250,17 +2249,19 @@ } }, "node_modules/@bugsnag/plugin-react-native-unhandled-rejection": { - "version": "7.22.7", - "resolved": "https://registry.npmjs.org/@bugsnag/plugin-react-native-unhandled-rejection/-/plugin-react-native-unhandled-rejection-7.22.7.tgz", - "integrity": "sha512-xmFpUPYrQxwsr9RJ1HTu9lfNUbAHM+hIyUEshg+/Wfj/1Zvnkr0AnkqRWbQFqkOBklzYI4s7maJvm4S2go/KOQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@bugsnag/plugin-react-native-unhandled-rejection/-/plugin-react-native-unhandled-rejection-7.23.0.tgz", + "integrity": "sha512-z0Nlqir3nnBcXVffw8uau12SS7vVUu0yS65SS5uWUn9cNIwNSTqZ/40pHVGh1VQBbpXlYrw7RVbPtufWmS20EA==", + "license": "MIT", "peerDependencies": { "@bugsnag/core": "^7.0.0" } }, "node_modules/@bugsnag/react-native": { - "version": "7.22.7", - "resolved": "https://registry.npmjs.org/@bugsnag/react-native/-/react-native-7.22.7.tgz", - "integrity": "sha512-vHmynQj7rzPW+1v8aK41G9T5HSaXipgFkkCmczOiFG9YYNzVKeaPcbwcS6Z6+tLZ55ZQeJdupfezcmj4rnAZVw==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@bugsnag/react-native/-/react-native-7.23.0.tgz", + "integrity": "sha512-4xw0BDUBYPYxxBM0rbRr+uI+8IA/22p2JdzW+DMzLjue9e5PY+dTGZgREllGLuE27NtMBvnaA0rlWSYfxE04cQ==", + "license": "MIT", "dependencies": { "@bugsnag/core": "^7.22.7", "@bugsnag/delivery-react-native": "^7.22.7", @@ -2272,7 +2273,7 @@ "@bugsnag/plugin-react-native-global-error-handler": "^7.22.7", "@bugsnag/plugin-react-native-hermes": "^7.22.7", "@bugsnag/plugin-react-native-session": "^7.22.7", - "@bugsnag/plugin-react-native-unhandled-rejection": "^7.22.7", + "@bugsnag/plugin-react-native-unhandled-rejection": "^7.23.0", "iserror": "^0.0.2" } }, @@ -19386,9 +19387,10 @@ } }, "node_modules/react-native-device-info": { - "version": "10.13.2", - "resolved": "https://registry.npmjs.org/react-native-device-info/-/react-native-device-info-10.13.2.tgz", - "integrity": "sha512-5EAls7uvGdZkVfp1KWHsR5BfJJHp/ux64+ZPj1865IcaUyrNQIWYFmrTHwTH8L/NGJUTBrzv+y6WODnN17LSbw==", + "version": "10.14.0", + "resolved": "https://registry.npmjs.org/react-native-device-info/-/react-native-device-info-10.14.0.tgz", + "integrity": "sha512-9NnTGfhEU4UgQtz4p6COk2Gbqly0dpSWrJtp+dw5rNAi96KtYbaNnO5yoOHDlJ1SVIzh8+hFu3WxVbnWkFU9gA==", + "license": "MIT", "peerDependencies": { "react-native": "*" } @@ -19507,9 +19509,9 @@ } }, "node_modules/react-native-idle-timer": { - "version": "2.1.6", - "resolved": "git+ssh://git@github.com/BlueWallet/react-native-idle-timer.git#8587876d68ab5920e79619726aeca9e672beaf2b", - "integrity": "sha512-d24QXgHUkwu+BTp676Pb7ng7u01iiw3YBBY+DYvurkMAHRoB9c+wj32oB4sLq07W+LbETyMEuDlLUxTOasqd3Q==", + "version": "2.2.2", + "resolved": "git+ssh://git@github.com/BlueWallet/react-native-idle-timer.git#7300b637c465c86e8db874c442e687950111da40", + "integrity": "sha512-izpVaEvAXucoKpn/pooJ+vY7h2/sp6LHb+tPVCYv8WYvXbha2v6cW8R762y1ErNyJbEMj7u7IfFtSTxjeIcT9g==", "license": "MIT" }, "node_modules/react-native-image-picker": { @@ -19739,9 +19741,10 @@ "license": "ISC" }, "node_modules/react-native-share": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/react-native-share/-/react-native-share-10.2.0.tgz", - "integrity": "sha512-dn6FNoEADHdeAkBihIN4ewyxc5PO0MmvFzaSsTsLCwOs7VrX8rBNbGDE5iMP3Anz+SY7YInW2UuxVttBzii6wg==", + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/react-native-share/-/react-native-share-10.2.1.tgz", + "integrity": "sha512-Z2LWGYWH7raM4H6Oauttv1tEhaB43XSWJAN8iS6oaSG9CnyrUBeYFF4QpU1AH5RgNeylXQdN8CtbizCHHt6coQ==", + "license": "MIT", "engines": { "node": ">=16" } @@ -19755,9 +19758,10 @@ } }, "node_modules/react-native-svg": { - "version": "13.14.0", - "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-13.14.0.tgz", - "integrity": "sha512-27ZnxUkHgWICimhuj6MuqBkISN53lVvgWJB7pIypjXysAyM+nqgQBPh4vXg+7MbqLBoYvR4PiBgKfwwGAqVxHg==", + "version": "13.14.1", + "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-13.14.1.tgz", + "integrity": "sha512-0DSa0EOySzV0J9utmRFVE5Vr4mKRXA7GtH1Ga1B6fzR967HGFW1ytH6hmnOf36316hjYpXMMB7s4oVe1FqghlQ==", + "license": "MIT", "dependencies": { "css-select": "^5.1.0", "css-tree": "^1.1.3" @@ -19868,15 +19872,6 @@ "react-native": ">=0.40" } }, - "node_modules/react-native-widget-center": { - "version": "0.0.9", - "resolved": "git+ssh://git@github.com/BlueWallet/react-native-widget-center.git#a128c389526d55afdd67937494f2fec224dd0009", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.1", - "react-native": ">=0.60.0-rc.0 <1.0.x" - } - }, "node_modules/react-native/node_modules/@jest/types": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", @@ -20110,10 +20105,11 @@ "integrity": "sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg==" }, "node_modules/realm": { - "version": "12.8.0", - "resolved": "https://registry.npmjs.org/realm/-/realm-12.8.0.tgz", - "integrity": "sha512-U1w5+ncyURQFQTrshoGn3KV+pzR1rQlPT7s3Sw6HPIPVBH80EWU3mirwvqp6RQ+Qi32ctRrBMTNeGd5mzAyiSw==", + "version": "12.8.1", + "resolved": "https://registry.npmjs.org/realm/-/realm-12.8.1.tgz", + "integrity": "sha512-+zj2bvU0EACXvPBdvRsp2TybHPqPtftciTXsAlhrTWMiaoqC8FO6lriPbUs/JwsXz1w9otJXl5kXRBghPQHgLQ==", "hasInstallScript": true, + "license": "apache-2.0", "dependencies": { "@realm/fetch": "^0.1.1", "bson": "^4.7.2", @@ -24003,14 +23999,14 @@ "integrity": "sha512-p3C7m6GXh9ICnGt+m1FwWpBCiGNGdQvoTzzN0LAxT6YQdB3t2nmhqE3QIpHmXpJK1PveTCIOO2DbeSerWtUEsg==" }, "@bugsnag/plugin-react-native-unhandled-rejection": { - "version": "7.22.7", - "resolved": "https://registry.npmjs.org/@bugsnag/plugin-react-native-unhandled-rejection/-/plugin-react-native-unhandled-rejection-7.22.7.tgz", - "integrity": "sha512-xmFpUPYrQxwsr9RJ1HTu9lfNUbAHM+hIyUEshg+/Wfj/1Zvnkr0AnkqRWbQFqkOBklzYI4s7maJvm4S2go/KOQ==" + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@bugsnag/plugin-react-native-unhandled-rejection/-/plugin-react-native-unhandled-rejection-7.23.0.tgz", + "integrity": "sha512-z0Nlqir3nnBcXVffw8uau12SS7vVUu0yS65SS5uWUn9cNIwNSTqZ/40pHVGh1VQBbpXlYrw7RVbPtufWmS20EA==" }, "@bugsnag/react-native": { - "version": "7.22.7", - "resolved": "https://registry.npmjs.org/@bugsnag/react-native/-/react-native-7.22.7.tgz", - "integrity": "sha512-vHmynQj7rzPW+1v8aK41G9T5HSaXipgFkkCmczOiFG9YYNzVKeaPcbwcS6Z6+tLZ55ZQeJdupfezcmj4rnAZVw==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@bugsnag/react-native/-/react-native-7.23.0.tgz", + "integrity": "sha512-4xw0BDUBYPYxxBM0rbRr+uI+8IA/22p2JdzW+DMzLjue9e5PY+dTGZgREllGLuE27NtMBvnaA0rlWSYfxE04cQ==", "requires": { "@bugsnag/core": "^7.22.7", "@bugsnag/delivery-react-native": "^7.22.7", @@ -24022,7 +24018,7 @@ "@bugsnag/plugin-react-native-global-error-handler": "^7.22.7", "@bugsnag/plugin-react-native-hermes": "^7.22.7", "@bugsnag/plugin-react-native-session": "^7.22.7", - "@bugsnag/plugin-react-native-unhandled-rejection": "^7.22.7", + "@bugsnag/plugin-react-native-unhandled-rejection": "^7.23.0", "iserror": "^0.0.2" } }, @@ -37100,9 +37096,9 @@ "integrity": "sha512-h0vtgiSKws3UmMRJykXAVM4ne1SgfoocUcoBD19ewRpQd6wqurE0HJRQGrSxcHK5LdKE7QPSIB1VX3YGIVS8Jg==" }, "react-native-device-info": { - "version": "10.13.2", - "resolved": "https://registry.npmjs.org/react-native-device-info/-/react-native-device-info-10.13.2.tgz", - "integrity": "sha512-5EAls7uvGdZkVfp1KWHsR5BfJJHp/ux64+ZPj1865IcaUyrNQIWYFmrTHwTH8L/NGJUTBrzv+y6WODnN17LSbw==" + "version": "10.14.0", + "resolved": "https://registry.npmjs.org/react-native-device-info/-/react-native-device-info-10.14.0.tgz", + "integrity": "sha512-9NnTGfhEU4UgQtz4p6COk2Gbqly0dpSWrJtp+dw5rNAi96KtYbaNnO5yoOHDlJ1SVIzh8+hFu3WxVbnWkFU9gA==" }, "react-native-document-picker": { "version": "git+ssh://git@github.com/BlueWallet/react-native-document-picker.git#6033c4e1b0dd0a6760b5f5a5a2c3b2e5d07f2ae4", @@ -37177,9 +37173,9 @@ "integrity": "sha512-3tqJOjCguWhIrX0nkURn4yw6kXdsSDjjrvZCRjKXYGlL28hdQmoW2okAHduDTD9FWj9lA+lHgwFWgGs4aFNN7A==" }, "react-native-idle-timer": { - "version": "git+ssh://git@github.com/BlueWallet/react-native-idle-timer.git#8587876d68ab5920e79619726aeca9e672beaf2b", - "integrity": "sha512-d24QXgHUkwu+BTp676Pb7ng7u01iiw3YBBY+DYvurkMAHRoB9c+wj32oB4sLq07W+LbETyMEuDlLUxTOasqd3Q==", - "from": "react-native-idle-timer@https://github.com/BlueWallet/react-native-idle-timer#8587876d68ab5920e79619726aeca9e672beaf2b" + "version": "git+ssh://git@github.com/BlueWallet/react-native-idle-timer.git#7300b637c465c86e8db874c442e687950111da40", + "integrity": "sha512-izpVaEvAXucoKpn/pooJ+vY7h2/sp6LHb+tPVCYv8WYvXbha2v6cW8R762y1ErNyJbEMj7u7IfFtSTxjeIcT9g==", + "from": "react-native-idle-timer@https://github.com/BlueWallet/react-native-idle-timer#7300b637c465c86e8db874c442e687950111da40" }, "react-native-image-picker": { "version": "7.1.2", @@ -37334,9 +37330,9 @@ "from": "react-native-secure-key-store@https://github.com/BlueWallet/react-native-secure-key-store#2076b48" }, "react-native-share": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/react-native-share/-/react-native-share-10.2.0.tgz", - "integrity": "sha512-dn6FNoEADHdeAkBihIN4ewyxc5PO0MmvFzaSsTsLCwOs7VrX8rBNbGDE5iMP3Anz+SY7YInW2UuxVttBzii6wg==" + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/react-native-share/-/react-native-share-10.2.1.tgz", + "integrity": "sha512-Z2LWGYWH7raM4H6Oauttv1tEhaB43XSWJAN8iS6oaSG9CnyrUBeYFF4QpU1AH5RgNeylXQdN8CtbizCHHt6coQ==" }, "react-native-size-matters": { "version": "0.3.1", @@ -37344,9 +37340,9 @@ "integrity": "sha512-mKOfBLIBFBcs9br1rlZDvxD5+mAl8Gfr5CounwJtxI6Z82rGrMO+Kgl9EIg3RMVf3G855a85YVqHJL2f5EDRlw==" }, "react-native-svg": { - "version": "13.14.0", - "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-13.14.0.tgz", - "integrity": "sha512-27ZnxUkHgWICimhuj6MuqBkISN53lVvgWJB7pIypjXysAyM+nqgQBPh4vXg+7MbqLBoYvR4PiBgKfwwGAqVxHg==", + "version": "13.14.1", + "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-13.14.1.tgz", + "integrity": "sha512-0DSa0EOySzV0J9utmRFVE5Vr4mKRXA7GtH1Ga1B6fzR967HGFW1ytH6hmnOf36316hjYpXMMB7s4oVe1FqghlQ==", "requires": { "css-select": "^5.1.0", "css-tree": "^1.1.3" @@ -37420,10 +37416,6 @@ "lodash.sortby": "^4.7.0" } }, - "react-native-widget-center": { - "version": "git+ssh://git@github.com/BlueWallet/react-native-widget-center.git#a128c389526d55afdd67937494f2fec224dd0009", - "from": "react-native-widget-center@https://github.com/BlueWallet/react-native-widget-center#a128c38" - }, "react-refresh": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.4.3.tgz", @@ -37517,9 +37509,9 @@ "integrity": "sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg==" }, "realm": { - "version": "12.8.0", - "resolved": "https://registry.npmjs.org/realm/-/realm-12.8.0.tgz", - "integrity": "sha512-U1w5+ncyURQFQTrshoGn3KV+pzR1rQlPT7s3Sw6HPIPVBH80EWU3mirwvqp6RQ+Qi32ctRrBMTNeGd5mzAyiSw==", + "version": "12.8.1", + "resolved": "https://registry.npmjs.org/realm/-/realm-12.8.1.tgz", + "integrity": "sha512-+zj2bvU0EACXvPBdvRsp2TybHPqPtftciTXsAlhrTWMiaoqC8FO6lriPbUs/JwsXz1w9otJXl5kXRBghPQHgLQ==", "requires": { "@realm/fetch": "^0.1.1", "bson": "^4.7.2", diff --git a/package.json b/package.json index 4494315b2..67c324a70 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bluewallet", - "version": "6.6.6", + "version": "6.6.7", "license": "MIT", "repository": { "type": "git", @@ -56,7 +56,7 @@ "android:clean": "cd android; ./gradlew clean ; cd .. ; npm run android", "ios": "react-native run-ios", "postinstall": "rn-nodeify --install buffer,events,process,stream,inherits,path,assert,crypto --hack; npm run releasenotes2json; npm run branch2json; npm run patches", - "patches": "patch -p1 < scripts/rn-ldk.patch; patch -p1 < scripts/react-native-camera-kit.patch; patch -p1 < scripts/react-native-widget-center.patch", + "patches": "patch -p1 < scripts/rn-ldk.patch; patch -p1 < scripts/react-native-camera-kit.patch;", "test": "npm run tslint && npm run lint && npm run unit && npm run jest", "jest": "jest -b tests/integration/*", "e2e:debug-build": "detox build -c android.debug", @@ -148,7 +148,7 @@ "react-native-camera-kit": "13.0.0", "react-native-crypto": "2.2.0", "react-native-default-preference": "1.4.4", - "react-native-device-info": "10.13.2", + "react-native-device-info": "10.14.0", "react-native-document-picker": "https://github.com/BlueWallet/react-native-document-picker#6033c4e1b0dd0a6760b5f5a5a2c3b2e5d07f2ae4", "react-native-draggable-flatlist": "github:BlueWallet/react-native-draggable-flatlist#ebfddc4", "react-native-elements": "3.4.3", @@ -156,7 +156,7 @@ "react-native-gesture-handler": "2.16.2", "react-native-handoff": "https://github.com/BlueWallet/react-native-handoff#31d005f93d31099d0e564590a3bbd052b8a02b39", "react-native-haptic-feedback": "2.2.0", - "react-native-idle-timer": "https://github.com/BlueWallet/react-native-idle-timer#8587876d68ab5920e79619726aeca9e672beaf2b", + "react-native-idle-timer": "https://github.com/BlueWallet/react-native-idle-timer#7300b637c465c86e8db874c442e687950111da40", "react-native-image-picker": "7.1.2", "react-native-ios-context-menu": "github:BlueWallet/react-native-ios-context-menu#v1.15.3", "react-native-keychain": "8.2.0", @@ -177,14 +177,13 @@ "react-native-safe-area-context": "4.10.1", "react-native-screens": "3.31.1", "react-native-secure-key-store": "https://github.com/BlueWallet/react-native-secure-key-store#2076b48", - "react-native-share": "10.2.0", - "react-native-svg": "13.14.0", + "react-native-share": "10.2.1", + "react-native-svg": "13.14.1", "react-native-tcp-socket": "6.0.6", "react-native-vector-icons": "10.1.0", "react-native-watch-connectivity": "1.1.0", - "react-native-widget-center": "https://github.com/BlueWallet/react-native-widget-center#a128c38", "readable-stream": "3.6.2", - "realm": "12.8.0", + "realm": "12.8.1", "rn-ldk": "github:BlueWallet/rn-ldk#v0.8.4", "rn-nodeify": "10.3.0", "scryptsy": "2.1.0", diff --git a/screen/UnlockWith.tsx b/screen/UnlockWith.tsx index cb544dc80..2d3e800ad 100644 --- a/screen/UnlockWith.tsx +++ b/screen/UnlockWith.tsx @@ -1,12 +1,12 @@ -import React, { useCallback, useContext, useEffect, useReducer, useRef } from 'react'; -import { View, Image, ActivityIndicator, NativeModules, StyleSheet } from 'react-native'; -import Biometric, { BiometricType } from '../class/biometrics'; -import { BlueStorageContext } from '../blue_modules/storage-context'; +import React, { useCallback, useEffect, useReducer, useRef } from 'react'; +import { View, Image, ActivityIndicator, StyleSheet } from 'react-native'; +import { useStorage } from '../blue_modules/storage-context'; import triggerHapticFeedback, { HapticFeedbackTypes } from '../blue_modules/hapticFeedback'; import SafeArea from '../components/SafeArea'; import { BlueTextCentered } from '../BlueComponents'; import loc from '../loc'; import Button from '../components/Button'; +import { BiometricType, useBiometrics } from '../hooks/useBiometrics'; enum AuthType { Encrypted, @@ -49,12 +49,11 @@ function reducer(state: State, action: Action): State { } } -const { SplashScreen } = NativeModules; - const UnlockWith: React.FC = () => { const [state, dispatch] = useReducer(reducer, initialState); const isUnlockingWallets = useRef(false); - const { setWalletsInitialized, isStorageEncrypted, startAndDecrypt } = useContext(BlueStorageContext); + const { setWalletsInitialized, isStorageEncrypted, startAndDecrypt } = useStorage(); + const { deviceBiometricType, unlockWithBiometrics, isBiometricUseCapableAndEnabled, isBiometricUseEnabled } = useBiometrics(); const successfullyAuthenticated = useCallback(() => { setWalletsInitialized(true); @@ -62,19 +61,20 @@ const UnlockWith: React.FC = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const unlockWithBiometrics = useCallback(async () => { + const unlockUsingBiometrics = useCallback(async () => { if (isUnlockingWallets.current || state.isAuthenticating) return; isUnlockingWallets.current = true; dispatch({ type: SET_IS_AUTHENTICATING, payload: true }); - if (await Biometric.unlockWithBiometrics()) { + if (await unlockWithBiometrics()) { await startAndDecrypt(); successfullyAuthenticated(); } dispatch({ type: SET_IS_AUTHENTICATING, payload: false }); isUnlockingWallets.current = false; - }, [state.isAuthenticating, startAndDecrypt, successfullyAuthenticated]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [state.isAuthenticating]); const unlockWithKey = useCallback(async () => { if (isUnlockingWallets.current || state.isAuthenticating) return; @@ -91,18 +91,16 @@ const UnlockWith: React.FC = () => { }, [state.isAuthenticating, startAndDecrypt, successfullyAuthenticated]); useEffect(() => { - SplashScreen?.dismissSplashScreen(); - const startUnlock = async () => { const storageIsEncrypted = await isStorageEncrypted(); - const isBiometricUseCapableAndEnabled = await Biometric.isBiometricUseCapableAndEnabled(); - const biometricType = isBiometricUseCapableAndEnabled ? await Biometric.biometricType() : undefined; - const biometricsUseEnabled = await Biometric.isBiometricUseEnabled(); + const biometricUseCapableAndEnabled = await isBiometricUseCapableAndEnabled(); + const biometricsUseEnabled = await isBiometricUseEnabled(); + const biometricType = biometricUseCapableAndEnabled ? deviceBiometricType : undefined; if (storageIsEncrypted) { dispatch({ type: SET_AUTH, payload: { type: AuthType.Encrypted, detail: undefined } }); unlockWithKey(); - } else if (isBiometricUseCapableAndEnabled) { + } else if (biometricUseCapableAndEnabled) { dispatch({ type: SET_AUTH, payload: { type: AuthType.Biometrics, detail: biometricType } }); unlockWithBiometrics(); } else if (biometricsUseEnabled && biometricType === undefined) { @@ -120,7 +118,7 @@ const UnlockWith: React.FC = () => { const onUnlockPressed = () => { if (state.auth.type === AuthType.Biometrics) { - unlockWithBiometrics(); + unlockUsingBiometrics(); } else { unlockWithKey(); } diff --git a/screen/lnd/ldkOpenChannel.tsx b/screen/lnd/ldkOpenChannel.tsx index a6186ae31..318cda0bd 100644 --- a/screen/lnd/ldkOpenChannel.tsx +++ b/screen/lnd/ldkOpenChannel.tsx @@ -1,8 +1,8 @@ -import React, { useContext, useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { View, StyleSheet } from 'react-native'; import { RouteProp, useNavigation, useRoute } from '@react-navigation/native'; import { BlueLoading, BlueDismissKeyboardInputAccessory, BlueSpacing20, BlueText } from '../../BlueComponents'; -import { BlueStorageContext } from '../../blue_modules/storage-context'; +import { useStorage } from '../../blue_modules/storage-context'; import BigNumber from 'bignumber.js'; import AddressInput from '../../components/AddressInput'; import AmountInput from '../../components/AmountInput'; @@ -11,13 +11,13 @@ import loc from '../../loc'; import { HDSegwitBech32Wallet, LightningLdkWallet } from '../../class'; import { ArrowPicker } from '../../components/ArrowPicker'; import { Psbt } from 'bitcoinjs-lib'; -import Biometric from '../../class/biometrics'; import presentAlert from '../../components/Alert'; import { useTheme } from '../../components/themes'; import Button from '../../components/Button'; import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback'; import SafeArea from '../../components/SafeArea'; import { btcToSatoshi, fiatToBTC } from '../../blue_modules/currency'; +import { useBiometrics } from '../../hooks/useBiometrics'; type LdkOpenChannelProps = RouteProp< { @@ -33,8 +33,8 @@ type LdkOpenChannelProps = RouteProp< >; const LdkOpenChannel = (props: any) => { - const { wallets, fetchAndSaveWalletTransactions } = useContext(BlueStorageContext); - const [isBiometricUseCapableAndEnabled, setIsBiometricUseCapableAndEnabled] = useState(false); + const { wallets, fetchAndSaveWalletTransactions } = useStorage(); + const { isBiometricUseCapableAndEnabled, unlockWithBiometrics } = useBiometrics(); const { colors }: { colors: any } = useTheme(); const { navigate, setParams } = useNavigation(); const { @@ -75,14 +75,10 @@ const LdkOpenChannel = (props: any) => { })(); }, [psbt]); - useEffect(() => { - Biometric.isBiometricUseCapableAndEnabled().then(setIsBiometricUseCapableAndEnabled); - }, []); - const finalizeOpenChannel = async () => { setIsLoading(true); - if (isBiometricUseCapableAndEnabled) { - if (!(await Biometric.unlockWithBiometrics())) { + if (await isBiometricUseCapableAndEnabled()) { + if (!(await unlockWithBiometrics())) { setIsLoading(false); return; } diff --git a/screen/lnd/lnurlPay.js b/screen/lnd/lnurlPay.js index 120d6f078..7b6a54bd5 100644 --- a/screen/lnd/lnurlPay.js +++ b/screen/lnd/lnurlPay.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useContext } from 'react'; +import React, { useState, useEffect } from 'react'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { I18nManager, Image, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { useNavigation, useRoute } from '@react-navigation/native'; @@ -8,8 +8,7 @@ import AmountInput from '../../components/AmountInput'; import Lnurl from '../../class/lnurl'; import { BitcoinUnit, Chain } from '../../models/bitcoinUnits'; import loc, { formatBalanceWithoutSuffix, formatBalance } from '../../loc'; -import Biometric from '../../class/biometrics'; -import { BlueStorageContext } from '../../blue_modules/storage-context'; +import { useStorage } from '../../blue_modules/storage-context'; import presentAlert from '../../components/Alert'; import { useTheme } from '../../components/themes'; import Button from '../../components/Button'; @@ -17,6 +16,7 @@ import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/h import SafeArea from '../../components/SafeArea'; import { btcToSatoshi, fiatToBTC, satoshiToBTC, satoshiToLocalCurrency } from '../../blue_modules/currency'; import prompt from '../../helpers/prompt'; +import { useBiometrics } from '../../hooks/useBiometrics'; /** * if user has default currency - fiat, attempting to pay will trigger conversion from entered in input field fiat value @@ -26,7 +26,8 @@ import prompt from '../../helpers/prompt'; const _cacheFiatToSat = {}; const LnurlPay = () => { - const { wallets } = useContext(BlueStorageContext); + const { wallets } = useStorage(); + const { isBiometricUseCapableAndEnabled, unlockWithBiometrics } = useBiometrics(); const { walletID, lnurl } = useRoute().params; /** @type {LightningCustodianWallet} */ const wallet = wallets.find(w => w.getID() === walletID); @@ -105,9 +106,9 @@ const LnurlPay = () => { /** @type {Lnurl} */ const LN = _LN; - const isBiometricsEnabled = await Biometric.isBiometricUseCapableAndEnabled(); + const isBiometricsEnabled = await isBiometricUseCapableAndEnabled(); if (isBiometricsEnabled) { - if (!(await Biometric.unlockWithBiometrics())) { + if (!(await unlockWithBiometrics())) { return; } } diff --git a/screen/lnd/scanLndInvoice.js b/screen/lnd/scanLndInvoice.js index e343f7e74..550607811 100644 --- a/screen/lnd/scanLndInvoice.js +++ b/screen/lnd/scanLndInvoice.js @@ -1,4 +1,4 @@ -import React, { useCallback, useContext, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { Text, ActivityIndicator, @@ -12,24 +12,24 @@ import { } from 'react-native'; import { Icon } from 'react-native-elements'; import { useFocusEffect, useNavigation, useRoute } from '@react-navigation/native'; - import { BlueCard, BlueDismissKeyboardInputAccessory, BlueLoading } from '../../BlueComponents'; import AddressInput from '../../components/AddressInput'; import AmountInput from '../../components/AmountInput'; import Lnurl from '../../class/lnurl'; import { BitcoinUnit, Chain } from '../../models/bitcoinUnits'; -import Biometric from '../../class/biometrics'; import loc, { formatBalanceWithoutSuffix } from '../../loc'; -import { BlueStorageContext } from '../../blue_modules/storage-context'; +import { useStorage } from '../../blue_modules/storage-context'; import presentAlert from '../../components/Alert'; import { useTheme } from '../../components/themes'; import Button from '../../components/Button'; import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback'; import SafeArea from '../../components/SafeArea'; import { btcToSatoshi, fiatToBTC } from '../../blue_modules/currency'; +import { useBiometrics } from '../../hooks/useBiometrics'; const ScanLndInvoice = () => { - const { wallets, fetchAndSaveWalletTransactions } = useContext(BlueStorageContext); + const { wallets, fetchAndSaveWalletTransactions } = useStorage(); + const { unlockWithBiometrics, isBiometricUseCapableAndEnabled } = useBiometrics(); const { colors } = useTheme(); const { walletID, uri, invoice } = useRoute().params; const name = useRoute().name; @@ -167,10 +167,10 @@ const ScanLndInvoice = () => { return null; } - const isBiometricsEnabled = await Biometric.isBiometricUseCapableAndEnabled(); + const isBiometricsEnabled = await isBiometricUseCapableAndEnabled(); if (isBiometricsEnabled) { - if (!(await Biometric.unlockWithBiometrics())) { + if (!(await unlockWithBiometrics())) { return; } } diff --git a/screen/send/Broadcast.tsx b/screen/send/Broadcast.tsx index 33d5358f0..83be8c227 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 { isTablet } from '../../blue_modules/environment'; const BROADCAST_RESULT = Object.freeze({ none: 'Input transaction hex', @@ -117,7 +117,7 @@ const Broadcast: React.FC = () => { return ( - + {BROADCAST_RESULT.success !== broadcastResult && ( diff --git a/screen/send/confirm.js b/screen/send/confirm.js index e95393583..e4354c6e7 100644 --- a/screen/send/confirm.js +++ b/screen/send/confirm.js @@ -5,11 +5,9 @@ import { PayjoinClient } from 'payjoin-client'; import PropTypes from 'prop-types'; import BigNumber from 'bignumber.js'; import * as bitcoin from 'bitcoinjs-lib'; - import PayjoinTransaction from '../../class/payjoin-transaction'; import { BlueText, BlueCard } from '../../BlueComponents'; import { BitcoinUnit } from '../../models/bitcoinUnits'; -import Biometric from '../../class/biometrics'; import loc, { formatBalance, formatBalanceWithoutSuffix } from '../../loc'; import Notifications from '../../blue_modules/notifications'; import { BlueStorageContext } from '../../blue_modules/storage-context'; @@ -21,10 +19,11 @@ import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/h import SafeArea from '../../components/SafeArea'; import { satoshiToBTC, satoshiToLocalCurrency } from '../../blue_modules/currency'; import * as BlueElectrum from '../../blue_modules/BlueElectrum'; +import { useBiometrics } from '../../hooks/useBiometrics'; const Confirm = () => { const { wallets, fetchAndSaveWalletTransactions, isElectrumDisabled } = useContext(BlueStorageContext); - const [isBiometricUseCapableAndEnabled, setIsBiometricUseCapableAndEnabled] = useState(false); + const { isBiometricUseCapableAndEnabled, unlockWithBiometrics } = useBiometrics(); const { params } = useRoute(); const { recipients = [], walletID, fee, memo, tx, satoshiPerByte, psbt } = params; const [isLoading, setIsLoading] = useState(false); @@ -64,7 +63,6 @@ const Confirm = () => { useEffect(() => { console.log('send/confirm - useEffect'); console.log('address = ', recipients); - Biometric.isBiometricUseCapableAndEnabled().then(setIsBiometricUseCapableAndEnabled); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -77,8 +75,8 @@ const Confirm = () => { testID="TransactionDetailsButton" style={[styles.txDetails, stylesHook.txDetails]} onPress={async () => { - if (isBiometricUseCapableAndEnabled) { - if (!(await Biometric.unlockWithBiometrics())) { + if (await isBiometricUseCapableAndEnabled()) { + if (!(await unlockWithBiometrics())) { return; } } @@ -99,7 +97,7 @@ const Confirm = () => { ), }); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [colors, fee, feeSatoshi, isBiometricUseCapableAndEnabled, memo, recipients, satoshiPerByte, tx, wallet]); + }, [colors, fee, feeSatoshi, memo, recipients, satoshiPerByte, tx, wallet]); /** * we need to look into `recipients`, find destination address and return its outputScript @@ -163,8 +161,8 @@ const Confirm = () => { await BlueElectrum.ping(); await BlueElectrum.waitTillConnected(); - if (isBiometricUseCapableAndEnabled) { - if (!(await Biometric.unlockWithBiometrics())) { + if (await isBiometricUseCapableAndEnabled()) { + if (!(await unlockWithBiometrics())) { return; } } diff --git a/screen/send/details.js b/screen/send/details.js index 348398b43..a64c47fba 100644 --- a/screen/send/details.js +++ b/screen/send/details.js @@ -334,7 +334,7 @@ const SendDetails = () => { // we need to re-calculate fees if user opens-closes coin control useFocusEffect( useCallback(() => { - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + setIsLoading(false); setDumb(v => !v); }, []), ); diff --git a/screen/send/psbtWithHardwareWallet.js b/screen/send/psbtWithHardwareWallet.js index ea5ce92a2..33fde1f1e 100644 --- a/screen/send/psbtWithHardwareWallet.js +++ b/screen/send/psbtWithHardwareWallet.js @@ -1,16 +1,14 @@ +import React, { useEffect, useRef, useState } from 'react'; import Clipboard from '@react-native-clipboard/clipboard'; import { useIsFocused, useNavigation, useRoute } from '@react-navigation/native'; import * as bitcoin from 'bitcoinjs-lib'; -import React, { useContext, useEffect, useRef, useState } from 'react'; import { ActivityIndicator, Linking, Platform, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native'; import DocumentPicker from 'react-native-document-picker'; import RNFS from 'react-native-fs'; - import { BlueCard, BlueSpacing20, BlueText } from '../../BlueComponents'; import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback'; import Notifications from '../../blue_modules/notifications'; -import { BlueStorageContext } from '../../blue_modules/storage-context'; -import Biometric from '../../class/biometrics'; +import { useStorage } from '../../blue_modules/storage-context'; import presentAlert from '../../components/Alert'; import CopyToClipboardButton from '../../components/CopyToClipboardButton'; import { DynamicQRCode } from '../../components/DynamicQRCode'; @@ -20,9 +18,12 @@ import { requestCameraAuthorization } from '../../helpers/scan-qr'; import loc from '../../loc'; import SaveFileButton from '../../components/SaveFileButton'; import * as BlueElectrum from '../../blue_modules/BlueElectrum'; +import { useBiometrics } from '../../hooks/useBiometrics'; const PsbtWithHardwareWallet = () => { - const { txMetadata, fetchAndSaveWalletTransactions, isElectrumDisabled } = useContext(BlueStorageContext); + const { txMetadata, fetchAndSaveWalletTransactions, isElectrumDisabled } = useStorage(); + const { isBiometricUseCapableAndEnabled, unlockWithBiometrics } = useBiometrics(); + const navigation = useNavigation(); const route = useRoute(); const { fromWallet, memo, psbt, deepLinkPSBT, launchedBy } = route.params; @@ -116,10 +117,10 @@ const PsbtWithHardwareWallet = () => { const broadcast = async () => { setIsLoading(true); - const isBiometricsEnabled = await Biometric.isBiometricUseCapableAndEnabled(); + const isBiometricsEnabled = await isBiometricUseCapableAndEnabled(); if (isBiometricsEnabled) { - if (!(await Biometric.unlockWithBiometrics())) { + if (!(await unlockWithBiometrics())) { setIsLoading(false); return; } diff --git a/screen/settings/SettingsPrivacy.tsx b/screen/settings/SettingsPrivacy.tsx index 3b9e95b38..f26814152 100644 --- a/screen/settings/SettingsPrivacy.tsx +++ b/screen/settings/SettingsPrivacy.tsx @@ -123,24 +123,24 @@ const SettingsPrivacy: React.FC = () => { - {!storageIsEncrypted && ( - <> - - - {loc.settings.privacy_quickactions_explanation} - - - )} + + {} + + {loc.settings.privacy_quickactions_explanation} + + {storageIsEncrypted && {loc.settings.encrypted_feature_disabled}} + + { {loc.settings.privacy_do_not_track_explanation} - {Platform.OS === 'ios' && !storageIsEncrypted && ( + {Platform.OS === 'ios' && ( <> @@ -162,12 +162,14 @@ const SettingsPrivacy: React.FC = () => { Component={TouchableWithoutFeedback} switch={{ onValueChange: onWidgetsTotalBalanceValueChange, - value: isWidgetBalanceDisplayAllowed, - disabled: isLoading === SettingsPrivacySection.All, + value: storageIsEncrypted ? false : isWidgetBalanceDisplayAllowed, + disabled: isLoading === SettingsPrivacySection.All || storageIsEncrypted, }} /> {loc.settings.total_balance_explanation} + + {storageIsEncrypted && {loc.settings.encrypted_feature_disabled}} )} diff --git a/screen/settings/electrumSettings.js b/screen/settings/electrumSettings.js index d23a44ad3..199c068ae 100644 --- a/screen/settings/electrumSettings.js +++ b/screen/settings/electrumSettings.js @@ -28,14 +28,14 @@ import { BlueDismissKeyboardInputAccessory, } from '../../BlueComponents'; import { BlueCurrentTheme } from '../../components/themes'; -import { reloadAllTimelines } from '../../components/WidgetCommunication'; import { BlueStorageContext } from '../../blue_modules/storage-context'; import presentAlert from '../../components/Alert'; -import { requestCameraAuthorization } from '../../helpers/scan-qr'; +import { scanQrHelper } from '../../helpers/scan-qr'; import Button from '../../components/Button'; import ListItem from '../../components/ListItem'; import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback'; import * as BlueElectrum from '../../blue_modules/BlueElectrum'; +import { navigationRef } from '../../NavigationService'; export default class ElectrumSettings extends Component { static contextType = BlueStorageContext; @@ -169,7 +169,6 @@ export default class ElectrumSettings extends Component { await DefaultPreference.clear(BlueElectrum.ELECTRUM_HOST); await DefaultPreference.clear(BlueElectrum.ELECTRUM_SSL_PORT); await DefaultPreference.clear(BlueElectrum.ELECTRUM_TCP_PORT); - reloadAllTimelines(); } catch (e) { // Must be running on Android console.log(e); @@ -198,7 +197,6 @@ export default class ElectrumSettings extends Component { await DefaultPreference.set(BlueElectrum.ELECTRUM_HOST, host); await DefaultPreference.set(BlueElectrum.ELECTRUM_TCP_PORT, port); await DefaultPreference.set(BlueElectrum.ELECTRUM_SSL_PORT, sslPort); - reloadAllTimelines(); } catch (e) { // Must be running on Android console.log(e); @@ -225,17 +223,9 @@ export default class ElectrumSettings extends Component { }); }; - importScan = () => { - requestCameraAuthorization().then(() => - this.props.navigation.navigate('ScanQRCodeRoot', { - screen: 'ScanQRCode', - params: { - launchedBy: this.props.route.name, - onBarScanned: this.onBarScanned, - showFileImportButton: true, - }, - }), - ); + importScan = async () => { + const scanned = await scanQrHelper(navigationRef.navigate, 'ElectrumSettings', true); + this.onBarScanned(scanned); }; useSSLPortToggled = value => { diff --git a/screen/settings/encryptStorage.js b/screen/settings/encryptStorage.js index 5d668eb5d..1319b1373 100644 --- a/screen/settings/encryptStorage.js +++ b/screen/settings/encryptStorage.js @@ -1,21 +1,22 @@ -import React, { useEffect, useState, useCallback, useContext } from 'react'; +import React, { useEffect, useState, useCallback } from 'react'; import { View, ScrollView, Alert, TouchableOpacity, TouchableWithoutFeedback, Text, StyleSheet, Platform } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { BlueLoading, BlueSpacing20, BlueCard, BlueText } from '../../BlueComponents'; -import Biometric from '../../class/biometrics'; import loc from '../../loc'; -import { BlueStorageContext } from '../../blue_modules/storage-context'; +import { useStorage } from '../../blue_modules/storage-context'; import presentAlert from '../../components/Alert'; import ListItem from '../../components/ListItem'; import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback'; import { useTheme } from '../../components/themes'; import prompt from '../../helpers/prompt'; +import { useBiometrics } from '../../hooks/useBiometrics'; const EncryptStorage = () => { - const { isStorageEncrypted, encryptStorage, decryptStorage, saveToDisk } = useContext(BlueStorageContext); + const { isStorageEncrypted, encryptStorage, decryptStorage, saveToDisk } = useStorage(); const [isLoading, setIsLoading] = useState(true); - const [biometrics, setBiometrics] = useState({ isDeviceBiometricCapable: false, isBiometricsEnabled: false, biometricsType: '' }); + const { isDeviceBiometricCapable, biometricEnabled, setBiometricUseEnabled, deviceBiometricType, unlockWithBiometrics } = useBiometrics(); const [storageIsEncryptedSwitchEnabled, setStorageIsEncryptedSwitchEnabled] = useState(false); + const [deviceBiometricCapable, setDeviceBiometricCapable] = useState(false); const { navigate, popToTop } = useNavigation(); const { colors } = useTheme(); const styleHooks = StyleSheet.create({ @@ -28,12 +29,10 @@ const EncryptStorage = () => { }); const initialState = useCallback(async () => { - const isBiometricsEnabled = await Biometric.isBiometricUseEnabled(); - const isDeviceBiometricCapable = await Biometric.isDeviceBiometricCapable(); - const biometricsType = (await Biometric.biometricType()) || loc.settings.biometrics; const isStorageEncryptedSwitchEnabled = await isStorageEncrypted(); + const isDeviceBiometricCapableSync = await isDeviceBiometricCapable(); setStorageIsEncryptedSwitchEnabled(isStorageEncryptedSwitchEnabled); - setBiometrics({ isBiometricsEnabled, isDeviceBiometricCapable, biometricsType }); + setDeviceBiometricCapable(isDeviceBiometricCapableSync); setIsLoading(false); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -107,15 +106,8 @@ const EncryptStorage = () => { }; const onUseBiometricSwitch = async value => { - const isBiometricsEnabled = { - isDeviceBiometricCapable: biometrics.isDeviceBiometricCapable, - isBiometricsEnabled: biometrics.isBiometricsEnabled, - biometricsType: biometrics.biometricsType, - }; - if (await Biometric.unlockWithBiometrics()) { - isBiometricsEnabled.isBiometricsEnabled = value; - await Biometric.setBiometricUseEnabled(value); - setBiometrics(isBiometricsEnabled); + if (await unlockWithBiometrics()) { + setBiometricUseEnabled(value); } }; @@ -135,7 +127,7 @@ const EncryptStorage = () => { return isCapable ? ( <> - {loc.formatString(loc.settings.biometrics_fail, { type: biometrics.biometricsType })} + {loc.formatString(loc.settings.biometrics_fail, { type: deviceBiometricType })} ) : null; }; @@ -147,18 +139,18 @@ const EncryptStorage = () => { ) : ( - {biometrics.isDeviceBiometricCapable && ( + {deviceBiometricCapable && ( <> {loc.settings.biometrics} - {loc.formatString(loc.settings.encrypt_use_expl, { type: biometrics.biometricsType })} + {loc.formatString(loc.settings.encrypt_use_expl, { type: deviceBiometricType })} {renderPasscodeExplanation()} diff --git a/screen/transactions/details.tsx b/screen/transactions/details.tsx index ee0076993..b40867fd8 100644 --- a/screen/transactions/details.tsx +++ b/screen/transactions/details.tsx @@ -23,6 +23,22 @@ interface TransactionDetailsProps { navigation: NativeStackNavigationProp; } +const actionKeys = { + CopyToClipboard: 'copyToClipboard', + GoToWallet: 'goToWallet', +}; + +const actionIcons = { + Clipboard: { + iconType: 'SYSTEM', + iconValue: 'doc.on.doc', + }, + GoToWallet: { + iconType: 'SYSTEM', + iconValue: 'wallet.pass', + }, +}; + function onlyUnique(value: any, index: number, self: any[]) { return self.indexOf(value) === index; } @@ -37,6 +53,14 @@ function arrDiff(a1: any[], a2: any[]) { return ret; } +const toolTipMenuActions = [ + { + id: actionKeys.CopyToClipboard, + text: loc.transactions.copy_link, + icon: actionIcons.Clipboard, + }, +]; + const TransactionDetails = () => { const { setOptions, navigate } = useNavigation(); const { hash, walletID } = useRoute().params; @@ -162,9 +186,7 @@ const TransactionDetails = () => { }; const handleCopyPress = (stringToCopy: string) => { - Clipboard.setString( - stringToCopy !== TransactionDetails.actionKeys.CopyToClipboard ? stringToCopy : `https://mempool.space/tx/${tx?.hash}`, - ); + Clipboard.setString(stringToCopy !== actionKeys.CopyToClipboard ? stringToCopy : `https://mempool.space/tx/${tx?.hash}`); }; if (isLoading || !tx) { @@ -189,9 +211,9 @@ const TransactionDetails = () => { }; const onPressMenuItem = (key: string) => { - if (key === TransactionDetails.actionKeys.CopyToClipboard) { + if (key === actionKeys.CopyToClipboard) { handleCopyPress(key); - } else if (key === TransactionDetails.actionKeys.GoToWallet) { + } else if (key === actionKeys.GoToWallet) { const wallet = weOwnAddress(key); if (wallet) { navigateToWallet(wallet); @@ -205,16 +227,16 @@ const TransactionDetails = () => { for (const [index, address] of array.entries()) { const actions = []; actions.push({ - id: TransactionDetails.actionKeys.CopyToClipboard, + id: actionKeys.CopyToClipboard, text: loc.transactions.details_copy, - icon: TransactionDetails.actionIcons.Clipboard, + icon: actionIcons.Clipboard, }); const isWeOwnAddress = weOwnAddress(address); if (isWeOwnAddress) { actions.push({ - id: TransactionDetails.actionKeys.GoToWallet, + id: actionKeys.GoToWallet, text: loc.formatString(loc.transactions.view_wallet, { walletLabel: isWeOwnAddress.getLabel() }), - icon: TransactionDetails.actionIcons.GoToWallet, + icon: actionIcons.GoToWallet, }); } @@ -320,16 +342,10 @@ const TransactionDetails = () => { )} {loc.transactions.details_show_in_block_explorer} @@ -338,22 +354,6 @@ const TransactionDetails = () => { ); }; -TransactionDetails.actionKeys = { - CopyToClipboard: 'copyToClipboard', - GoToWallet: 'goToWallet', -}; - -TransactionDetails.actionIcons = { - Clipboard: { - iconType: 'SYSTEM', - iconValue: 'doc.on.doc', - }, - GoToWallet: { - iconType: 'SYSTEM', - iconValue: 'wallet.pass', - }, -}; - const styles = StyleSheet.create({ scroll: { flex: 1, diff --git a/screen/wallets/paymentCode.tsx b/screen/wallets/PaymentCode.tsx similarity index 100% rename from screen/wallets/paymentCode.tsx rename to screen/wallets/PaymentCode.tsx diff --git a/screen/wallets/PaymentCodesList.tsx b/screen/wallets/PaymentCodesList.tsx new file mode 100644 index 000000000..256c9fd32 --- /dev/null +++ b/screen/wallets/PaymentCodesList.tsx @@ -0,0 +1,259 @@ +import React, { useContext, useEffect, useMemo, useState } from 'react'; +import { SectionList, StyleSheet, Text, View } from 'react-native'; +import { useRoute } from '@react-navigation/native'; +import { NativeStackScreenProps } from '@react-navigation/native-stack'; +import { BlueStorageContext } from '../../blue_modules/storage-context'; +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'; +import { PaymentCodeStackParamList } from '../../navigation/PaymentCodeStack'; +import presentAlert from '../../components/Alert'; +import { Action } from '../../components/types'; + +interface DataSection { + title: string; + data: string[]; +} +enum Actions { + pay, + rename, + copyToClipboard, +} + +const actionKeys: Action[] = [ + { + id: Actions.pay, + text: loc.bip47.pay_this_contact, + icon: { + iconType: 'SYSTEM', + iconValue: 'square.and.arrow.up', + }, + }, + { + id: Actions.rename, + text: loc.bip47.rename_contact, + icon: { + iconType: 'SYSTEM', + iconValue: 'note.text', + }, + }, + { + id: Actions.copyToClipboard, + text: loc.bip47.copy_payment_code, + icon: { + iconType: 'SYSTEM', + iconValue: 'doc.on.doc', + }, + }, +]; + +type Props = NativeStackScreenProps; + +function onlyUnique(value: any, index: number, self: any[]) { + return self.indexOf(value) === index; +} + +export default function PaymentCodesList() { + const route = useRoute(); + const { walletID } = route.params as Props['route']['params']; + const { wallets, txMetadata, counterpartyMetadata, saveToDisk } = useContext(BlueStorageContext); + const [reload, setReload] = useState(0); + const [data, setData] = useState([]); + const { colors } = useTheme(); + const [isLoading, setIsLoading] = useState(false); + const [loadingText, setLoadingText] = useState('Loading...'); + + useEffect(() => { + if (!walletID) return; + + const foundWallet = wallets.find(w => w.getID() === walletID) as unknown as AbstractHDElectrumWallet; + if (!foundWallet) return; + + const newData: DataSection[] = [ + { + title: '', + data: foundWallet.getBIP47SenderPaymentCodes().concat(foundWallet.getBIP47ReceiverPaymentCodes()).filter(onlyUnique), + }, + ]; + setData(newData); + }, [walletID, wallets, reload]); + + const toolTipActions = useMemo(() => 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 (String(id) === String(Actions.copyToClipboard)) { + Clipboard.setString(pc); + presentAlert({ message: loc.bip47.copied }); + } + + if (String(id) === String(Actions.rename)) { + const newName = await prompt(loc.bip47.rename, loc.bip47.provide_name, false, 'plain-text'); + if (!newName) return; + + counterpartyMetadata[pc] = { label: newName }; + setReload(Math.random()); + } + + if (String(id) === String(Actions.pay)) { + presentAlert({ message: 'Not implemented yet' }); + } + }; + + 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)} + isButton={true} + isMenuPrimaryAction={true} + > + + + + {displayName} + + + + + ); + }; + + const onAddContactPress = async () => { + try { + const foundWallet = wallets.find(w => w.getID() === walletID) as unknown as HDSegwitBech32Wallet; + assert(foundWallet); + + const newPc = await prompt(loc.bip47.add_contact, loc.bip47.provide_payment_code, false, 'plain-text'); + if (!newPc) return; + const cl = new ContactList(foundWallet); + + if (!cl.isPaymentCodeValid(newPc)) { + presentAlert({ message: loc.bip47.invalid_pc }); + 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 + presentAlert({ message: loc.bip47.notification_tx_unconfirmed }); + 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) { + presentAlert({ message: loc.bip47.failed_create_notif_tx }); + return; + } + + setLoadingText(''); + if ( + await confirm( + loc.bip47.onchain_tx_needed, + `${loc.send.create_fee}: ${formatBalance(fee, BitcoinUnit.BTC)} (${satoshiToLocalCurrency(fee)}). `, + ) + ) { + setLoadingText('Broadcasting...'); + try { + await foundWallet.broadcastTx(tx.toHex()); + foundWallet.addBIP47Receiver(newPc); + presentAlert({ message: loc.bip47.notif_tx_sent }); + txMetadata[tx.getId()] = { memo: loc.bip47.notif_tx }; + 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) { + presentAlert({ message: error.message }); + } finally { + setIsLoading(false); + } + }; + + if (isLoading) { + return ( + + + {loadingText} + + ); + } + + return ( + + {!walletID ? ( + Internal error + ) : ( + + item + index} renderItem={({ item }) => renderItem(item)} /> + + )} + +