Merge branch 'master' into storageuse

This commit is contained in:
Marcos Rodriguez Velez 2024-05-20 17:31:01 -04:00
commit 6612360b1e
No known key found for this signature in database
GPG key ID: 6030B2F48CCE86D7
82 changed files with 1842 additions and 1568 deletions

294
App.js
View file

@ -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<boolean>} 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 (
<NavigationContainer ref={navigationRef} theme={colorScheme === 'dark' ? BlueDarkTheme : BlueDefaultTheme}>
<NavigationProvider>
<SafeAreaProvider>
<View style={styles.root}>
<MainRoot />
<Notifications onProcessNotifications={processPushNotifications} />
<MenuElements />
<DeviceQuickActions />
<Biometric />
<HandOffComponentListener />
</View>
<WatchConnectivity />
<WidgetCommunication />
</SafeAreaProvider>
</NavigationProvider>
</NavigationContainer>
);
};
const styles = StyleSheet.create({
root: {
flex: 1,
},
});
export default App;

34
App.tsx Normal file
View file

@ -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 (
<LargeScreenProvider>
<NavigationContainer ref={navigationRef} theme={colorScheme === 'dark' ? BlueDarkTheme : BlueDefaultTheme}>
<NavigationProvider>
<SafeAreaProvider>
<BlueStorageProvider>
<SettingsProvider>
<MasterView />
</SettingsProvider>
</BlueStorageProvider>
</SafeAreaProvider>
</NavigationProvider>
</NavigationContainer>
</LargeScreenProvider>
);
};
export default App;

22
MasterView.tsx Normal file
View file

@ -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 (
<>
<MainRoot />
{walletsInitialized && (
<Suspense>
<CompanionDelegates />
</Suspense>
)}
</>
);
};
export default MasterView;

View file

@ -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'
}

View file

@ -7,50 +7,52 @@
android:required="false" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.ACTION_OPEN_DOCUMENT" />
<uses-permission android:name="android.permission.ACTION_GET_CONTENT" />
<uses-permission android:name="android.permission.ACTION_CREATE_DOCUMENT" />
<application
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">
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">
<meta-data android:name="com.dieam.reactnativepushnotification.notification_channel_name"
android:value="BlueWallet notifications"/> <!-- YOUR NOTIFICATION CHANNEL NAME -->
<meta-data android:name="com.dieam.reactnativepushnotification.notification_channel_description"
android:value="Notifications about incoming payments"/> <!-- YOUR NOTIFICATION CHANNEL DESCRIPTION -->
<!-- Change the value to true to enable pop-up for in foreground (remote-only, for local use ignoreInForeground) -->
<meta-data android:name="com.dieam.reactnativepushnotification.notification_foreground"
android:value="true"/>
<!-- Change the value to false if you don't want the creation of the default channel -->
<meta-data android:name="com.dieam.reactnativepushnotification.channel_create_default"
android:value="true"/>
<!-- Change the resource name to your App's accent color - or any other color you want -->
<meta-data android:name="com.dieam.reactnativepushnotification.notification_color"
android:resource="@color/white"/> <!-- or @android:color/{name} to use a standard color -->
<meta-data
android:name="com.dieam.reactnativepushnotification.notification_channel_name"
android:value="BlueWallet notifications" />
<meta-data
android:name="com.dieam.reactnativepushnotification.notification_channel_description"
android:value="Notifications about incoming payments" />
<meta-data
android:name="com.dieam.reactnativepushnotification.notification_foreground"
android:value="true" />
<meta-data
android:name="com.dieam.reactnativepushnotification.channel_create_default"
android:value="true" />
<meta-data
android:name="com.dieam.reactnativepushnotification.notification_color"
android:resource="@color/white" />
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationActions" />
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationPublisher" />
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationBootEventReceiver"
<receiver
android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationBootEventReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
@ -58,71 +60,65 @@
</receiver>
<service
android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationListenerService"
android:exported="false" >
android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationListenerService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<activity android:name=".SplashActivity"
android:exported="true"
>
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:launchMode="singleTop"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
android:windowSoftInputMode="adjustResize"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="bitcoin" />
<data android:scheme="lightning" />
<data android:scheme="bluewallet" />
<data android:scheme="lapp" />
<data android:scheme="blue" />
<data android:scheme="http" />
<data android:scheme="https" />
</intent-filter>
<intent-filter tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.EDIT" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:mimeType="application/octet-stream"
android:pathPattern=".*\\.psbt" />
</intent-filter>
<intent-filter tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.EDIT" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:mimeType="text/plain"
android:pathPattern=".*\\.psbt" />
</intent-filter>
</activity>
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:launchMode="singleTop"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
android:windowSoftInputMode="adjustResize"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="bitcoin" />
<data android:scheme="lightning" />
<data android:scheme="bluewallet" />
<data android:scheme="lapp" />
<data android:scheme="blue" />
<data android:scheme="http" />
<data android:scheme="https" />
</intent-filter>
<intent-filter tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.EDIT" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:mimeType="application/octet-stream"
android:pathPattern=".*\\.psbt" />
</intent-filter>
<intent-filter tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.EDIT" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:mimeType="text/plain"
android:pathPattern=".*\\.psbt" />
</intent-filter>
</activity>
<meta-data android:name="com.bugsnag.android.API_KEY" android:value="17ba9059f676f1cc4f45d98182388b01" />
<meta-data android:name="com.bugsnag.android.API_KEY" android:value="17ba9059f676f1cc4f45d98182388b01" />
</application>
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https"/>
<data android:scheme="https" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="http"/>
<data android:scheme="http" />
</intent>
</queries>
</manifest>

View file

@ -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);
}
}

View file

@ -1,13 +0,0 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background_color">
<ImageView
android:layout_width="100dp"
android:layout_height="75dp"
android:scaleType="fitCenter"
android:layout_centerInParent="true"
android:src="@drawable/splash_icon" />
</RelativeLayout>

View file

@ -83,9 +83,3 @@ subprojects {
}
}
subprojects { subproject ->
if(project['name'] == 'react-native-widget-center') {
project.configurations { compile { } }
}
}

View file

@ -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<void> {
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);

View file

@ -1,9 +0,0 @@
function WidgetCommunication() {
return null;
}
WidgetCommunication.isBalanceDisplayAllowed = () => {};
WidgetCommunication.setBalanceDisplayAllowed = () => {};
WidgetCommunication.reloadAllTimelines = () => {};
export default WidgetCommunication;

View file

@ -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<void> {
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<FiatUnitType> {

View file

@ -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 };

View file

@ -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<boolean> => {
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;
}

View file

@ -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<boolean>;
isDeviceBiometricCapable: () => Promise<boolean>;
setBiometricUseEnabled: (arg: boolean) => Promise<void>;
biometricType: () => Promise<keyof typeof RNBiometryTypes | undefined>;
isBiometricUseEnabled: () => Promise<boolean>;
unlockWithBiometrics: () => Promise<boolean>;
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 };

23
class/contact-list.ts Normal file
View file

@ -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;
}
}
}

View file

@ -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<void> {
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;
}

View file

@ -101,7 +101,7 @@ export type Transaction = {
inputs: TransactionInput[];
outputs: TransactionOutput[];
blockhash: string;
confirmations?: number;
confirmations: number;
time: number;
blocktime: number;
received?: number;

View file

@ -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<AppStateStatus>(AppState.currentState);
const clipboardContent = useRef<undefined | string>();
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 (
<>
<Notifications onProcessNotifications={processPushNotifications} />
<Suspense fallback={null}>
<MenuElements />
<DeviceQuickActions />
<HandOffComponentListener />
<WidgetCommunication />
<WatchConnectivity />
</Suspense>
</>
);
};
export default CompanionDelegates;

View file

@ -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<ILargeScreenContext | undefined>(undefined);
interface LargeScreenProviderProps {
children: ReactNode;
}
export const LargeScreenProvider: React.FC<LargeScreenProviderProps> = ({ children }) => {
const [windowWidth, setWindowWidth] = useState<number>(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 <LargeScreenContext.Provider value={{ isLargeScreen }}>{children}</LargeScreenContext.Provider>;
};

View file

@ -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;

View file

@ -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<QRCodeComponentProps> = ({
value = '',
isLogoRendered = true,
@ -75,23 +76,6 @@ const QRCodeComponent: React.FC<QRCodeComponentProps> = ({
}
};
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 = (
<QRCode
value={value}
@ -115,7 +99,7 @@ const QRCodeComponent: React.FC<QRCodeComponentProps> = ({
accessibilityLabel={loc.receive.qrcode_for_the_address}
>
{isMenuAvailable ? (
<ToolTipMenu actions={menuActions()} onPressMenuItem={onPressMenuItem}>
<ToolTipMenu actions={menuActions} onPressMenuItem={onPressMenuItem}>
{renderQRCode}
</ToolTipMenu>
) : (

View file

@ -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<ViewStyle>;
afterOnPress?: () => void;
beforeOnPress?: () => Promise<void>; // Changed this line
beforeOnPress?: () => Promise<void>;
onMenuWillHide?: () => void;
onMenuWillShow?: () => void;
}
@ -26,30 +27,24 @@ const SaveFileButton: React.FC<SaveFileButtonProps> = ({
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}
<ToolTipMenu
onMenuWillHide={onMenuWillHide}
onMenuWillShow={onMenuWillShow}
@ -57,7 +52,7 @@ const SaveFileButton: React.FC<SaveFileButtonProps> = ({
isMenuPrimaryAction
actions={actions}
onPressMenuItem={handlePressMenuItem}
buttonStyle={style}
buttonStyle={style as ViewStyle} // Type assertion to match ViewStyle
>
{children}
</ToolTipMenu>
@ -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 },
];

View file

@ -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 (
<Pressable
{...(enableAndroidRipple ? { android_ripple: { color: 'lightgrey' } } : {})}
ref={menuRef}
disabled={disabled}
style={buttonStyle}
{...(isMenuPrimaryAction ? { onPress: showMenu } : { onPress: props.onPress, onLongPress: showMenu })}
>
{props.children}
</Pressable>
);
};
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,
};

View file

@ -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<View>(null);
const {
actions,
children,
onPressMenuItem,
isMenuPrimaryAction = false,
buttonStyle = {},
enableAndroidRipple = true,
disabled = false,
onPress,
...restProps
} = props;
const handleToolTipSelection = useCallback<OnPopupMenuItemSelect>(
(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 (
<Pressable
{...(enableAndroidRipple ? { android_ripple: { color: 'lightgrey' } } : {})}
ref={menuRef}
disabled={disabled}
style={buttonStyle}
{...(isMenuPrimaryAction ? { onPress: showMenu } : { onPress, onLongPress: showMenu })}
{...restProps}
>
{children}
</Pressable>
);
};
const ToolTipMenu = forwardRef(BaseToolTipMenu);
export default ToolTipMenu;

View file

@ -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 ? (
<TouchableOpacity onPress={onPress} disabled={disabled} accessibilityRole="button" style={buttonStyle}>
<ContextMenuButton
ref={ref}
onMenuWillShow={onMenuWillShow}
onMenuWillHide={onMenuWillHide}
useActionSheetFallback={false}
onPressMenuItem={({ nativeEvent }) => {
props.onPressMenuItem(nativeEvent.actionKey);
}}
isMenuPrimaryAction={isMenuPrimaryAction}
menuConfig={{
menuTitle,
menuItems,
}}
>
{props.children}
</ContextMenuButton>
</TouchableOpacity>
) : props.onPress ? (
<ContextMenuView
ref={ref}
lazyPreview
shouldEnableAggressiveCleanup
shouldCleanupOnComponentWillUnmountForMenuPreview
internalCleanupMode="automatic"
onPressMenuItem={({ nativeEvent }) => {
props.onPressMenuItem(nativeEvent.actionKey);
}}
useActionSheetFallback={false}
menuConfig={{
menuTitle,
menuItems,
}}
{...(renderPreview
? {
previewConfig: {
previewType: 'CUSTOM',
backgroundColor: 'white',
},
renderPreview,
}
: {})}
>
<TouchableOpacity accessibilityRole="button" onPress={props.onPress}>
{props.children}
</TouchableOpacity>
</ContextMenuView>
) : (
<ContextMenuView
ref={ref}
internalCleanupMode="viewController"
onPressMenuItem={({ nativeEvent }) => {
props.onPressMenuItem(nativeEvent.actionKey);
}}
lazyPreview
shouldEnableAggressiveCleanup
useActionSheetFallback={false}
menuConfig={{
menuTitle,
menuItems,
}}
{...(renderPreview
? {
previewConfig: {
previewType: 'CUSTOM',
backgroundColor: 'white',
},
renderPreview,
}
: {})}
>
{props.children}
</ContextMenuView>
);
};
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,
};

View file

@ -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<any>) => {
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 = () => (
<ContextMenuButton
ref={ref}
onMenuWillShow={onMenuWillShow}
onMenuWillHide={onMenuWillHide}
useActionSheetFallback={false}
onPressMenuItem={handlePressMenuItem}
isMenuPrimaryAction={isMenuPrimaryAction}
menuConfig={{
menuTitle: title,
menuItems,
}}
>
{props.children}
</ContextMenuButton>
);
const renderContextMenuView = () => (
<ContextMenuView
ref={ref}
lazyPreview
shouldEnableAggressiveCleanup
internalCleanupMode="automatic"
onPressMenuItem={handlePressMenuItem}
useActionSheetFallback={false}
menuConfig={{
menuTitle: title,
menuItems,
}}
{...(renderPreview
? {
previewConfig: {
previewType: 'CUSTOM',
backgroundColor: 'white',
},
renderPreview: renderPreview as RenderItem,
}
: {})}
>
{onPress ? (
<TouchableOpacity accessibilityRole="button" onPress={onPress}>
{props.children}
</TouchableOpacity>
) : (
props.children
)}
</ContextMenuView>
);
return isMenuPrimaryAction && onPress ? (
<TouchableOpacity onPress={onPress} disabled={disabled} accessibilityRole="button" style={buttonStyle}>
{renderContextMenuButton()}
</TouchableOpacity>
) : isButton ? (
renderContextMenuButton()
) : (
renderContextMenuView()
);
};
const ToolTipMenu = forwardRef(BaseToolTipMenu);
export default ToolTipMenu;

View file

@ -1,9 +0,0 @@
import { forwardRef } from 'react';
const BaseToolTipMenu = (props, _ref) => {
return props.children;
};
const ToolTipMenu = forwardRef(BaseToolTipMenu);
export default ToolTipMenu;

View file

@ -0,0 +1,11 @@
import { forwardRef, Ref } from 'react';
import { ToolTipMenuProps } from './types';
const BaseToolTipMenu = (props: ToolTipMenuProps, ref: Ref<any>) => {
console.debug('ToolTipMenu.tsx ref:', ref);
return props.children;
};
const ToolTipMenu = forwardRef(BaseToolTipMenu);
export default ToolTipMenu;

View file

@ -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<TransactionListItemProps> = 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<TransactionListItemProps> = React.mem
icon: actionIcons.Clipboard,
});
}
if (item.hash) {
actions.push(
{
@ -337,10 +339,9 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
]);
}
return actions;
return actions as Action[] | Action[][];
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [item.hash, subtitle, rowTitle, subtitleNumberOfLines, txMetadata]);
return (
<View style={styles.container}>
<ToolTipMenu ref={menuRef} actions={toolTipActions} onPressMenuItem={onToolTipPress} onPress={onPress}>

View file

@ -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
</Pressable>
</Animated.View>
);
};
});
WalletCarouselItem.propTypes = {
item: PropTypes.any,
@ -292,7 +292,7 @@ const WalletsCarousel = forwardRef((props, ref) => {
<NewWalletPanel onPress={onPress} />
),
// eslint-disable-next-line react-hooks/exhaustive-deps
[horizontal, selectedWallet, handleLongPress, onPress, preferredFiatCurrency, language],
[horizontal, selectedWallet, preferredFiatCurrency, language],
);
const flatListRef = useRef();

View file

@ -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);

View file

@ -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<boolean> => {
try {
await DefaultPreference.setName('group.io.bluewallet.bluewallet');
@ -34,7 +28,6 @@ export const setBalanceDisplayAllowed = async (value: boolean): Promise<void> =>
} else {
await DefaultPreference.clear(WidgetCommunicationKeys.DisplayBalanceAllowed);
}
reloadAllTimelines();
};
export const syncWidgetBalanceWithWallets = async (wallets: TWallet[], walletsInitialized: boolean): Promise<void> => {
@ -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 (

View file

@ -7,8 +7,6 @@ export const isBalanceDisplayAllowed = async (): Promise<boolean> => {
export const setBalanceDisplayAllowed = async (value: boolean): Promise<void> => {};
export const reloadAllTimelines = (): void => {};
export const syncWidgetBalanceWithWallets = async (_wallets: TWallet[], _walletsInitialized: boolean): Promise<void> => {};
const WidgetCommunication: React.FC = () => {

View file

@ -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 <QRCodeComponent value={item.address} isMenuAvailable={false} />;
};
@ -171,7 +141,7 @@ const AddressItem = ({ item, balanceUnit, walletID, allowSignVerifyMessage }: Ad
<TooltipMenu
title={item.address}
ref={menuRef}
actions={getAvailableActions()}
actions={menuActions}
onPressMenuItem={onToolTipPress}
renderPreview={renderPreview}
onPress={navigateToReceive}
@ -246,15 +216,37 @@ const styles = StyleSheet.create({
},
});
AddressItem.propTypes = {
item: PropTypes.shape({
key: PropTypes.string,
index: PropTypes.number,
address: PropTypes.string,
isInternal: PropTypes.bool,
transactions: PropTypes.number,
balance: PropTypes.number,
}),
balanceUnit: PropTypes.string,
const getAvailableActions = ({ allowSignVerifyMessage }: { allowSignVerifyMessage: boolean }): Action[] | Action[][] => {
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 };

30
components/types.ts Normal file
View file

@ -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;
}

195
hooks/useBiometrics.ts Normal file
View file

@ -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 };

View file

@ -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<ParamListBase> => {
const originalNavigation = useNavigation<NavigationProp<ParamListBase>>();
const { wallets, saveToDisk } = useContext(BlueStorageContext);
const { wallets, saveToDisk } = useStorage();
const { isBiometricUseEnabled, unlockWithBiometrics } = useBiometrics();
const enhancedNavigate: NavigationProp<ParamListBase>['navigate'] = (screenOrOptions: any, params?: any) => {
let screenName: string;
@ -42,12 +42,12 @@ export const useExtendedNavigation = (): NavigationProp<ParamListBase> => {
(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

View file

@ -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;
};

View file

@ -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 (
<BlueStorageProvider>
<SettingsProvider>
<App />
</SettingsProvider>
</BlueStorageProvider>
);
return <App />;
};
AppRegistry.registerComponent('BlueWallet', () => BlueAppComponent);

View file

@ -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 <React/RCTBridgeModule.h>

View file

@ -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 = "<group>"; };
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 = "<absolute>"; };
B4AB21062B61D8CA0080440C /* SplashScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreen.swift; sourceTree = "<group>"; };
B4AB21082B61DC3F0080440C /* SplashScreen.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SplashScreen.m; sourceTree = "<group>"; };
B4AB225C2B02AD12001F4328 /* XMLParserDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XMLParserDelegate.swift; sourceTree = "<group>"; };
B4B1A4612BFA73110072E3BB /* WidgetHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetHelper.swift; sourceTree = "<group>"; };
B4D3235A177F4580BA52F2F9 /* libRNCSlider.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNCSlider.a; sourceTree = "<group>"; };
B642AFB13483418CAB6FF25E /* libRCTQRCodeLocalImage.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRCTQRCodeLocalImage.a; sourceTree = "<group>"; };
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 = "<group>"; };
@ -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 = "<group>";
@ -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 */ = {

View file

@ -9,10 +9,11 @@
#import "EventEmitter.h"
#import <React/RCTRootView.h>
#import <Bugsnag/Bugsnag.h>
#import "BlueWallet-Swift.h"
@interface AppDelegate() <UNUserNotificationCenterDelegate>
@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<NSKeyValueChangeKey,id> *)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<id<UIUserActivityRestoring>> * _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

View file

@ -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

View file

@ -12,16 +12,32 @@
<key>FooterText</key>
<string>Provide this Unique ID when reporting an issue</string>
<key>Title</key>
<string></string>
<string>Report Issue</string>
</dict>
<dict>
<key>Type</key>
<string>PSTextFieldSpecifier</string>
<key>Title</key>
<string>Unique ID</string>
<key>Key</key>
<key>Key</key>
<string>deviceUIDCopy</string>
</dict>
<dict>
<key>Type</key>
<string>PSGroupSpecifier</string>
<key>Title</key>
<string>Cache</string>
</dict>
<dict>
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
<key>Title</key>
<string>Clear Cache on Next Launch</string>
<key>Key</key>
<string>clearFilesOnLaunch</string>
<key>DefaultValue</key>
<false/>
</dict>
</array>
</dict>
</plist>

View file

@ -1,14 +0,0 @@
//
// SplashScreen.m
// BlueWallet
//
// Created by Marcos Rodriguez on 1/24/24.
// Copyright © 2024 BlueWallet. All rights reserved.
//
#import <React/RCTBridgeModule.h>
@interface RCT_EXTERN_MODULE(SplashScreen, NSObject)
RCT_EXTERN_METHOD(addObserver)
RCT_EXTERN_METHOD(dismissSplashScreen)
@end

View file

@ -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)
}
}
}

11
ios/WidgetHelper.swift Normal file
View file

@ -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
}
}
}

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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,
},

View file

@ -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 = () => (
<Suspense fallback={<LazyLoadingIndicator />}>

View file

@ -1,10 +0,0 @@
import React, { lazy, Suspense } from 'react';
import { LazyLoadingIndicator } from './LazyLoadingIndicator';
const ReorderWallets = lazy(() => import('../screen/wallets/reorderWallets'));
export const ReorderWalletsComponent = () => (
<Suspense fallback={<LazyLoadingIndicator />}>
<ReorderWallets />
</Suspense>
);

22
navigation/MasterView.tsx Normal file
View file

@ -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 (
<>
<MainRoot />
{walletsInitialized && (
<Suspense>
<CompanionDelegates />
</Suspense>
)}
</>
);
};
export default MasterView;

View file

@ -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<PaymentCodeStackParamList>();
const PaymentCodeStackRoot = () => {
const theme = useTheme();
return (
<Stack.Navigator screenOptions={{ headerShadowVisible: false }} initialRouteName="PaymentCode">
<Stack.Screen name="PaymentCode" component={PaymentCodeComponent} options={{ headerTitle: loc.bip47.payment_code }} />
<Stack.Screen name="PaymentCodesList" component={PaymentCodesListComponent} options={{ headerTitle: loc.bip47.payment_codes_list }} />
<Stack.Screen
name="PaymentCode"
component={PaymentCodeComponent}
options={navigationStyle({ title: loc.bip47.payment_code, closeButton: true })(theme)}
/>
<Stack.Screen
name="PaymentCodesList"
component={PaymentCodesListComponent}
options={navigationStyle({ title: loc.bip47.contacts, closeButton: true })(theme)}
/>
</Stack.Navigator>
);
};

View file

@ -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 = () => {
<Stack.Navigator screenOptions={{ headerShadowVisible: false }}>
<Stack.Screen
name="ReorderWalletsScreen"
component={ReorderWalletsComponent}
component={ReorderWallets}
options={navigationStyle({
headerBackVisible: false,
headerLargeTitle: true,

View file

@ -1,12 +1,12 @@
import React, { Suspense, lazy } from 'react';
import { NativeStackNavigationOptions, createNativeStackNavigator } from '@react-navigation/native-stack';
import { useStorage } from './blue_modules/storage-context';
import UnlockWith from './screen/UnlockWith';
import { LazyLoadingIndicator } from './navigation/LazyLoadingIndicator';
import { isHandset } from './blue_modules/environment';
import { useStorage } from '../blue_modules/storage-context';
import UnlockWith from '../screen/UnlockWith';
import { LazyLoadingIndicator } from './LazyLoadingIndicator';
import { isHandset } from '../blue_modules/environment';
const DetailViewScreensStack = lazy(() => import('./navigation/DetailViewScreensStack'));
const DrawerRoot = lazy(() => import('./navigation/DrawerRoot'));
const DetailViewScreensStack = lazy(() => import('./DetailViewScreensStack'));
const DrawerRoot = lazy(() => import('./DrawerRoot'));
export const NavigationDefaultOptions: NativeStackNavigationOptions = {
headerShown: false,

124
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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();
}

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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 (
<SafeArea>
<KeyboardAvoidingView enabled={!isTablet()} behavior={Platform.OS === 'ios' ? 'position' : undefined}>
<KeyboardAvoidingView enabled={!isTablet} behavior={Platform.OS === 'ios' ? 'position' : undefined}>
<View style={styles.wrapper} testID="BroadcastView">
{BROADCAST_RESULT.success !== broadcastResult && (
<BlueCard style={styles.mainCard}>

View file

@ -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;
}
}

View file

@ -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);
}, []),
);

View file

@ -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;
}

View file

@ -123,24 +123,24 @@ const SettingsPrivacy: React.FC = () => {
</Pressable>
</BlueCard>
<BlueSpacing20 />
{!storageIsEncrypted && (
<>
<ListItem
hideChevron
title={loc.settings.privacy_quickactions}
Component={TouchableWithoutFeedback}
switch={{
onValueChange: onQuickActionsValueChange,
value: isQuickActionsEnabled,
disabled: isLoading === SettingsPrivacySection.All,
testID: 'QuickActionsSwitch',
}}
/>
<BlueCard>
<BlueText>{loc.settings.privacy_quickactions_explanation}</BlueText>
</BlueCard>
</>
)}
<ListItem
hideChevron
title={loc.settings.privacy_quickactions}
Component={TouchableWithoutFeedback}
switch={{
onValueChange: onQuickActionsValueChange,
value: storageIsEncrypted ? false : isQuickActionsEnabled,
disabled: isLoading === SettingsPrivacySection.All || storageIsEncrypted,
testID: 'QuickActionsSwitch',
}}
/>
{}
<BlueCard>
<BlueText>{loc.settings.privacy_quickactions_explanation}</BlueText>
<BlueSpacing20 />
{storageIsEncrypted && <BlueText>{loc.settings.encrypted_feature_disabled}</BlueText>}
</BlueCard>
<ListItem
hideChevron
title={loc.settings.privacy_do_not_track}
@ -150,7 +150,7 @@ const SettingsPrivacy: React.FC = () => {
<BlueCard>
<BlueText>{loc.settings.privacy_do_not_track_explanation}</BlueText>
</BlueCard>
{Platform.OS === 'ios' && !storageIsEncrypted && (
{Platform.OS === 'ios' && (
<>
<BlueSpacing40 />
<Text adjustsFontSizeToFit style={[styles.widgetsHeader, styleHooks.widgetsHeader]}>
@ -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,
}}
/>
<BlueCard>
<BlueText>{loc.settings.total_balance_explanation}</BlueText>
<BlueSpacing20 />
{storageIsEncrypted && <BlueText>{loc.settings.encrypted_feature_disabled}</BlueText>}
</BlueCard>
</>
)}

View file

@ -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 => {

View file

@ -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 ? (
<>
<BlueText />
<BlueText>{loc.formatString(loc.settings.biometrics_fail, { type: biometrics.biometricsType })}</BlueText>
<BlueText>{loc.formatString(loc.settings.biometrics_fail, { type: deviceBiometricType })}</BlueText>
</>
) : null;
};
@ -147,18 +139,18 @@ const EncryptStorage = () => {
) : (
<ScrollView contentContainerStyle={styles.root} automaticallyAdjustContentInsets contentInsetAdjustmentBehavior="automatic">
<View style={styles.paddingTop} />
{biometrics.isDeviceBiometricCapable && (
{deviceBiometricCapable && (
<>
<Text adjustsFontSizeToFit style={[styles.headerText, styleHooks.headerText]}>
{loc.settings.biometrics}
</Text>
<ListItem
title={loc.formatString(loc.settings.encrypt_use, { type: biometrics.biometricsType })}
title={loc.formatString(loc.settings.encrypt_use, { type: deviceBiometricType })}
Component={TouchableWithoutFeedback}
switch={{ value: biometrics.isBiometricsEnabled, onValueChange: onUseBiometricSwitch }}
switch={{ value: biometricEnabled, onValueChange: onUseBiometricSwitch }}
/>
<BlueCard>
<BlueText>{loc.formatString(loc.settings.encrypt_use_expl, { type: biometrics.biometricsType })}</BlueText>
<BlueText>{loc.formatString(loc.settings.encrypt_use_expl, { type: deviceBiometricType })}</BlueText>
{renderPasscodeExplanation()}
</BlueCard>
<BlueSpacing20 />

View file

@ -23,6 +23,22 @@ interface TransactionDetailsProps {
navigation: NativeStackNavigationProp<any>;
}
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<TransactionDetailsProps['route']>().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 = () => {
)}
<ToolTipMenu
isButton
actions={[
{
id: TransactionDetails.actionKeys.CopyToClipboard,
text: loc.transactions.copy_link,
icon: TransactionDetails.actionIcons.Clipboard,
},
]}
actions={toolTipMenuActions}
onPressMenuItem={handleCopyPress}
onPress={handleOnOpenTransactionOnBlockExplorerTapped}
buttonStyle={[styles.greyButton, stylesHooks.greyButton]}
buttonStyle={StyleSheet.flatten([styles.greyButton, stylesHooks.greyButton])}
>
<Text style={[styles.Link, stylesHooks.Link]}>{loc.transactions.details_show_in_block_explorer}</Text>
</ToolTipMenu>
@ -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,

View file

@ -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<PaymentCodeStackParamList, 'PaymentCodesList'>;
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<number>(0);
const [data, setData] = useState<DataSection[]>([]);
const { colors } = useTheme();
const [isLoading, setIsLoading] = useState<boolean>(false);
const [loadingText, setLoadingText] = useState<string>('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 (
<ToolTipMenu
actions={toolTipActions}
onPressMenuItem={(item: any) => onToolTipPress(item, pc)}
isButton={true}
isMenuPrimaryAction={true}
>
<View style={styles.contactRowContainer}>
<View style={[styles.circle, { backgroundColor: '#' + color }]} />
<View style={styles.contactRowBody}>
<Text style={[styles.contactRowNameText, { color: colors.shadowColor }]}>{displayName}</Text>
</View>
</View>
<View style={styles.stick} />
</ToolTipMenu>
);
};
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 (
<View style={styles.container}>
<BlueLoading />
<Text>{loadingText}</Text>
</View>
);
}
return (
<View style={styles.container}>
{!walletID ? (
<Text>Internal error</Text>
) : (
<View style={styles.sectionListContainer}>
<SectionList sections={data} keyExtractor={(item, index) => item + index} renderItem={({ item }) => renderItem(item)} />
</View>
)}
<Button title={loc.bip47.add_contact} onPress={onAddContactPress} />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
sectionListContainer: { flex: 1, width: '100%' },
circle: {
width: 35,
height: 35,
borderRadius: 25,
},
contactRowBody: { flex: 6, justifyContent: 'center', top: -3 },
contactRowNameText: { marginLeft: 10, fontSize: 16 },
contactRowContainer: { flexDirection: 'row', padding: 15 },
stick: { borderStyle: 'solid', borderWidth: 0.5, borderColor: 'gray', opacity: 0.5, top: 0, left: -10, width: '110%' },
});

View file

@ -31,7 +31,6 @@ import * as NavigationService from '../../NavigationService';
import { useStorage } from '../../blue_modules/storage-context';
import { encodeUR } from '../../blue_modules/ur';
import { HDSegwitBech32Wallet, MultisigCosigner, MultisigHDWallet } from '../../class';
import Biometric from '../../class/biometrics';
import presentAlert from '../../components/Alert';
import BottomModal from '../../components/BottomModal';
import Button from '../../components/Button';
@ -52,11 +51,13 @@ import SaveFileButton from '../../components/SaveFileButton';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import prompt from '../../helpers/prompt';
import { useSettings } from '../../components/Context/SettingsContext';
import { useBiometrics } from '../../hooks/useBiometrics';
const ViewEditMultisigCosigners: React.FC = () => {
const hasLoaded = useRef(false);
const { colors } = useTheme();
const { wallets, setWalletsWithNewOrder, isElectrumDisabled } = useStorage();
const { isBiometricUseCapableAndEnabled, unlockWithBiometrics } = useBiometrics();
const { isAdvancedModeEnabled } = useSettings();
const { navigate, dispatch, addListener } = useExtendedNavigation();
const openScannerButtonRef = useRef();
@ -173,10 +174,10 @@ const ViewEditMultisigCosigners: React.FC = () => {
}
setIsLoading(true);
const isBiometricsEnabled = await Biometric.isBiometricUseCapableAndEnabled();
const isBiometricsEnabled = await isBiometricUseCapableAndEnabled();
if (isBiometricsEnabled) {
if (!(await Biometric.unlockWithBiometrics())) {
if (!(await unlockWithBiometrics())) {
setIsLoading(false);
return;
}

View file

@ -1,5 +1,5 @@
import { useRoute } from '@react-navigation/native';
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
ActivityIndicator,
Alert,
@ -20,7 +20,7 @@ import {
import { BlueCard, BlueLoading, BlueSpacing10, 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 { useStorage } from '../../blue_modules/storage-context';
import {
HDAezeedWallet,
HDSegwitBech32Wallet,
@ -31,7 +31,6 @@ import {
SegwitP2SHWallet,
WatchOnlyWallet,
} from '../../class';
import Biometric from '../../class/biometrics';
import { AbstractHDElectrumWallet } from '../../class/wallets/abstract-hd-electrum-wallet';
import { LightningCustodianWallet } from '../../class/wallets/lightning-custodian-wallet';
import presentAlert from '../../components/Alert';
@ -47,6 +46,7 @@ import SaveFileButton from '../../components/SaveFileButton';
import { useSettings } from '../../components/Context/SettingsContext';
import HeaderRightButton from '../../components/HeaderRightButton';
import { writeFileAndExport } from '../../blue_modules/fs';
import { useBiometrics } from '../../hooks/useBiometrics';
const styles = StyleSheet.create({
scrollViewContent: {
@ -107,7 +107,8 @@ const styles = StyleSheet.create({
});
const WalletDetails = () => {
const { saveToDisk, wallets, deleteWallet, setSelectedWalletID, txMetadata } = useContext(BlueStorageContext);
const { saveToDisk, wallets, deleteWallet, setSelectedWalletID, txMetadata } = useStorage();
const { isBiometricUseCapableAndEnabled, unlockWithBiometrics } = useBiometrics();
const { walletID } = useRoute().params;
const [isLoading, setIsLoading] = useState(false);
const [backdoorPressed, setBackdoorPressed] = useState(0);
@ -402,10 +403,10 @@ const WalletDetails = () => {
{
text: loc.wallets.details_yes_delete,
onPress: async () => {
const isBiometricsEnabled = await Biometric.isBiometricUseCapableAndEnabled();
const isBiometricsEnabled = await isBiometricUseCapableAndEnabled();
if (isBiometricsEnabled) {
if (!(await Biometric.unlockWithBiometrics())) {
if (!(await unlockWithBiometrics())) {
return;
}
}

View file

@ -1,72 +0,0 @@
import React, { useContext, useEffect, 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 from '../../loc';
import CopyTextToClipboard from '../../components/CopyTextToClipboard';
import { AbstractHDElectrumWallet } from '../../class/wallets/abstract-hd-electrum-wallet';
import { PaymentCodeStackParamList } from '../../navigation/PaymentCodeStack';
interface DataSection {
title: string;
data: string[];
}
type Props = NativeStackScreenProps<PaymentCodeStackParamList, 'PaymentCodesList'>;
export default function PaymentCodesList() {
const route = useRoute();
const { walletID } = route.params as Props['route']['params'];
const { wallets } = useContext(BlueStorageContext);
const [data, setData] = useState<DataSection[]>([]);
useEffect(() => {
if (!walletID) return;
const foundWallet = wallets.find(w => w.getID() === walletID) as unknown as AbstractHDElectrumWallet;
if (!foundWallet) return;
const newData: DataSection[] = [
{
title: loc.bip47.who_can_pay_me,
data: foundWallet.getBIP47SenderPaymentCodes(),
},
{
title: loc.bip47.whom_can_i_pay,
data: foundWallet.getBIP47ReceiverPaymentCodes(),
},
];
setData(newData);
}, [walletID, wallets]);
return (
<View style={styles.container}>
{!walletID ? (
<Text>Internal error</Text>
) : (
<View>
<SectionList
sections={data}
keyExtractor={(item, index) => item + index}
renderItem={({ item }) => (
<View>
<CopyTextToClipboard truncated text={item} />
</View>
)}
renderSectionHeader={({ section: { title } }) => <Text style={styles.titleText}>{title}</Text>}
/>
</View>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
titleText: { fontSize: 20 },
});

View file

@ -1,6 +1,6 @@
import { useFocusEffect, useRoute } from '@react-navigation/native';
import PropTypes from 'prop-types';
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import {
ActivityIndicator,
Alert,
@ -24,9 +24,8 @@ import BlueClipboard from '../../blue_modules/clipboard';
import { isDesktop } from '../../blue_modules/environment';
import * as fs from '../../blue_modules/fs';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import { BlueStorageContext, WalletTransactionsStatus } from '../../blue_modules/storage-context';
import { WalletTransactionsStatus, useStorage } from '../../blue_modules/storage-context';
import { LightningCustodianWallet, LightningLdkWallet, MultisigHDWallet, WatchOnlyWallet } from '../../class';
import Biometric from '../../class/biometrics';
import WalletGradient from '../../class/wallet-gradient';
import presentAlert from '../../components/Alert';
import { FButton, FContainer } from '../../components/FloatButtons';
@ -41,6 +40,7 @@ import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import loc from '../../loc';
import { Chain } from '../../models/bitcoinUnits';
import ActionSheet from '../ActionSheet';
import { useBiometrics } from '../../hooks/useBiometrics';
const buttonFontSize =
PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26) > 22
@ -55,7 +55,8 @@ const WalletTransactions = ({ navigation }) => {
walletTransactionUpdateStatus,
isElectrumDisabled,
setReloadTransactionsMenuActionFunction,
} = useContext(BlueStorageContext);
} = useStorage();
const { isBiometricUseCapableAndEnabled, unlockWithBiometrics } = useBiometrics();
const [isLoading, setIsLoading] = useState(false);
const { walletID } = useRoute().params;
const { name } = useRoute();
@ -485,10 +486,10 @@ const WalletTransactions = ({ navigation }) => {
})
}
onWalletBalanceVisibilityChange={async isShouldBeVisible => {
const isBiometricsEnabled = await Biometric.isBiometricUseCapableAndEnabled();
const isBiometricsEnabled = await isBiometricUseCapableAndEnabled();
if (wallet.hideBalance && isBiometricsEnabled) {
const unlocked = await Biometric.unlockWithBiometrics();
const unlocked = await unlockWithBiometrics();
if (!unlocked) {
throw new Error('Biometrics failed');
}

View file

@ -3,7 +3,7 @@ const path = require('path');
const mainLocFile = './loc/en.json';
const dirsToInterate = ['components', 'screen', 'blue_modules', 'class', 'hooks', 'helpers', 'navigation'];
const addFiles = ['BlueComponents.js', 'App.js', 'Navigation.tsx'];
const addFiles = ['BlueComponents.js', 'App.tsx', 'navigation/index.tsx'];
const allowedLocPrefixes = ['loc.lnurl_auth', 'loc.units'];
const allLocKeysHashmap = {}; // loc key -> used or not
@ -32,7 +32,7 @@ for (const dir of dirsToInterate) {
for (const filename of addFiles) {
allDirFiles.push(path.resolve(filename));
}
allDirFiles.push(path.resolve('App.js'));
allDirFiles.push(path.resolve('App.tsx'));
// got all source files

View file

@ -1,18 +0,0 @@
--- ../node_modules/react-native-widget-center/android/build.gradle 2023-11-10 11:25:36
+++ ../node_modules/react-native-widget-center/android/build.gradle 2023-11-10 11:25:42
@@ -103,12 +103,12 @@
}
task androidJavadocJar(type: Jar, dependsOn: androidJavadoc) {
- classifier = 'javadoc'
+ archiveClassifier.set('javadoc')
from androidJavadoc.destinationDir
}
task androidSourcesJar(type: Jar) {
- classifier = 'sources'
+ archiveClassifier.set('sources')
from android.sourceSets.main.java.srcDirs
include '**/*.java'
}

View file

@ -164,12 +164,11 @@ describe('Bech32 Segwit HD (BIP84) with BIP47', () => {
assert.ok(walletSender.getTransactions().length >= 3);
assert.ok(walletSender._receive_payment_codes.length === 1);
assert.strictEqual(walletSender.needToNotifyBIP47(bip47instanceReceiver.getSerializedPaymentCode()), false); // already notified in the past
assert.strictEqual(
walletSender.needToNotifyBIP47(
assert.ok(walletSender.getBIP47NotificationTransaction(bip47instanceReceiver.getSerializedPaymentCode())); // already notified in the past
assert.ok(
!walletSender.getBIP47NotificationTransaction(
'PM8TJdfXvRasx4WNpxky25ZKxhvfEiGYW9mka92tfiqDRSL7LQdxnC8uAk9k3okXctZowVwY2PUndjCQR6DHyuVVwqmy2aodmZNHgfFZcJRNTuBAXJCp',
),
true,
); // random PC from interwebz. never interacted with him, so need to notify
});
@ -201,7 +200,7 @@ describe('Bech32 Segwit HD (BIP84) with BIP47', () => {
); // sparrow paid us after sparrow made a notification tx
assert.ok(
!w.needToNotifyBIP47(
w.getBIP47NotificationTransaction(
'PM8TJi1RuCrgSHTzGMoayUf8xUW6zYBGXBPSWwTiMhMMwqto7G6NA4z9pN5Kn8Pbhryo2eaHMFRRcidCGdB3VCDXJD4DdPD2ZyG3ScLMEvtStAetvPMo',
),
);
@ -216,7 +215,7 @@ describe('Bech32 Segwit HD (BIP84) with BIP47', () => {
'PM8TJi1RuCrgSHTzGMoayUf8xUW6zYBGXBPSWwTiMhMMwqto7G6NA4z9pN5Kn8Pbhryo2eaHMFRRcidCGdB3VCDXJD4DdPD2ZyG3ScLMEvtStAetvPMo',
);
assert.ok(
!w.needToNotifyBIP47(
w.getBIP47NotificationTransaction(
'PM8TJi1RuCrgSHTzGMoayUf8xUW6zYBGXBPSWwTiMhMMwqto7G6NA4z9pN5Kn8Pbhryo2eaHMFRRcidCGdB3VCDXJD4DdPD2ZyG3ScLMEvtStAetvPMo',
),
); // dont need to notify
@ -258,4 +257,51 @@ describe('Bech32 Segwit HD (BIP84) with BIP47', () => {
'PM8TJi1RuCrgSHTzGMoayUf8xUW6zYBGXBPSWwTiMhMMwqto7G6NA4z9pN5Kn8Pbhryo2eaHMFRRcidCGdB3VCDXJD4DdPD2ZyG3ScLMEvtStAetvPMo',
);
});
it('can tell with which counterparty PC transaction is (sparrow)', async () => {
if (!process.env.BIP47_HD_MNEMONIC) {
console.error('process.env.BIP47_HD_MNEMONIC not set, skipped');
return;
}
const w = new HDSegwitBech32Wallet();
w.setSecret(process.env.BIP47_HD_MNEMONIC.split(':')[1]);
w.switchBIP47(true);
await w.fetchBIP47SenderPaymentCodes();
await w.fetchBalance();
await w.fetchTransactions();
assert.strictEqual(
w.getTransactions().find(tx => tx.txid === '64058a49bb75481fc0bebbb0d84a4aceebe319f9d32929e73cefb21d83342e9f')?.value,
-102308,
); // we paid samurai
assert.deepStrictEqual(w.getBIP47SenderPaymentCodes(), [
'PM8TJXuZNUtSibuXKFM6bhCxpNaSye6r4px2GXRV5v86uRdH9Raa8ZtXEkG7S4zLREf4ierjMsxLXSFTbRVUnRmvjw9qnc7zZbyXyBstSmjcb7uVcDYF',
]); // samurai can pay us
assert.deepStrictEqual(w.getBIP47ReceiverPaymentCodes(), []); // we can pay no-one
assert.ok(!w.getBip47CounterpartyByTxid('64058a49bb75481fc0bebbb0d84a4aceebe319f9d32929e73cefb21d83342e9f')); // dont know whom we paid
w.addBIP47Receiver(
'PM8TJXuZNUtSibuXKFM6bhCxpNaSye6r4px2GXRV5v86uRdH9Raa8ZtXEkG7S4zLREf4ierjMsxLXSFTbRVUnRmvjw9qnc7zZbyXyBstSmjcb7uVcDYF',
);
assert.deepStrictEqual(w.getBIP47ReceiverPaymentCodes(), [
'PM8TJXuZNUtSibuXKFM6bhCxpNaSye6r4px2GXRV5v86uRdH9Raa8ZtXEkG7S4zLREf4ierjMsxLXSFTbRVUnRmvjw9qnc7zZbyXyBstSmjcb7uVcDYF',
]); // we can now pay samurai
// await w.fetchBIP47SenderPaymentCodes();
// await w.fetchBalance();
// await w.fetchTransactions();
await w.syncBip47ReceiversAddresses(
'PM8TJXuZNUtSibuXKFM6bhCxpNaSye6r4px2GXRV5v86uRdH9Raa8ZtXEkG7S4zLREf4ierjMsxLXSFTbRVUnRmvjw9qnc7zZbyXyBstSmjcb7uVcDYF',
);
assert.strictEqual(
w.getBip47CounterpartyByTxid('64058a49bb75481fc0bebbb0d84a4aceebe319f9d32929e73cefb21d83342e9f'),
'PM8TJXuZNUtSibuXKFM6bhCxpNaSye6r4px2GXRV5v86uRdH9Raa8ZtXEkG7S4zLREf4ierjMsxLXSFTbRVUnRmvjw9qnc7zZbyXyBstSmjcb7uVcDYF',
); // we paid samurai, and now we can seethat
});
});

View file

@ -59,6 +59,7 @@ jest.mock('react-native-device-info', () => {
getDeviceType: jest.fn().mockReturnValue(false),
hasGmsSync: jest.fn().mockReturnValue(true),
hasHmsSync: jest.fn().mockReturnValue(false),
isTablet: jest.fn().mockReturnValue(false),
};
});
@ -187,12 +188,6 @@ jest.mock('react-native-share', () => {
};
});
jest.mock('../components/WidgetCommunication', () => {
return {
reloadAllTimelines: jest.fn(),
};
});
const mockKeychain = {
SECURITY_LEVEL_ANY: 'MOCK_SECURITY_LEVEL_ANY',
SECURITY_LEVEL_SECURE_SOFTWARE: 'MOCK_SECURITY_LEVEL_SECURE_SOFTWARE',
@ -205,6 +200,6 @@ jest.mock('react-native-keychain', () => mockKeychain);
jest.mock('react-native-tcp-socket', () => mockKeychain);
jest.mock('../components/TooltipMenu.ios.js', () => require('../components/TooltipMenu.js'));
jest.mock('../components/TooltipMenu.ios.tsx', () => require('../components/TooltipMenu.tsx'));
global.alert = () => {};

View file

@ -0,0 +1,19 @@
import assert from 'assert';
import { HDSegwitBech32Wallet } from '../../class';
import { ContactList } from '../../class/contact-list';
describe('ContactList', () => {
it('isPaymentCodeValid()', async () => {
const w = new HDSegwitBech32Wallet();
w.switchBIP47(true);
const cl = new ContactList(w);
assert.ok(!cl.isPaymentCodeValid('sfdgsfdghsfd'));
assert.ok(
cl.isPaymentCodeValid(
'PM8TJS2JxQ5ztXUpBBRnpTbcUXbUHy2T1abfrb3KkAAtMEGNbey4oumH7Hc578WgQJhPjBxteQ5GHHToTYHE3A1w6p7tU6KSoFmWBVbFGjKPisZDbP97',
),
);
});
});