OPS: Upgrade RNav 7 (#7419)

This commit is contained in:
Marcos Rodriguez Vélez 2025-02-17 15:24:05 -04:00 committed by GitHub
parent 632500b734
commit d338f813cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
60 changed files with 1384 additions and 1019 deletions

View File

@ -47,6 +47,24 @@
"device": "emulator",
"app": "android.debug"
},
"android.debug.device": {
"device": {
"device": {
"adbName": ".*"
},
"type": "android.attached"
},
"app": "android.debug"
},
"android.release.device": {
"device": {
"device": {
"adbName": ".*"
},
"type": "android.attached"
},
"app": "android.release"
},
"android.release": {
"device": "emulator",
"app": "android.release"

17
App.tsx
View File

@ -1,5 +1,3 @@
import 'react-native-gesture-handler'; // should be on top
import { NavigationContainer } from '@react-navigation/native';
import React from 'react';
import { useColorScheme } from 'react-native';
@ -9,23 +7,26 @@ import { SettingsProvider } from './components/Context/SettingsProvider';
import { BlueDarkTheme, BlueDefaultTheme } from './components/themes';
import MasterView from './navigation/MasterView';
import { navigationRef } from './NavigationService';
import { useLogger } from '@react-navigation/devtools';
import { StorageProvider } from './components/Context/StorageProvider';
const App = () => {
const colorScheme = useColorScheme();
useLogger(navigationRef);
return (
<LargeScreenProvider>
<NavigationContainer ref={navigationRef} theme={colorScheme === 'dark' ? BlueDarkTheme : BlueDefaultTheme}>
<SafeAreaProvider>
<NavigationContainer ref={navigationRef} theme={colorScheme === 'dark' ? BlueDarkTheme : BlueDefaultTheme}>
<SafeAreaProvider>
<LargeScreenProvider>
<StorageProvider>
<SettingsProvider>
<MasterView />
</SettingsProvider>
</StorageProvider>
</SafeAreaProvider>
</NavigationContainer>
</LargeScreenProvider>
</LargeScreenProvider>
</SafeAreaProvider>
</NavigationContainer>
);
};

View File

@ -82,6 +82,7 @@ export const BlueFormMultiInput = props => {
multiline
underlineColorAndroid="transparent"
numberOfLines={4}
editable={!props.editable}
style={{
paddingHorizontal: 8,
paddingVertical: 16,

View File

@ -14,10 +14,6 @@ export function dispatch(action: NavigationAction) {
}
}
export function navigateToWalletsList() {
navigate('WalletsList');
}
export function reset() {
if (navigationRef.isReady()) {
navigationRef.current?.reset({

View File

@ -19,10 +19,10 @@
<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" />
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.ACTION_SCREEN_OFF"/>
<uses-permission android:name="android.permission.ACTION_SCREEN_ON"/>
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<application
android:name=".MainApplication"

View File

@ -4,6 +4,7 @@ import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.util.Log
import android.widget.Toast
import androidx.work.WorkManager
class BitcoinPriceWidget : AppWidgetProvider() {
@ -27,7 +28,8 @@ class BitcoinPriceWidget : AppWidgetProvider() {
val sharedPref = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
val widgetCount = sharedPref.getInt(WIDGET_COUNT_KEY, 0)
if (widgetCount >= 1) {
Log.e(TAG, "Only one widget instance is allowed.")
Toast.makeText(context, "Only one widget instance is allowed.", Toast.LENGTH_SHORT).show()
Log.e(TAG, "Attempted to add multiple widget instances.")
return
}
sharedPref.edit().putInt(WIDGET_COUNT_KEY, widgetCount + 1).apply()
@ -37,9 +39,7 @@ class BitcoinPriceWidget : AppWidgetProvider() {
override fun onDisabled(context: Context) {
super.onDisabled(context)
val sharedPref = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
val widgetCount = sharedPref.getInt(WIDGET_COUNT_KEY, 1)
sharedPref.edit().putInt(WIDGET_COUNT_KEY, widgetCount - 1).apply()
clearWidgetCount(context)
Log.d(TAG, "onDisabled called")
clearCache(context)
WorkManager.getInstance(context).cancelUniqueWork(WidgetUpdateWorker.WORK_NAME)
@ -49,10 +49,17 @@ class BitcoinPriceWidget : AppWidgetProvider() {
super.onDeleted(context, appWidgetIds)
val sharedPref = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
val widgetCount = sharedPref.getInt(WIDGET_COUNT_KEY, 1)
sharedPref.edit().putInt(WIDGET_COUNT_KEY, widgetCount - appWidgetIds.size).apply()
val newCount = widgetCount - appWidgetIds.size
sharedPref.edit().putInt(WIDGET_COUNT_KEY, if (newCount >= 0) newCount else 0).apply()
Log.d(TAG, "onDeleted called for widgets: ${appWidgetIds.joinToString()}")
}
private fun clearWidgetCount(context: Context) {
val sharedPref = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
sharedPref.edit().putInt(WIDGET_COUNT_KEY, 0).apply()
Log.d(TAG, "Widget count reset to 0")
}
private fun clearCache(context: Context) {
val sharedPref = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
sharedPref.edit().clear().apply()

View File

@ -5,7 +5,7 @@ import { useTheme } from './themes';
import ToolTipMenu from './TooltipMenu';
import { CommonToolTipActions } from '../typings/CommonToolTipActions';
import loc from '../loc';
import { navigationRef } from '../NavigationService';
import { useExtendedNavigation } from '../hooks/useExtendedNavigation';
type AddWalletButtonProps = {
onPress?: (event: GestureResponderEvent) => void;
@ -23,21 +23,25 @@ const styles = StyleSheet.create({
const AddWalletButton: React.FC<AddWalletButtonProps> = ({ onPress }) => {
const { colors } = useTheme();
const navigation = useExtendedNavigation();
const stylesHook = StyleSheet.create({
ball: {
backgroundColor: colors.buttonBackgroundColor,
},
});
const onPressMenuItem = useCallback((action: string) => {
switch (action) {
case CommonToolTipActions.ImportWallet.id:
navigationRef.current?.navigate('AddWalletRoot', { screen: 'ImportWallet' });
break;
default:
break;
}
}, []);
const onPressMenuItem = useCallback(
(action: string) => {
switch (action) {
case CommonToolTipActions.ImportWallet.id:
navigation.navigate('AddWalletRoot', { screen: 'ImportWallet' });
break;
default:
break;
}
},
[navigation],
);
const actions = useMemo(() => [CommonToolTipActions.ImportWallet], []);

View File

@ -11,7 +11,6 @@ interface AddressInputProps {
address?: string;
placeholder?: string;
onChangeText: (text: string) => void;
scanButtonTapped?: () => void;
editable?: boolean;
inputAccessoryViewID?: string;
onFocus?: () => void;
@ -41,7 +40,6 @@ const AddressInput = ({
testID = 'AddressInput',
placeholder = loc.send.details_address,
onChangeText,
scanButtonTapped = () => {},
editable = true,
inputAccessoryViewID,
onFocus = () => {},
@ -104,7 +102,7 @@ const AddressInput = ({
keyboardType={keyboardType}
{...(skipValidation ? { onBlur } : { onBlur: onBlurEditing })}
/>
{editable ? <AddressInputScanButton isLoading={isLoading} scanButtonTapped={scanButtonTapped} onChangeText={onChangeText} /> : null}
{editable ? <AddressInputScanButton isLoading={isLoading} onChangeText={onChangeText} /> : null}
</View>
);
};

View File

@ -13,11 +13,14 @@ import { useExtendedNavigation } from '../hooks/useExtendedNavigation';
interface AddressInputScanButtonProps {
isLoading: boolean;
scanButtonTapped: () => void;
onChangeText: (text: string) => void;
}
export const AddressInputScanButton = ({ isLoading, scanButtonTapped, onChangeText }: AddressInputScanButtonProps) => {
export const AddressInputScanButton = ({
isLoading,
onChangeText,
}: AddressInputScanButtonProps) => {
const { colors } = useTheme();
const { isClipboardGetContentEnabled } = useSettings();
@ -32,12 +35,11 @@ export const AddressInputScanButton = ({ isLoading, scanButtonTapped, onChangeTe
});
const toolTipOnPress = useCallback(async () => {
await scanButtonTapped();
Keyboard.dismiss();
navigation.navigate('ScanQRCode', {
showFileImportButton: true,
});
}, [navigation, scanButtonTapped]);
}, [navigation]);
const actions = useMemo(() => {
const availableActions = [
@ -57,7 +59,6 @@ export const AddressInputScanButton = ({ isLoading, scanButtonTapped, onChangeTe
async (action: string) => {
switch (action) {
case CommonToolTipActions.ScanQR.id:
scanButtonTapped();
navigation.navigate('ScanQRCode', {
showFileImportButton: true,
});
@ -124,7 +125,7 @@ export const AddressInputScanButton = ({ isLoading, scanButtonTapped, onChangeTe
}
Keyboard.dismiss();
},
[navigation, onChangeText, scanButtonTapped],
[navigation, onChangeText],
);
const buttonStyle = useMemo(() => [styles.scan, stylesHook.scan], [stylesHook.scan]);

View File

@ -1,5 +1,3 @@
import 'react-native-gesture-handler'; // should be on top
import { CommonActions } from '@react-navigation/native';
import { useCallback, useEffect, useRef } from 'react';
import { AppState, AppStateStatus, Linking } from 'react-native';

View File

@ -351,10 +351,10 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
return;
}
const emptyWalletLabel = new LegacyWallet().getLabel();
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
if (w.getLabel() === emptyWalletLabel) w.setLabel(loc.wallets.import_imported + ' ' + w.typeReadable);
w.setUserHasSavedExport(true);
addWallet(w);
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
await saveToDisk();
A(A.ENUM.CREATED_WALLET);
presentAlert({

View File

@ -1,5 +1,5 @@
import React, { useState, useRef, forwardRef, useImperativeHandle, useEffect } from 'react';
import { View, Text, TextInput, StyleSheet, Animated, Easing, ViewStyle, Keyboard, Platform, UIManager, ScrollView } from 'react-native';
import { View, Text, TextInput, StyleSheet, Animated, Easing, ViewStyle, Keyboard, Platform, UIManager } from 'react-native';
import BottomModal, { BottomModalHandle } from './BottomModal';
import { useTheme } from '../components/themes';
import loc from '../loc';
@ -43,11 +43,10 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
const fadeInAnimation = useRef(new Animated.Value(0)).current;
const scaleAnimation = useRef(new Animated.Value(1)).current;
const shakeAnimation = useRef(new Animated.Value(0)).current;
const explanationOpacity = useRef(new Animated.Value(1)).current; // New animated value for opacity
const explanationOpacity = useRef(new Animated.Value(1)).current;
const { colors } = useTheme();
const passwordInputRef = useRef<TextInput>(null);
const confirmPasswordInputRef = useRef<TextInput>(null);
const scrollView = useRef<ScrollView>(null);
const { isVisible } = useKeyboard();
const stylesHook = StyleSheet.create({
@ -103,42 +102,43 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [modalType]);
const handleShakeAnimation = () => {
const performShake = (shakeAnimRef: Animated.Value) => {
Animated.sequence([
Animated.timing(shakeAnimation, {
Animated.timing(shakeAnimRef, {
toValue: 10,
duration: 100,
easing: Easing.inOut(Easing.ease),
useNativeDriver: true,
}),
Animated.timing(shakeAnimation, {
Animated.timing(shakeAnimRef, {
toValue: -10,
duration: 100,
easing: Easing.inOut(Easing.ease),
useNativeDriver: true,
}),
Animated.timing(shakeAnimation, {
Animated.timing(shakeAnimRef, {
toValue: 5,
duration: 100,
easing: Easing.inOut(Easing.ease),
useNativeDriver: true,
}),
Animated.timing(shakeAnimation, {
Animated.timing(shakeAnimRef, {
toValue: -5,
duration: 100,
easing: Easing.inOut(Easing.ease),
useNativeDriver: true,
}),
Animated.timing(shakeAnimation, {
Animated.timing(shakeAnimRef, {
toValue: 0,
duration: 100,
easing: Easing.inOut(Easing.ease),
useNativeDriver: true,
}),
]).start(() => {
confirmPasswordInputRef.current?.focus();
confirmPasswordInputRef.current?.setNativeProps({ selection: { start: 0, end: confirmPassword.length } });
});
]).start();
};
const handleShakeAnimation = () => {
performShake(shakeAnimation);
};
const handleSuccessAnimation = () => {
@ -180,6 +180,17 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
});
};
const handleConfirmationFailure = () => {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
if (!isSuccess) handleShakeAnimation();
onConfirmationFailure();
};
const handleConfirmSuccess = () => {
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
handleSuccessAnimation();
};
const handleSubmit = async () => {
Keyboard.dismiss();
setIsLoading(true);
@ -189,37 +200,13 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
if (modalType === MODAL_TYPES.CREATE_PASSWORD || modalType === MODAL_TYPES.CREATE_FAKE_STORAGE) {
if (password === confirmPassword && password) {
success = await onConfirmationSuccess(password);
if (success) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
handleSuccessAnimation();
} else {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
onConfirmationFailure();
if (!isSuccess) {
// Prevent shake animation if success is detected
handleShakeAnimation();
}
}
success ? handleConfirmSuccess() : handleConfirmationFailure();
} else {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
if (!isSuccess) {
// Prevent shake animation if success is detected
handleShakeAnimation();
}
handleConfirmationFailure();
}
} else if (modalType === MODAL_TYPES.ENTER_PASSWORD) {
success = await onConfirmationSuccess(password);
if (success) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
handleSuccessAnimation();
} else {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
if (!isSuccess) {
// Prevent shake animation if success is detected
handleShakeAnimation();
}
onConfirmationFailure();
}
success ? handleConfirmSuccess() : handleConfirmationFailure();
}
} finally {
setIsLoading(false); // Ensure loading state is reset
@ -258,17 +245,18 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
onConfirmationFailure();
};
const opacity = isVisible ? 0 : 1;
return (
<BottomModal
ref={modalRef}
onDismiss={onModalDismiss}
onClose={onModalDismiss}
grabber={false}
showCloseButton={!isSuccess}
onCloseModalPressed={handleCancel}
backgroundColor={colors.modal}
isGrabberVisible={!isSuccess}
scrollRef={scrollView}
dismissible={false}
sizes={Platform.OS === 'ios' ? ['auto'] : [420, 'auto']}
footer={
!isSuccess ? (
showExplanation && modalType === MODAL_TYPES.CREATE_PASSWORD ? (
@ -281,16 +269,19 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
/>
</Animated.View>
) : (
<Animated.View style={[{ opacity: fadeOutAnimation, transform: [{ scale: scaleAnimation }] }, styles.feeModalFooter]}>
{!isVisible && (
<SecondButton
title={isLoading ? '' : loc._.ok}
onPress={handleSubmit}
testID="OKButton"
loading={isLoading}
disabled={isLoading || !password || (modalType === MODAL_TYPES.CREATE_PASSWORD && !confirmPassword)}
/>
)}
<Animated.View
style={[
{ opacity: isVisible ? opacity : fadeOutAnimation, transform: [{ scale: scaleAnimation }] },
styles.feeModalFooterSpacing,
]}
>
<SecondButton
title={isLoading ? '' : loc._.ok}
onPress={handleSubmit}
testID="OKButton"
loading={isLoading}
disabled={isLoading || !password || (modalType === MODAL_TYPES.CREATE_PASSWORD && !confirmPassword)}
/>
</Animated.View>
)
) : null
@ -301,14 +292,14 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
{modalType === MODAL_TYPES.CREATE_PASSWORD && showExplanation && (
<Animated.View style={{ opacity: explanationOpacity }}>
<Text style={[styles.textLabel, stylesHook.feeModalLabel]}>{loc.settings.encrypt_storage_explanation_headline}</Text>
<Animated.ScrollView style={styles.explanationScrollView} ref={scrollView}>
<Text style={[styles.description, stylesHook.feeModalCustomText]}>
<Animated.View>
<Text style={[styles.description, stylesHook.feeModalCustomText]} maxFontSizeMultiplier={1.2}>
{loc.settings.encrypt_storage_explanation_description_line1}
</Text>
<Text style={[styles.description, stylesHook.feeModalCustomText]}>
<Text style={[styles.description, stylesHook.feeModalCustomText]} maxFontSizeMultiplier={1.2}>
{loc.settings.encrypt_storage_explanation_description_line2}
</Text>
</Animated.ScrollView>
</Animated.View>
<View style={styles.feeModalFooter} />
</Animated.View>
)}
@ -397,29 +388,30 @@ export default PromptPasswordConfirmationModal;
const styles = StyleSheet.create({
modalContent: {
padding: 22,
width: '100%', // Ensure modal content takes full width
width: '100%',
justifyContent: 'center',
alignItems: 'center',
},
minHeight: {
minHeight: 280,
minHeight: 420,
},
feeModalFooter: {
padding: 16,
},
feeModalFooterSpacing: {
padding: 16,
padding: 24,
marginVertical: 24,
},
inputContainer: {
marginBottom: 10,
width: '100%', // Ensure full width
width: '100%',
},
input: {
borderRadius: 4,
padding: 8,
marginVertical: 8,
fontSize: 16,
width: '100%', // Ensure full width
width: '100%',
},
textLabel: {
fontSize: 20,
@ -435,7 +427,8 @@ const styles = StyleSheet.create({
successContainer: {
justifyContent: 'center',
alignItems: 'center',
height: 100,
margin: 24,
marginBottom: 48,
},
circle: {
width: 60,
@ -449,7 +442,4 @@ const styles = StyleSheet.create({
color: 'white',
fontSize: 30,
},
explanationScrollView: {
maxHeight: 200,
},
});

View File

@ -109,8 +109,8 @@ const SelectFeeModal = forwardRef<BottomModalHandle, SelectFeeModalProps>(
});
useImperativeHandle(ref, () => ({
present: async () => feeModalRef.current?.present(),
dismiss: async () => feeModalRef.current?.dismiss(),
present: async () => await feeModalRef.current?.present(),
dismiss: async () => await feeModalRef.current?.dismiss(),
}));
const options: Option[] = [
@ -163,8 +163,8 @@ const SelectFeeModal = forwardRef<BottomModalHandle, SelectFeeModalProps>(
const handleSelectOption = async (fee: number | null, rate: number) => {
setFeePrecalc(fp => ({ ...fp, current: fee }));
await feeModalRef.current?.dismiss();
setCustomFee(rate.toString());
await feeModalRef.current?.dismiss();
};
return (

View File

@ -1,6 +1,6 @@
import { NativeStackNavigationOptions } from '@react-navigation/native-stack';
import React from 'react';
import { Image, Keyboard, StyleSheet, TouchableOpacity } from 'react-native';
import { Image, Keyboard, Platform, StyleSheet, TouchableOpacity } from 'react-native';
import loc from '../loc';
import { Theme } from './themes';
@ -59,7 +59,7 @@ const navigationStyle = (
{
closeButtonPosition,
onCloseButtonPressed,
headerBackVisible = true,
headerBackVisible = Platform.OS === 'ios' || !closeButtonPosition,
...opts
}: NativeStackNavigationOptions & {
closeButtonPosition?: CloseButtonPosition;
@ -78,11 +78,6 @@ const navigationStyle = (
let headerRight;
let headerLeft;
if (!headerBackVisible) {
headerLeft = () => <></>;
opts.headerLeft = headerLeft;
}
if (closeButton === CloseButtonPosition.Right) {
headerRight = () => (
<TouchableOpacity
@ -115,7 +110,9 @@ const navigationStyle = (
fontWeight: '600',
color: theme.colors.foregroundColor,
},
headerBackTitleVisible: false,
headerBackVisible,
headerBackTitle: undefined,
headerBackButtonDisplayMode: 'minimal',
headerTintColor: theme.colors.foregroundColor,
headerRight,
headerLeft,

1
gesture-handler.js Normal file
View File

@ -0,0 +1 @@
// Don't import react-native-gesture-handler on web

View File

@ -0,0 +1,2 @@
// Only import react-native-gesture-handler on native platforms
import 'react-native-gesture-handler';

View File

@ -22,8 +22,25 @@ export const useExtendedNavigation = <T extends NavigationProp<ParamListBase>>()
const { wallets, saveToDisk } = useStorage();
const { isBiometricUseEnabled } = useBiometrics();
const enhancedNavigate: NavigationProp<ParamListBase>['navigate'] = useCallback(
(screenOrOptions: any, params?: any, options?: { merge?: boolean }) => {
const enhancedNavigate = useCallback(
(
...args:
| [string]
| [string, object | undefined]
| [string, object | undefined, { merge?: boolean }]
| [{ name: string; params?: object; path?: string; merge?: boolean }]
) => {
let screenOrOptions: any;
let params: any;
let options: { merge?: boolean } | undefined;
if (typeof args[0] === 'string') {
screenOrOptions = args[0];
params = args[1];
options = args[2];
} else {
screenOrOptions = args[0];
}
let screenName: string;
if (typeof screenOrOptions === 'string') {
screenName = screenOrOptions;

View File

@ -1,3 +1,4 @@
import './gesture-handler';
import './shim.js';
import React, { useEffect } from 'react';

View File

@ -162,7 +162,7 @@
B4D0B2682C1DED67006B6B1B /* ReceiveMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D0B2672C1DED67006B6B1B /* ReceiveMethod.swift */; };
B4EE583C226703320003363C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B40D4E35225841ED00428FCC /* Assets.xcassets */; };
B4EFF73B2C3F6C5E0095D655 /* MockData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4EFF73A2C3F6C5E0095D655 /* MockData.swift */; };
C978A716948AB7DEC5B6F677 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; };
C978A716948AB7DEC5B6F677 /* (null) in Frameworks */ = {isa = PBXBuildFile; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -427,7 +427,7 @@
files = (
782F075B5DD048449E2DECE9 /* libz.tbd in Frameworks */,
764B49B1420D4AEB8109BF62 /* libsqlite3.0.tbd in Frameworks */,
C978A716948AB7DEC5B6F677 /* BuildFile in Frameworks */,
C978A716948AB7DEC5B6F677 /* (null) in Frameworks */,
17CDA0718F42DB2CE856C872 /* libPods-BlueWallet.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -1471,7 +1471,7 @@
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance";
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = io.bluewallet.bluewallet;
INFOPLIST_KEY_WKExtensionDelegateClassName = "$(PRODUCT_BUNDLE_IDENTIFIER).ExtensionDelegate";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -1529,7 +1529,7 @@
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance";
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = io.bluewallet.bluewallet;
INFOPLIST_KEY_WKExtensionDelegateClassName = "$(PRODUCT_BUNDLE_IDENTIFIER).ExtensionDelegate";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",

View File

@ -1297,7 +1297,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- react-native-image-picker (7.2.2):
- react-native-image-picker (7.2.3):
- DoubleConversion
- glog
- hermes-engine
@ -1324,7 +1324,7 @@ PODS:
- React
- react-native-randombytes (3.6.1):
- React-Core
- react-native-safe-area-context (4.14.1):
- react-native-safe-area-context (5.2.0):
- React-Core
- react-native-screen-capture (0.2.3):
- React
@ -1621,7 +1621,7 @@ PODS:
- React-logger (= 0.76.7)
- React-perflogger (= 0.76.7)
- React-utils (= 0.76.7)
- ReactNativeCameraKit (14.1.0):
- ReactNativeCameraKit (14.2.0):
- DoubleConversion
- glog
- hermes-engine
@ -1656,7 +1656,7 @@ PODS:
- React-Core
- RNFS (2.20.0):
- React-Core
- RNGestureHandler (2.22.1):
- RNGestureHandler (2.23.1):
- DoubleConversion
- glog
- hermes-engine
@ -1702,7 +1702,7 @@ PODS:
- Yoga
- RNLocalize (3.4.1):
- React-Core
- RNPermissions (5.2.4):
- RNPermissions (5.2.5):
- React-Core
- RNQrGenerator (1.4.3):
- React
@ -1819,7 +1819,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- RNScreens (3.35.0):
- RNScreens (4.6.0):
- DoubleConversion
- glog
- hermes-engine
@ -2258,11 +2258,11 @@ SPEC CHECKSUMS:
react-native-blue-crypto: de5babd59b17fbf3fc31d2e1e5d59ec859093fbc
react-native-bw-file-access: fe925b77dbf48500df0b294c6851f8c84607a203
react-native-document-picker: 530879d9e89b490f0954bcc4ab697c5b5e35d659
react-native-image-picker: 19a8d8471a239890675726f88f9c18dd213656d5
react-native-image-picker: 130fad649d07e4eec8faaed361d3bba570e1e5ff
react-native-ios-context-menu: 986da6dcba70094bcc2a8049f68410fe7d25aff1
react-native-menu: 74230a5879e0ca697e98ee7c3087297dc774bf06
react-native-randombytes: 3c8f3e89d12487fd03a2f966c288d495415fc116
react-native-safe-area-context: 758e894ca5a9bd1868d2a9cfbca7326a2b6bf9dc
react-native-safe-area-context: 3e33e7c43c8b74dba436a5a32651cb8d7064c740
react-native-screen-capture: 7b6121f529681ed2fde36cdedadd0bb39e9a3796
react-native-secure-key-store: eb45b44bdec3f48e9be5cdfca0f49ddf64892ea6
react-native-tcp-socket: 61379457d7e702e83e28c213b6e085ac079e480f
@ -2294,7 +2294,7 @@ SPEC CHECKSUMS:
React-utils: 0342746d2cf989cf5e0d1b84c98cfa152edbdf3f
ReactCodegen: e1c019dc68733dd2c5d3b263b4a6dc72002c0045
ReactCommon: 81e0744ee33adfd6d586141b927024f488bc49ea
ReactNativeCameraKit: e72b838dac4ea2da19b7eb5d00b23125072790fd
ReactNativeCameraKit: 72cc60b69ae192fe55a3e3f294ed46833308bb22
RealmJS: 9fd51c849eb552ade9f7b11db42a319b4f6cab4c
RNCAsyncStorage: c91d753ede6dc21862c4922cd13f98f7cfde578e
RNCClipboard: ee059e6006b137e369caed5eb852b4aad9f5d886
@ -2302,17 +2302,17 @@ SPEC CHECKSUMS:
RNDefaultPreference: 8a089ee8ce829a66c5453e3c5434f0785499d1c3
RNDeviceInfo: 801c18d0525e86580900e7b5f562dca0c8c05021
RNFS: 89de7d7f4c0f6bafa05343c578f61118c8282ed8
RNGestureHandler: e597c3cf95ce53819280c34d008d63bfbb05ddeb
RNGestureHandler: 79e731e77dbec11fb5508e646d13897f1ee45912
RNHandoff: bc8af5a86853ff13b033e7ba1114c3c5b38e6385
RNKeychain: 4df48b5186ca2b6a99f5ead69ad587154e084a32
RNLocalize: 15463c4d79c7da45230064b4adcf5e9bb984667e
RNPermissions: 257cd3630c304d59877aeb36f885c21543adde41
RNPermissions: a19629bb6d6fdb521540f39262e356ae30d6cc4e
RNQrGenerator: afacf12b55dfba0e3aaca963eec23691e8426431
RNQuickAction: c2c8f379e614428be0babe4d53a575739667744d
RNRate: 7641919330e0d6688ad885a985b4bd697ed7d14c
RNReactNativeHapticFeedback: 00ba111b82aa266bb3ee1aa576831c2ea9a9dfad
RNReanimated: 66cf0f600a26d2b5e74c6e0b1c77c1ab1f62fc05
RNScreens: 35bb8e81aeccf111baa0ea01a54231390dbbcfd9
RNScreens: b05d3b8e716e68d9e2f1364d440d23de5b6885f1
RNShare: 6204e6a1987ba3e7c47071ef703e5449a0e3548a
RNSVG: 86fecdfc637614ba9def63f7f3f2e7795e018356
RNVectorIcons: 182892e7d1a2f27b52d3c627eca5d2665a22ee28

View File

@ -182,6 +182,7 @@
"input_total": "Total:",
"permission_camera_message": "We need your permission to use your camera.",
"psbt_sign": "Sign a transaction",
"invalid_psbt": "Invalid PSBT provided.",
"open_settings": "Open Settings",
"permission_storage_denied_message": "BlueWallet is unable to save this file. Please open your device settings and enable Storage Permission.",
"permission_storage_title": "Storage Access Permission",

View File

@ -58,6 +58,7 @@ export type AddWalletStackParamList = {
n: number;
walletLabel: string;
format: string;
onBarScanned?: string;
};
WalletsAddMultisigHelp: undefined;
ScanQRCode: ScanQRCodeParamList;
@ -139,7 +140,7 @@ const AddWalletStack = () => {
backgroundColor: '#0070FF',
},
headerTintColor: '#FFFFFF',
headerBackTitleVisible: false,
headerBackTitle: undefined,
statusBarStyle: 'light',
headerShadowVisible: false,
})(theme)}

View File

@ -26,7 +26,7 @@ import WalletDetails from '../screen/wallets/WalletDetails';
import GenerateWord from '../screen/wallets/generateWord';
import SelectWallet from '../screen/wallets/SelectWallet';
import WalletsList from '../screen/wallets/WalletsList';
import { NavigationDefaultOptions, NavigationFormModalOptions, StatusBarLightOptions, DetailViewStack } from './index'; // Importing the navigator
import { NavigationDefaultOptions, StatusBarLightOptions, DetailViewStack } from './index'; // Importing the navigator
import AddWalletStack from './AddWalletStack';
import AztecoRedeemStackRoot from './AztecoRedeemStack';
import PaymentCodesListComponent from './LazyLoadPaymentCodeStack';
@ -65,12 +65,14 @@ import ReleaseNotes from '../screen/settings/ReleaseNotes';
import ToolsScreen from '../screen/settings/tools';
import SettingsPrivacy from '../screen/settings/SettingsPrivacy';
import { ScanQRCodeComponent } from './LazyLoadScanQRCodeStack';
import { useIsLargeScreen } from '../hooks/useIsLargeScreen';
const DetailViewStackScreensStack = () => {
const theme = useTheme();
const navigation = useExtendedNavigation();
const { wallets } = useStorage();
const { isTotalBalanceEnabled } = useSettings();
const { isLargeScreen } = useIsLargeScreen();
const DetailButton = useMemo(() => <HeaderRightButton testID="DetailButton" disabled={true} title={loc.send.create_details} />, []);
@ -79,14 +81,17 @@ const DetailViewStackScreensStack = () => {
}, [navigation]);
const RightBarButtons = useMemo(
() => (
<>
<AddWalletButton onPress={navigateToAddWallet} />
<View style={styles.width24} />
() =>
isLargeScreen ? (
<SettingsButton />
</>
),
[navigateToAddWallet],
) : (
<>
<AddWalletButton onPress={navigateToAddWallet} />
<View style={styles.width24} />
<SettingsButton />
</>
),
[isLargeScreen, navigateToAddWallet],
);
const useWalletListScreenOptions = useMemo<NativeStackNavigationOptions>(() => {
@ -146,9 +151,9 @@ const DetailViewStackScreensStack = () => {
headerStyle: {
backgroundColor: theme.colors.customHeader,
},
headerBackTitle: undefined,
headerRight: () => DetailButton,
headerBackTitleStyle: { fontSize: 0 },
headerBackTitleVisible: true,
})(theme)}
/>
<DetailViewStack.Screen name="CPFP" component={CPFP} options={navigationStyle({ title: loc.transactions.cpfp_title })(theme)} />
@ -243,15 +248,14 @@ const DetailViewStackScreensStack = () => {
component={WalletAddresses}
options={navigationStyle({ title: loc.addresses.addresses_title, statusBarStyle: 'auto' })(theme)}
/>
<DetailViewStack.Screen
name="Settings"
component={Settings}
options={navigationStyle({
headerTransparent: true,
title: loc.settings.header,
// workaround to deal with the flicker when headerBackTitleVisible is false
headerBackTitleStyle: { fontSize: 0 },
headerBackTitleVisible: true,
headerBackButtonDisplayMode: 'default',
headerShadowVisible: false,
headerLargeTitle: true,
animationTypeForReplace: 'push',
@ -320,29 +324,12 @@ const DetailViewStackScreensStack = () => {
component={SettingsPrivacy}
options={navigationStyle({ title: loc.settings.privacy })(theme)}
/>
<DetailViewStack.Screen
name="ScanQRCode"
component={ScanQRCodeComponent}
options={navigationStyle({
headerShown: false,
statusBarHidden: true,
presentation: 'fullScreenModal',
headerShadowVisible: false,
})(theme)}
/>
<DetailViewStack.Screen
name="ViewEditMultisigCosignersRoot"
component={ViewEditMultisigCosignersStackRoot}
options={{ ...NavigationDefaultOptions, ...StatusBarLightOptions, gestureEnabled: false, fullScreenGestureEnabled: false }}
initialParams={{ walletID: undefined, cosigners: undefined }}
/>
<DetailViewStack.Screen
name="AddWalletRoot"
component={AddWalletStack}
options={navigationStyle({ closeButtonPosition: CloseButtonPosition.Left, ...NavigationFormModalOptions })(theme)}
options={navigationStyle({ closeButtonPosition: CloseButtonPosition.Left, ...NavigationDefaultOptions })(theme)}
/>
<DetailViewStack.Screen name="SendDetailsRoot" component={SendDetailsStack} options={NavigationFormModalOptions} />
<DetailViewStack.Screen name="SendDetailsRoot" component={SendDetailsStack} options={NavigationDefaultOptions} />
<DetailViewStack.Screen name="LNDCreateInvoiceRoot" component={LNDCreateInvoiceRoot} options={NavigationDefaultOptions} />
<DetailViewStack.Screen name="ScanLndInvoiceRoot" component={ScanLndInvoiceRoot} options={NavigationDefaultOptions} />
<DetailViewStack.Screen name="AztecoRedeemRoot" component={AztecoRedeemStackRoot} options={NavigationDefaultOptions} />
@ -357,6 +344,13 @@ const DetailViewStackScreensStack = () => {
component={ExportMultisigCoordinationSetupStack}
options={NavigationDefaultOptions}
/>
<DetailViewStack.Screen
name="ViewEditMultisigCosignersRoot"
component={ViewEditMultisigCosignersStackRoot}
options={{ ...NavigationDefaultOptions, ...StatusBarLightOptions, gestureEnabled: false, fullScreenGestureEnabled: false }}
initialParams={{ walletID: undefined, cosigners: undefined }}
/>
<DetailViewStack.Screen
name="WalletXpubRoot"
component={WalletXpubStackRoot}
@ -379,6 +373,16 @@ const DetailViewStackScreensStack = () => {
statusBarStyle: 'auto',
})(theme)}
/>
<DetailViewStack.Screen
name="ScanQRCode"
component={ScanQRCodeComponent}
options={navigationStyle({
headerShown: false,
statusBarHidden: true,
presentation: 'fullScreenModal',
headerShadowVisible: false,
})(theme)}
/>
</DetailViewStack.Navigator>
);
};

View File

@ -45,11 +45,7 @@ export type DetailViewStackParamList = {
Success: undefined;
WalletAddresses: { walletID: string };
AddWalletRoot: undefined;
SendDetailsRoot: {
screen: string;
params: SendDetailsParams;
merge: boolean;
};
SendDetailsRoot: SendDetailsParams;
LNDCreateInvoiceRoot: undefined;
ScanLndInvoiceRoot: {
screen: string;

View File

@ -33,11 +33,7 @@ const DrawerRoot = () => {
return (
<Drawer.Navigator screenOptions={drawerStyle} drawerContent={DrawerListContent}>
<Drawer.Screen
name="DetailViewStackScreensStack"
component={DetailViewStackScreensStack}
options={{ headerShown: false, gestureHandlerProps: { enableTrackpadTwoFingerGesture: false } }}
/>
<Drawer.Screen name="DetailViewStackScreensStack" component={DetailViewStackScreensStack} options={{ headerShown: false }} />
</Drawer.Navigator>
);
};

View File

@ -1,9 +1,7 @@
import 'react-native-gesture-handler'; // should be on top
import React, { lazy, Suspense } from 'react';
import MainRoot from '../navigation';
import { useStorage } from '../hooks/context/useStorage';
import DevMenu from '../components/DevMenu';
import MainRoot from './index';
const CompanionDelegates = lazy(() => import('../components/CompanionDelegates'));
const MasterView = () => {

View File

@ -1,5 +1,5 @@
import { Psbt } from 'bitcoinjs-lib';
import { CreateTransactionTarget, CreateTransactionUtxo } from '../class/wallets/types';
import { CreateTransactionTarget, CreateTransactionUtxo, TWallet } from '../class/wallets/types';
import { BitcoinUnit, Chain } from '../models/bitcoinUnits';
import { ScanQRCodeParamList } from './DetailViewStackParamList';
@ -21,6 +21,7 @@ export type SendDetailsParams = {
utxos?: CreateTransactionUtxo[] | null;
isEditable?: boolean;
uri?: string;
paymentCode?: string;
addRecipientParams?: {
address: string;
amount?: number;
@ -77,13 +78,18 @@ export type SendDetailsStackParamList = {
txid?: string;
};
SelectWallet: {
chainType: Chain;
chainType?: Chain;
onWalletSelect?: (wallet: TWallet, navigation: any) => void;
availableWallets?: TWallet[];
noWalletExplanationText?: string;
onChainRequireSend?: boolean;
};
CoinControl: {
walletID: string;
};
PaymentCodeList: {
walletID: string;
merge?: boolean;
};
ScanQRCode: ScanQRCodeParamList;
};

View File

@ -29,7 +29,6 @@ const getTransactionStatusOptions = ({ route, navigation, theme }: GetTransactio
backgroundColor: theme.colors.customHeader,
},
headerBackTitleStyle: { fontSize: 0 },
headerBackTitleVisible: true,
statusBarStyle: 'auto',
})(theme),
headerRight: () => (

View File

@ -10,7 +10,7 @@ import { RouteProp } from '@react-navigation/native';
export type WalletTransactionsRouteProps = RouteProp<DetailViewStackParamList, 'WalletTransactions'>;
const getWalletTransactionsOptions = ({ route }: { route: WalletTransactionsRouteProps }): NativeStackNavigationOptions => {
const { isLoading, walletID, walletType } = route.params;
const { isLoading = false, walletID, walletType } = route.params;
const onPress = () => {
navigationRef.navigate('WalletDetails', {
@ -34,7 +34,7 @@ const getWalletTransactionsOptions = ({ route }: { route: WalletTransactionsRout
},
headerShadowVisible: false,
headerTintColor: '#FFFFFF',
headerBackTitleVisible: true,
headerBackTitle: undefined,
headerRight: () => RightButton,
};
};

642
package-lock.json generated
View File

@ -19,16 +19,17 @@
"@noble/secp256k1": "1.6.3",
"@react-native-async-storage/async-storage": "2.1.0",
"@react-native-clipboard/clipboard": "1.16.1",
"@react-native-community/cli": "15.1.3",
"@react-native-community/cli-platform-android": "15.1.3",
"@react-native-community/cli-platform-ios": "15.1.3",
"@react-native-community/cli": "15.0.1",
"@react-native-community/cli-platform-android": "15.0.1",
"@react-native-community/cli-platform-ios": "15.0.1",
"@react-native-community/push-notification-ios": "1.11.0",
"@react-native-menu/menu": "https://github.com/BlueWallet/menu.git#14bab79",
"@react-native/gradle-plugin": "0.76.7",
"@react-native/metro-config": "0.76.7",
"@react-navigation/drawer": "6.7.2",
"@react-navigation/native": "6.1.18",
"@react-navigation/native-stack": "6.11.0",
"@react-navigation/devtools": "7.0.15",
"@react-navigation/drawer": "7.1.1",
"@react-navigation/native": "7.0.14",
"@react-navigation/native-stack": "7.2.0",
"@rneui/base": "4.0.0-rc.8",
"@rneui/themed": "4.0.0-rc.8",
"@spsina/bip47": "github:BlueWallet/bip47#df82345",
@ -61,27 +62,27 @@
"payjoin-client": "1.0.1",
"process": "0.11.10",
"prop-types": "15.8.1",
"react": "18.3.1",
"react": "18.2.0",
"react-localization": "github:BlueWallet/react-localization#ae7969a",
"react-native": "0.76.7",
"react-native-biometrics": "3.0.1",
"react-native-blue-crypto": "github:BlueWallet/react-native-blue-crypto#3cb5442",
"react-native-camera-kit": "14.1.0",
"react-native-camera-kit": "14.2.0",
"react-native-crypto": "2.2.0",
"react-native-default-preference": "https://github.com/BlueWallet/react-native-default-preference.git#6338a1f1235e4130b8cfc2dd3b53015eeff2870c",
"react-native-device-info": "14.0.2",
"react-native-document-picker": "9.3.1",
"react-native-draglist": "github:BlueWallet/react-native-draglist#a4af02f",
"react-native-fs": "2.20.0",
"react-native-gesture-handler": "2.22.1",
"react-native-gesture-handler": "2.23.1",
"react-native-handoff": "github:BlueWallet/react-native-handoff#v0.0.4",
"react-native-haptic-feedback": "2.3.3",
"react-native-image-picker": "7.2.2",
"react-native-image-picker": "7.2.3",
"react-native-ios-context-menu": "github:BlueWallet/react-native-ios-context-menu#e5c1217cd220bfab6e6d9a7c65838545082e3f8e",
"react-native-keychain": "9.1.0",
"react-native-linear-gradient": "2.8.3",
"react-native-localize": "3.4.1",
"react-native-permissions": "5.2.4",
"react-native-permissions": "5.2.5",
"react-native-prompt-android": "github:BlueWallet/react-native-prompt-android#ed168d66fed556bc2ed07cf498770f058b78a376",
"react-native-push-notification": "8.1.1",
"react-native-qrcode-svg": "6.3.12",
@ -89,15 +90,16 @@
"react-native-randombytes": "3.6.1",
"react-native-rate": "1.2.12",
"react-native-reanimated": "3.16.7",
"react-native-safe-area-context": "4.14.1",
"react-native-safe-area-context": "5.2.0",
"react-native-screen-capture": "github:BlueWallet/react-native-screen-capture#18cb79f",
"react-native-screens": "3.35.0",
"react-native-screens": "4.6.0",
"react-native-secure-key-store": "github:BlueWallet/react-native-secure-key-store#2076b4849e88aa0a78e08bfbb4ce3923e0925cbc",
"react-native-share": "11.1.0",
"react-native-svg": "15.11.1",
"react-native-tcp-socket": "6.2.0",
"react-native-vector-icons": "10.2.0",
"react-native-watch-connectivity": "1.1.0",
"react-test-renderer": "18.2.0",
"readable-stream": "3.6.2",
"realm": "20.1.0",
"rn-nodeify": "10.3.0",
@ -119,13 +121,14 @@
"@react-native/js-polyfills": "^0.76.7",
"@react-native/metro-babel-transformer": "^0.76.7",
"@react-native/typescript-config": "0.76.7",
"@testing-library/react-native": "^13.0.1",
"@types/bip38": "^3.1.2",
"@types/bs58check": "^2.1.0",
"@types/create-hash": "^1.2.2",
"@types/crypto-js": "^4.2.2",
"@types/jest": "^29.5.2",
"@types/react": "^18.2.16",
"@types/react-test-renderer": "^18.0.0",
"@types/react-test-renderer": "^19.0.0",
"@types/wif": "^2.0.5",
"@typescript-eslint/eslint-plugin": "^7.15.0",
"@typescript-eslint/parser": "^7.15.0",
@ -147,7 +150,6 @@
"metro-react-native-babel-preset": "0.76.8",
"node-fetch": "^2.6.7",
"prettier": "^3.2.5",
"react-test-renderer": "18.3.1",
"ts-jest": "^29.1.1",
"typescript": "^5.1.6"
},
@ -4843,18 +4845,18 @@
}
},
"node_modules/@react-native-community/cli": {
"version": "15.1.3",
"resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-15.1.3.tgz",
"integrity": "sha512-+ih/WYUkJsEV2CMAnOHvVoSIz/Ahg5UJk+sqSIOmY79mWAglQzfLP71o7b0neJCnJWLmWiO6G6/S+kmULefD5g==",
"version": "15.0.1",
"resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-15.0.1.tgz",
"integrity": "sha512-xIGPytx2bj5HxFk0c7S25AVuJowHmEFg5LFC9XosKc0TSOjP1r6zGC6OqC/arQV/pNuqmZN2IFnpgJn0Bn+hhQ==",
"license": "MIT",
"dependencies": {
"@react-native-community/cli-clean": "15.1.3",
"@react-native-community/cli-config": "15.1.3",
"@react-native-community/cli-debugger-ui": "15.1.3",
"@react-native-community/cli-doctor": "15.1.3",
"@react-native-community/cli-server-api": "15.1.3",
"@react-native-community/cli-tools": "15.1.3",
"@react-native-community/cli-types": "15.1.3",
"@react-native-community/cli-clean": "15.0.1",
"@react-native-community/cli-config": "15.0.1",
"@react-native-community/cli-debugger-ui": "15.0.1",
"@react-native-community/cli-doctor": "15.0.1",
"@react-native-community/cli-server-api": "15.0.1",
"@react-native-community/cli-tools": "15.0.1",
"@react-native-community/cli-types": "15.0.1",
"chalk": "^4.1.2",
"commander": "^9.4.1",
"deepmerge": "^4.3.0",
@ -4873,12 +4875,12 @@
}
},
"node_modules/@react-native-community/cli-clean": {
"version": "15.1.3",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-clean/-/cli-clean-15.1.3.tgz",
"integrity": "sha512-3s9NGapIkONFoCUN2s77NYI987GPSCdr74rTf0TWyGIDf4vTYgKoWKKR+Ml3VTa1BCj51r4cYuHEKE1pjUSc0w==",
"version": "15.0.1",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-clean/-/cli-clean-15.0.1.tgz",
"integrity": "sha512-flGTfT005UZvW2LAXVowZ/7ri22oiiZE4pPgMvc8klRxO5uofKIRuohgiHybHtiCo/HNqIz45JmZJvuFrhc4Ow==",
"license": "MIT",
"dependencies": {
"@react-native-community/cli-tools": "15.1.3",
"@react-native-community/cli-tools": "15.0.1",
"chalk": "^4.1.2",
"execa": "^5.0.0",
"fast-glob": "^3.3.2"
@ -4955,12 +4957,12 @@
}
},
"node_modules/@react-native-community/cli-config": {
"version": "15.1.3",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-config/-/cli-config-15.1.3.tgz",
"integrity": "sha512-fJ9MrWp+/SszEVg5Wja8A57Whl5EfjRCHWFNkvFBtfjVUfi2hWvSTW3VBxzJuCHnPIIwpQafwjEgOrIRUI8y6w==",
"version": "15.0.1",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-config/-/cli-config-15.0.1.tgz",
"integrity": "sha512-SL3/9zIyzQQPKWei0+W1gNHxCPurrxqpODUWnVLoP38DNcvYCGtsRayw/4DsXgprZfBC+FsscNpd3IDJrG59XA==",
"license": "MIT",
"dependencies": {
"@react-native-community/cli-tools": "15.1.3",
"@react-native-community/cli-tools": "15.0.1",
"chalk": "^4.1.2",
"cosmiconfig": "^9.0.0",
"deepmerge": "^4.3.0",
@ -4968,95 +4970,13 @@
"joi": "^17.2.1"
}
},
"node_modules/@react-native-community/cli-config-android": {
"version": "15.1.3",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-config-android/-/cli-config-android-15.1.3.tgz",
"integrity": "sha512-v9okV/WQfMJEWRddI0S6no2v9Lvk54KgFkw1mvMYhJKVqloCNsIWzoqme0u7zIuYSzwsjXUQXVlGiDzbbwdkBw==",
"license": "MIT",
"dependencies": {
"@react-native-community/cli-tools": "15.1.3",
"chalk": "^4.1.2",
"fast-glob": "^3.3.2",
"fast-xml-parser": "^4.4.1"
}
},
"node_modules/@react-native-community/cli-config-android/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/@react-native-community/cli-config-android/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/@react-native-community/cli-config-android/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/@react-native-community/cli-config-android/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/@react-native-community/cli-config-android/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/@react-native-community/cli-config-android/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@react-native-community/cli-config-apple": {
"version": "15.1.3",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-config-apple/-/cli-config-apple-15.1.3.tgz",
"integrity": "sha512-Qv6jaEaycv+7s8wR9l9bdpIeSNFCeVANfGCX1x76SgOmGfZNIa7J3l1HaeF/5ktERMYsw/hm4u3rUn4Ks0YV1g==",
"version": "15.0.1",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-config-apple/-/cli-config-apple-15.0.1.tgz",
"integrity": "sha512-GEHUx4NRp9W9or6vygn0TgNeFkcJdNjrtko0vQEJAS4gJdWqP/9LqqwJNlUfaW5jHBN7TKALAMlfRmI12Op3sg==",
"license": "MIT",
"dependencies": {
"@react-native-community/cli-tools": "15.1.3",
"@react-native-community/cli-tools": "15.0.1",
"chalk": "^4.1.2",
"execa": "^5.0.0",
"fast-glob": "^3.3.2"
@ -5203,25 +5123,25 @@
}
},
"node_modules/@react-native-community/cli-debugger-ui": {
"version": "15.1.3",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-15.1.3.tgz",
"integrity": "sha512-m+fb9iAUNb9WiDdokCBLh0InJvollcgAM3gLjCT8DGTP6bH/jxtZ3DszzyIRqN9cMamItVrvDM0vkIg48xK7rQ==",
"version": "15.0.1",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-15.0.1.tgz",
"integrity": "sha512-xkT2TLS8zg5r7Vl9l/2f7JVUoFECnVBS+B5ivrSu2PNZhKkr9lRmJFxC9aVLFb5lIxQQKNDvEyiIDNfP7wjJiA==",
"license": "MIT",
"dependencies": {
"serve-static": "^1.13.1"
}
},
"node_modules/@react-native-community/cli-doctor": {
"version": "15.1.3",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-doctor/-/cli-doctor-15.1.3.tgz",
"integrity": "sha512-WC9rawobuITAtJjyZ68E1M0geRt+b9A2CGB354L/tQp+XMKobGGVI4Y0DsattK83Wdg59GPyldE8C0Wevfgm/A==",
"version": "15.0.1",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-doctor/-/cli-doctor-15.0.1.tgz",
"integrity": "sha512-YCu44lZR3zZxJJYVTqYZFz9cT9KBfbKI4q2MnKOvkamt00XY3usooMqfuwBAdvM/yvpx7M5w8kbM/nPyj4YCvQ==",
"license": "MIT",
"dependencies": {
"@react-native-community/cli-config": "15.1.3",
"@react-native-community/cli-platform-android": "15.1.3",
"@react-native-community/cli-platform-apple": "15.1.3",
"@react-native-community/cli-platform-ios": "15.1.3",
"@react-native-community/cli-tools": "15.1.3",
"@react-native-community/cli-config": "15.0.1",
"@react-native-community/cli-platform-android": "15.0.1",
"@react-native-community/cli-platform-apple": "15.0.1",
"@react-native-community/cli-platform-ios": "15.0.1",
"@react-native-community/cli-tools": "15.0.1",
"chalk": "^4.1.2",
"command-exists": "^1.2.8",
"deepmerge": "^4.3.0",
@ -5294,9 +5214,9 @@
}
},
"node_modules/@react-native-community/cli-doctor/node_modules/semver": {
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"version": "7.7.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.0.tgz",
"integrity": "sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@ -5318,15 +5238,16 @@
}
},
"node_modules/@react-native-community/cli-platform-android": {
"version": "15.1.3",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-android/-/cli-platform-android-15.1.3.tgz",
"integrity": "sha512-ZwrBK0UK4DRHoQm2v5m8+PlNHBK5gmibBU6rqNFLo4aZJ2Rufalbv3SF+DukgSyoI9/kI8UVrzSNj17e+HhN5A==",
"version": "15.0.1",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-android/-/cli-platform-android-15.0.1.tgz",
"integrity": "sha512-QlAMomj6H6TY6pHwjTYMsHDQLP5eLzjAmyW1qb03w/kyS/72elK2bjsklNWJrscFY9TMQLqw7qoAsXf1m5t/dg==",
"license": "MIT",
"dependencies": {
"@react-native-community/cli-config-android": "15.1.3",
"@react-native-community/cli-tools": "15.1.3",
"@react-native-community/cli-tools": "15.0.1",
"chalk": "^4.1.2",
"execa": "^5.0.0",
"fast-glob": "^3.3.2",
"fast-xml-parser": "^4.4.1",
"logkitty": "^0.7.1"
}
},
@ -5401,13 +5322,13 @@
}
},
"node_modules/@react-native-community/cli-platform-apple": {
"version": "15.1.3",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-apple/-/cli-platform-apple-15.1.3.tgz",
"integrity": "sha512-awotqCGVcTdeRmTlE3wlsZgNxZUDGojUhPYOVMKejgdCzNM2bvzF8fqhETH2sc64Hbw/tQJg8pYeD4MZR0bHxw==",
"version": "15.0.1",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-apple/-/cli-platform-apple-15.0.1.tgz",
"integrity": "sha512-iQj1Dt2fr/Q7X2CQhyhWnece3eLDCark1osfiwpViksOfTH2WdpNS3lIwlFcIKhsieFU7YYwbNuFqQ3tF9Dlvw==",
"license": "MIT",
"dependencies": {
"@react-native-community/cli-config-apple": "15.1.3",
"@react-native-community/cli-tools": "15.1.3",
"@react-native-community/cli-config-apple": "15.0.1",
"@react-native-community/cli-tools": "15.0.1",
"chalk": "^4.1.2",
"execa": "^5.0.0",
"fast-xml-parser": "^4.4.1"
@ -5484,22 +5405,22 @@
}
},
"node_modules/@react-native-community/cli-platform-ios": {
"version": "15.1.3",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-ios/-/cli-platform-ios-15.1.3.tgz",
"integrity": "sha512-DxM8GYkqjrlGuuGiGjrcvUmKC2xv9zDzHbsc4+S160Nn0zbF2OH1DhMlnIuUeCmQnAO6QFMqU99O120iEzCAug==",
"version": "15.0.1",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-ios/-/cli-platform-ios-15.0.1.tgz",
"integrity": "sha512-6pKzXEIgGL20eE1uOn8iSsNBlMzO1LG+pQOk+7mvD172EPhKm/lRzUVDX5gO/2jvsGoNw6VUW0JX1FI2firwqA==",
"license": "MIT",
"dependencies": {
"@react-native-community/cli-platform-apple": "15.1.3"
"@react-native-community/cli-platform-apple": "15.0.1"
}
},
"node_modules/@react-native-community/cli-server-api": {
"version": "15.1.3",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-server-api/-/cli-server-api-15.1.3.tgz",
"integrity": "sha512-kXZ0evedluLt6flWQiI3JqwnW8rSBspOoQ7JVTQYiG5lDHAeL3Om9PjAyiQBg1EZRMjiWZDV7nDxhR+m+6NX5Q==",
"version": "15.0.1",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-server-api/-/cli-server-api-15.0.1.tgz",
"integrity": "sha512-f3rb3t1ELLaMSX5/LWO/IykglBIgiP3+pPnyl8GphHnBpf3bdIcp7fHlHLemvHE06YxT2nANRxRPjy1gNskenA==",
"license": "MIT",
"dependencies": {
"@react-native-community/cli-debugger-ui": "15.1.3",
"@react-native-community/cli-tools": "15.1.3",
"@react-native-community/cli-debugger-ui": "15.0.1",
"@react-native-community/cli-tools": "15.0.1",
"compression": "^1.7.1",
"connect": "^3.6.5",
"errorhandler": "^1.5.1",
@ -5510,9 +5431,9 @@
}
},
"node_modules/@react-native-community/cli-tools": {
"version": "15.1.3",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-tools/-/cli-tools-15.1.3.tgz",
"integrity": "sha512-2RzoUKR+Y03ijBeeSoCSQihyN6dxy3fbHjraO4MheZZUzt/Yd1VMEDd0R5aa6rtiRDoknbHt45RL2GMa8MSaEA==",
"version": "15.0.1",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-tools/-/cli-tools-15.0.1.tgz",
"integrity": "sha512-N79A+u/94roanfmNohVcNGu6Xg+0idh63JHZFLC9OJJuZwTifGMLDfSTHZATpR1J7rebozQ5ClcSUePavErnSg==",
"license": "MIT",
"dependencies": {
"appdirsjs": "^1.2.4",
@ -5587,9 +5508,9 @@
}
},
"node_modules/@react-native-community/cli-tools/node_modules/semver": {
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"version": "7.7.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.0.tgz",
"integrity": "sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@ -5611,9 +5532,9 @@
}
},
"node_modules/@react-native-community/cli-types": {
"version": "15.1.3",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-types/-/cli-types-15.1.3.tgz",
"integrity": "sha512-0ZaM8LMsa7z7swBN+ObL2ysc6aA3AdS698Q6uEukym6RyX1uLbiofUdlvFSMpWSEL3D8f9OTymmL4RkCH8k6dw==",
"version": "15.0.1",
"resolved": "https://registry.npmjs.org/@react-native-community/cli-types/-/cli-types-15.0.1.tgz",
"integrity": "sha512-sWiJ62kkGu2mgYni2dsPxOMBzpwTjNsDH1ubY4mqcNEI9Zmzs0vRwwDUEhYqwNGys9+KpBKoZRrT2PAlhO84xA==",
"license": "MIT",
"dependencies": {
"joi": "^17.2.1"
@ -6332,94 +6253,126 @@
}
},
"node_modules/@react-navigation/core": {
"version": "6.4.17",
"resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-6.4.17.tgz",
"integrity": "sha512-Nd76EpomzChWAosGqWOYE3ItayhDzIEzzZsT7PfGcRFDgW5miHV2t4MZcq9YIK4tzxZjVVpYbIynOOQQd1e0Cg==",
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.3.1.tgz",
"integrity": "sha512-S3KCGvNsoqVk8ErAtQI2EAhg9185lahF5OY01ofrrD4Ij/uk3QEHHjoGQhR5l5DXSCSKr1JbMQA7MEKMsBiWZA==",
"license": "MIT",
"dependencies": {
"@react-navigation/routers": "^6.1.9",
"@react-navigation/routers": "^7.1.2",
"escape-string-regexp": "^4.0.0",
"nanoid": "^3.1.23",
"nanoid": "3.3.8",
"query-string": "^7.1.3",
"react-is": "^16.13.0",
"use-latest-callback": "^0.2.1"
"react-is": "^18.2.0",
"use-latest-callback": "^0.2.1",
"use-sync-external-store": "^1.2.2"
},
"peerDependencies": {
"react": "*"
"react": ">= 18.2.0"
}
},
"node_modules/@react-navigation/core/node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"license": "MIT"
},
"node_modules/@react-navigation/devtools": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@react-navigation/devtools/-/devtools-7.0.15.tgz",
"integrity": "sha512-pxEBVtd6e5ocT7bs6k6ghOJNyb9Fzxm+EYHemHQ53GEin1sQKYpsSHWZEJdFj1cxYp+/+KCT+TueuNDFkJOr4Q==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"nanoid": "3.3.8",
"stacktrace-parser": "^0.1.10"
},
"peerDependencies": {
"react": ">= 18.2.0"
}
},
"node_modules/@react-navigation/drawer": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@react-navigation/drawer/-/drawer-6.7.2.tgz",
"integrity": "sha512-o4g2zgTZa2+oLd+8V33etrSM38KIqu8S/zCBTsdsHUoQyVE7JNRiv3Qgq/jMvEb8PZCqWmm7jHItcgzrBuwyOQ==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/@react-navigation/drawer/-/drawer-7.1.1.tgz",
"integrity": "sha512-34UqRS5OLFaNXPs5ocz3Du9c7em0P7fFMPYCZn/MxadDzQ4Mn/74pmJczmiyvyvz8vcWsNRbZ3Qswm0Dv6z60w==",
"license": "MIT",
"dependencies": {
"@react-navigation/elements": "^1.3.31",
"@react-navigation/elements": "^2.2.5",
"color": "^4.2.3",
"warn-once": "^0.1.0"
"react-native-drawer-layout": "^4.1.1",
"use-latest-callback": "^0.2.1"
},
"peerDependencies": {
"@react-navigation/native": "^6.0.0",
"react": "*",
"@react-navigation/native": "^7.0.14",
"react": ">= 18.2.0",
"react-native": "*",
"react-native-gesture-handler": ">= 1.0.0",
"react-native-reanimated": ">= 1.0.0",
"react-native-safe-area-context": ">= 3.0.0",
"react-native-screens": ">= 3.0.0"
"react-native-gesture-handler": ">= 2.0.0",
"react-native-reanimated": ">= 2.0.0",
"react-native-safe-area-context": ">= 4.0.0",
"react-native-screens": ">= 4.0.0"
}
},
"node_modules/@react-navigation/elements": {
"version": "1.3.31",
"resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.3.31.tgz",
"integrity": "sha512-bUzP4Awlljx5RKEExw8WYtif8EuQni2glDaieYROKTnaxsu9kEIA515sXQgUDZU4Ob12VoL7+z70uO3qrlfXcQ==",
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.2.5.tgz",
"integrity": "sha512-sDhE+W14P7MNWLMxXg1MEVXwkLUpMZJGflE6nQNzLmolJQIHgcia0Mrm8uRa3bQovhxYu1UzEojLZ+caoZt7Fg==",
"license": "MIT",
"dependencies": {
"color": "^4.2.3"
},
"peerDependencies": {
"@react-navigation/native": "^6.0.0",
"react": "*",
"@react-native-masked-view/masked-view": ">= 0.2.0",
"@react-navigation/native": "^7.0.14",
"react": ">= 18.2.0",
"react-native": "*",
"react-native-safe-area-context": ">= 3.0.0"
"react-native-safe-area-context": ">= 4.0.0"
},
"peerDependenciesMeta": {
"@react-native-masked-view/masked-view": {
"optional": true
}
}
},
"node_modules/@react-navigation/native": {
"version": "6.1.18",
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-6.1.18.tgz",
"integrity": "sha512-mIT9MiL/vMm4eirLcmw2h6h/Nm5FICtnYSdohq4vTLA2FF/6PNhByM7s8ffqoVfE5L0uAa6Xda1B7oddolUiGg==",
"version": "7.0.14",
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.0.14.tgz",
"integrity": "sha512-Gi6lLw4VOGSWAhmUdJOMauOKGK51/YA1CprjXm91sNfgERWvznqEMw8QmUQx9SEqYfi0LfZhbzpMst09SJ00lw==",
"license": "MIT",
"dependencies": {
"@react-navigation/core": "^6.4.17",
"@react-navigation/core": "^7.3.1",
"escape-string-regexp": "^4.0.0",
"fast-deep-equal": "^3.1.3",
"nanoid": "^3.1.23"
"nanoid": "3.3.8",
"use-latest-callback": "^0.2.1"
},
"peerDependencies": {
"react": "*",
"react": ">= 18.2.0",
"react-native": "*"
}
},
"node_modules/@react-navigation/native-stack": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-6.11.0.tgz",
"integrity": "sha512-U5EcUB9Q2NQspCFwYGGNJm0h6wBCOv7T30QjndmvlawLkNt7S7KWbpWyxS9XBHSIKF57RgWjfxuJNTgTstpXxw==",
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.2.0.tgz",
"integrity": "sha512-mw7Nq9qQrGsmJmCTwIIWB7yY/3tWYXvQswx+HJScGAadIjemvytJXm1fcl3+YZ9T9Ym0aERcVe5kDs+ny3X4vA==",
"license": "MIT",
"dependencies": {
"@react-navigation/elements": "^1.3.31",
"warn-once": "^0.1.0"
"@react-navigation/elements": "^2.2.5",
"warn-once": "^0.1.1"
},
"peerDependencies": {
"@react-navigation/native": "^6.0.0",
"react": "*",
"@react-navigation/native": "^7.0.14",
"react": ">= 18.2.0",
"react-native": "*",
"react-native-safe-area-context": ">= 3.0.0",
"react-native-screens": ">= 3.0.0"
"react-native-safe-area-context": ">= 4.0.0",
"react-native-screens": ">= 4.0.0"
}
},
"node_modules/@react-navigation/routers": {
"version": "6.1.9",
"resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-6.1.9.tgz",
"integrity": "sha512-lTM8gSFHSfkJvQkxacGM6VJtBt61ip2XO54aNfswD+KMw6eeZ4oehl7m0me3CR9hnDE4+60iAZR8sAhvCiI3NA==",
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.1.2.tgz",
"integrity": "sha512-emdEjpVDK8zbiu2GChC8oYIAub9i/OpNuQJekVsbyFCBz4/TzaBzms38Q53YaNhdIFNmiYLfHv/Y1Ub7KYfm3w==",
"license": "MIT",
"dependencies": {
"nanoid": "^3.1.23"
"nanoid": "3.3.8"
}
},
"node_modules/@realm/fetch": {
@ -6544,6 +6497,144 @@
"node": ">=6.0.0"
}
},
"node_modules/@testing-library/react-native": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/@testing-library/react-native/-/react-native-13.0.1.tgz",
"integrity": "sha512-sKMRNNniSOZ68qe1OBQAWvK87WCEmbfLp/MXfn2JE3x3WrNU8OFCVL0z/YKqw0/JO/d44J8Wq6FmRSaod/+VAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"chalk": "^4.1.2",
"jest-matcher-utils": "^29.7.0",
"pretty-format": "^29.7.0",
"redent": "^3.0.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"jest": ">=29.0.0",
"react": ">=18.2.0",
"react-native": ">=0.71",
"react-test-renderer": ">=18.2.0"
},
"peerDependenciesMeta": {
"jest": {
"optional": true
}
}
},
"node_modules/@testing-library/react-native/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/@testing-library/react-native/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/@testing-library/react-native/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/@testing-library/react-native/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT"
},
"node_modules/@testing-library/react-native/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/@testing-library/react-native/node_modules/pretty-format": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
"integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/schemas": "^29.6.3",
"ansi-styles": "^5.0.0",
"react-is": "^18.0.0"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/@testing-library/react-native/node_modules/pretty-format/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/@testing-library/react-native/node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"dev": true,
"license": "MIT"
},
"node_modules/@testing-library/react-native/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@ -6790,9 +6881,9 @@
}
},
"node_modules/@types/react-test-renderer": {
"version": "18.3.0",
"resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-18.3.0.tgz",
"integrity": "sha512-HW4MuEYxfDbOHQsVlY/XtOvNHftCVEPhJF2pQXXwcUiUF+Oyb0usgp48HSgpK5rt8m9KZb22yqOeZm+rrVG8gw==",
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-19.0.0.tgz",
"integrity": "sha512-qDVnNybqFm2eZKJ4jD34EvRd6VHD67KjgnWaEMM0Id9L22EpWe3nOSVKHWL1XWRCxUWe3lhXwlEeCKD1BlJCQA==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -13170,6 +13261,16 @@
"node": ">=0.8.19"
}
},
"node_modules/indent-string": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@ -19696,6 +19797,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/min-indent": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
@ -19861,9 +19972,9 @@
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"funding": [
{
"type": "github",
@ -21469,9 +21580,9 @@
}
},
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
@ -21620,9 +21731,9 @@
}
},
"node_modules/react-native-camera-kit": {
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/react-native-camera-kit/-/react-native-camera-kit-14.1.0.tgz",
"integrity": "sha512-idkg+Sa2KbGvF6SUqmuAr2U12qBELdiuUJ6fxgB4whUC2AyYHi5jBxiGv6whY/eTB3is7nW1S+TjyM9pEBzNzw==",
"version": "14.2.0",
"resolved": "https://registry.npmjs.org/react-native-camera-kit/-/react-native-camera-kit-14.2.0.tgz",
"integrity": "sha512-rPk/4Ux52/Kc6oIPk0x6NsrvDkeL+kd/GAUJ4xBtTlnmiWjLTgeA2Vjgg9ik03mmyf6rV+LaqaOBT7KejhuHKQ==",
"license": "MIT",
"engines": {
"node": ">=18"
@ -21714,6 +21825,21 @@
"react-native": ">=0.64.0"
}
},
"node_modules/react-native-drawer-layout": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/react-native-drawer-layout/-/react-native-drawer-layout-4.1.1.tgz",
"integrity": "sha512-ob6O3ph7PZ3A2FpdlsSxHuMpHDXREZPR8A6S3q0dSxV7i6d+8Z6CPCTbegfN2QZyizSow9NLrKyXP93tlqZ3dA==",
"license": "MIT",
"dependencies": {
"use-latest-callback": "^0.2.1"
},
"peerDependencies": {
"react": ">= 18.2.0",
"react-native": "*",
"react-native-gesture-handler": ">= 2.0.0",
"react-native-reanimated": ">= 2.0.0"
}
},
"node_modules/react-native-fs": {
"version": "2.20.0",
"resolved": "https://registry.npmjs.org/react-native-fs/-/react-native-fs-2.20.0.tgz",
@ -21734,9 +21860,9 @@
}
},
"node_modules/react-native-gesture-handler": {
"version": "2.22.1",
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.22.1.tgz",
"integrity": "sha512-E0C9D+Ia2UZYevoSV9rTKjhFWEVdR/3l4Z3TUoQrI/wewgzDlmJOrYvGW5aMlPUuQF2vHQOdFfAWhVEqFu4tWw==",
"version": "2.23.1",
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.23.1.tgz",
"integrity": "sha512-dr5CxIu+kXlxS3snSyt0qXtXGqfDvV26gtQVPNteFIfP0qWBuye/j48XPXePe2pH48QCuXpZM2VOooig7jzooA==",
"license": "MIT",
"dependencies": {
"@egjs/hammerjs": "^2.0.17",
@ -21770,9 +21896,9 @@
}
},
"node_modules/react-native-image-picker": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/react-native-image-picker/-/react-native-image-picker-7.2.2.tgz",
"integrity": "sha512-hcSaD/ohDac1ooDbZyKltfmOEAWrQOrPbusKEfbJQ7EfGveBEIF5wfmZsCjXRc0HNpGdQ71P1x2JZckkqu7IRw==",
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/react-native-image-picker/-/react-native-image-picker-7.2.3.tgz",
"integrity": "sha512-zKIZUlQNU3EtqizsXSH92zPeve4vpUrsqHu2kkpCxWE9TZhJFZBb+irDsBOY8J21k0+Edgt06TMQGJ+iPUIXyA==",
"license": "MIT",
"peerDependencies": {
"react": "*",
@ -21832,9 +21958,9 @@
}
},
"node_modules/react-native-permissions": {
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/react-native-permissions/-/react-native-permissions-5.2.4.tgz",
"integrity": "sha512-WmFY3mDBwj0eO93ziEi8r+6uuZo6XrcEIdQ66AfzC8CeNXXclDwypTnewBj51K0mn4QVRJxM6PSgIlj0ii5qig==",
"version": "5.2.5",
"resolved": "https://registry.npmjs.org/react-native-permissions/-/react-native-permissions-5.2.5.tgz",
"integrity": "sha512-pECev4EuA+XAq9kduJu9V5OtqKlOokf+5lawPmkTHvr6LoOl6VLZlI2Ue4tnOu1PLc6tQaG19kQ5gbKG4gyNAw==",
"license": "MIT",
"peerDependencies": {
"react": ">=18.1.0",
@ -21956,9 +22082,9 @@
}
},
"node_modules/react-native-safe-area-context": {
"version": "4.14.1",
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-4.14.1.tgz",
"integrity": "sha512-+tUhT5WBl8nh5+P+chYhAjR470iCByf9z5EYdCEbPaAK3Yfzw+o8VRPnUgmPAKlSccOgQBxx3NOl/Wzckn9ujg==",
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.2.0.tgz",
"integrity": "sha512-QpcGA6MRKe8Zbpf1hirCBudNQYGsMv0n/CTTROMOFcXbqRUoEXLCsYxUmYKi7JJb3ziL2DbyzWXyH2/gw4Tkfw==",
"license": "MIT",
"peerDependencies": {
"react": "*",
@ -21975,9 +22101,9 @@
}
},
"node_modules/react-native-screens": {
"version": "3.35.0",
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-3.35.0.tgz",
"integrity": "sha512-rmkqb/M/SQIrXwygk6pXcOhgHltYAhidf1WceO7ujAxkr6XtwmgFyd1HIztsrJa568GrAuwPdQ11I7TpVk+XsA==",
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.6.0.tgz",
"integrity": "sha512-PqGtR/moJLiTMSavhfo5spKXNHZrlxffq3g5UUVPmyuu7MmazFlPvYqiAYnR2iB9tkJYgvZO6sbjYAE7619M0A==",
"license": "MIT",
"dependencies": {
"react-freeze": "^1.0.0",
@ -22289,7 +22415,6 @@
"version": "16.15.0",
"resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz",
"integrity": "sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"object-assign": "^4.1.1",
@ -22300,32 +22425,29 @@
}
},
"node_modules/react-test-renderer": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-18.3.1.tgz",
"integrity": "sha512-KkAgygexHUkQqtvvx/otwxtuFu5cVjfzTCtjXLH9boS19/Nbtg84zS7wIQn39G8IlrhThBpQsMKkq5ZHZIYFXA==",
"dev": true,
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-18.2.0.tgz",
"integrity": "sha512-JWD+aQ0lh2gvh4NM3bBM42Kx+XybOxCpgYK7F8ugAlpaTSnWsX+39Z4XkOykGZAHrjwwTZT3x3KxswVWxHPUqA==",
"license": "MIT",
"dependencies": {
"react-is": "^18.3.1",
"react-is": "^18.2.0",
"react-shallow-renderer": "^16.15.0",
"scheduler": "^0.23.2"
"scheduler": "^0.23.0"
},
"peerDependencies": {
"react": "^18.3.1"
"react": "^18.2.0"
}
},
"node_modules/react-test-renderer/node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"dev": true,
"license": "MIT"
},
"node_modules/react-test-renderer/node_modules/scheduler": {
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
@ -22494,6 +22616,20 @@
"node": ">= 4"
}
},
"node_modules/redent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
"integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
"dev": true,
"license": "MIT",
"dependencies": {
"indent-string": "^4.0.0",
"strip-indent": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/reduce-flatten": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz",
@ -23710,6 +23846,19 @@
"node": ">=6"
}
},
"node_modules/strip-indent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
"integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"min-indent": "^1.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@ -24742,6 +24891,15 @@
"react": ">=16.8"
}
},
"node_modules/use-sync-external-store": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz",
"integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/utf8": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz",

View File

@ -16,13 +16,14 @@
"@react-native/js-polyfills": "^0.76.7",
"@react-native/metro-babel-transformer": "^0.76.7",
"@react-native/typescript-config": "0.76.7",
"@testing-library/react-native": "^13.0.1",
"@types/bip38": "^3.1.2",
"@types/bs58check": "^2.1.0",
"@types/create-hash": "^1.2.2",
"@types/crypto-js": "^4.2.2",
"@types/jest": "^29.5.2",
"@types/react": "^18.2.16",
"@types/react-test-renderer": "^18.0.0",
"@types/react-test-renderer": "^19.0.0",
"@types/wif": "^2.0.5",
"@typescript-eslint/eslint-plugin": "^7.15.0",
"@typescript-eslint/parser": "^7.15.0",
@ -44,7 +45,6 @@
"metro-react-native-babel-preset": "0.76.8",
"node-fetch": "^2.6.7",
"prettier": "^3.2.5",
"react-test-renderer": "18.3.1",
"ts-jest": "^29.1.1",
"typescript": "^5.1.6"
},
@ -66,9 +66,11 @@
"jest": "jest tests/integration/*",
"e2e:debug-build": "detox build -c android.debug",
"e2e:debug-test": "detox test -c android.debug -d 200000 --loglevel error --reuse",
"e2e:debug-test-device": "detox test -c android.debug.device -d 200000 --loglevel error --reuse",
"e2e:debug": "(test -f android/app/build/outputs/apk/debug/app-debug.apk && test -f android/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk) || npm run e2e:debug-build; npm run e2e:debug-test",
"e2e:release-build": "detox build -c android.release",
"e2e:release-test": "detox test -c android.release --loglevel error",
"e2e:release-test-device": "detox test -c android.release.device --loglevel info --record-videos all",
"tslint": "tsc",
"lint": " npm run tslint && node scripts/find-unused-loc.js && eslint --ext .js,.ts,.tsx '*.@(js|ts|tsx)' screen 'blue_modules/*.@(js|ts|tsx)' class models loc tests components navigation typings",
"lint:fix": "npm run lint -- --fix",
@ -79,22 +81,23 @@
"@babel/preset-env": "7.25.8",
"@bugsnag/react-native": "8.2.0",
"@bugsnag/source-maps": "2.3.3",
"@react-native-community/cli": "15.1.3",
"@react-native-community/cli-platform-android": "15.1.3",
"@react-native-community/cli-platform-ios": "15.1.3",
"@keystonehq/bc-ur-registry": "0.7.0",
"@lodev09/react-native-true-sheet": "github:BlueWallet/react-native-true-sheet#c6fee8925120d94328dc2aace45d4fa0a111d57f",
"@ngraveio/bc-ur": "1.1.13",
"@noble/secp256k1": "1.6.3",
"@react-native-async-storage/async-storage": "2.1.0",
"@react-native-clipboard/clipboard": "1.16.1",
"@react-native-community/cli": "15.0.1",
"@react-native-community/cli-platform-android": "15.0.1",
"@react-native-community/cli-platform-ios": "15.0.1",
"@react-native-community/push-notification-ios": "1.11.0",
"@react-native-menu/menu": "https://github.com/BlueWallet/menu.git#14bab79",
"@react-native/gradle-plugin": "0.76.7",
"@react-native/metro-config": "0.76.7",
"@react-navigation/drawer": "6.7.2",
"@react-navigation/native": "6.1.18",
"@react-navigation/native-stack": "6.11.0",
"@react-navigation/devtools": "7.0.15",
"@react-navigation/drawer": "7.1.1",
"@react-navigation/native": "7.0.14",
"@react-navigation/native-stack": "7.2.0",
"@rneui/base": "4.0.0-rc.8",
"@rneui/themed": "4.0.0-rc.8",
"@spsina/bip47": "github:BlueWallet/bip47#df82345",
@ -127,27 +130,27 @@
"payjoin-client": "1.0.1",
"process": "0.11.10",
"prop-types": "15.8.1",
"react": "18.3.1",
"react": "18.2.0",
"react-localization": "github:BlueWallet/react-localization#ae7969a",
"react-native": "0.76.7",
"react-native-biometrics": "3.0.1",
"react-native-blue-crypto": "github:BlueWallet/react-native-blue-crypto#3cb5442",
"react-native-camera-kit": "14.1.0",
"react-native-camera-kit": "14.2.0",
"react-native-crypto": "2.2.0",
"react-native-default-preference": "https://github.com/BlueWallet/react-native-default-preference.git#6338a1f1235e4130b8cfc2dd3b53015eeff2870c",
"react-native-device-info": "14.0.2",
"react-native-document-picker": "9.3.1",
"react-native-draglist": "github:BlueWallet/react-native-draglist#a4af02f",
"react-native-fs": "2.20.0",
"react-native-gesture-handler": "2.22.1",
"react-native-gesture-handler": "2.23.1",
"react-native-handoff": "github:BlueWallet/react-native-handoff#v0.0.4",
"react-native-haptic-feedback": "2.3.3",
"react-native-image-picker": "7.2.2",
"react-native-image-picker": "7.2.3",
"react-native-ios-context-menu": "github:BlueWallet/react-native-ios-context-menu#e5c1217cd220bfab6e6d9a7c65838545082e3f8e",
"react-native-keychain": "9.1.0",
"react-native-linear-gradient": "2.8.3",
"react-native-localize": "3.4.1",
"react-native-permissions": "5.2.4",
"react-native-permissions": "5.2.5",
"react-native-prompt-android": "github:BlueWallet/react-native-prompt-android#ed168d66fed556bc2ed07cf498770f058b78a376",
"react-native-push-notification": "8.1.1",
"react-native-qrcode-svg": "6.3.12",
@ -155,15 +158,16 @@
"react-native-randombytes": "3.6.1",
"react-native-rate": "1.2.12",
"react-native-reanimated": "3.16.7",
"react-native-safe-area-context": "4.14.1",
"react-native-safe-area-context": "5.2.0",
"react-native-screen-capture": "github:BlueWallet/react-native-screen-capture#18cb79f",
"react-native-screens": "3.35.0",
"react-native-screens": "4.6.0",
"react-native-secure-key-store": "github:BlueWallet/react-native-secure-key-store#2076b4849e88aa0a78e08bfbb4ce3923e0925cbc",
"react-native-share": "11.1.0",
"react-native-svg": "15.11.1",
"react-native-tcp-socket": "6.2.0",
"react-native-vector-icons": "10.2.0",
"react-native-watch-connectivity": "1.1.0",
"react-test-renderer": "18.2.0",
"readable-stream": "3.6.2",
"realm": "20.1.0",
"rn-nodeify": "10.3.0",

View File

@ -11,6 +11,7 @@ import PromptPasswordConfirmationModal, {
MODAL_TYPES,
} from '../components/PromptPasswordConfirmationModal';
import { useExtendedNavigation } from '../hooks/useExtendedNavigation';
import { StackActions } from '@react-navigation/native';
// Action Types
const SET_LOADING = 'SET_LOADING';
@ -46,13 +47,13 @@ function reducer(state: State, action: Action): State {
const PlausibleDeniability: React.FC = () => {
const { cachedPassword, isPasswordInUse, createFakeStorage, resetWallets } = useStorage();
const [state, dispatch] = useReducer(reducer, initialState);
const { navigate } = useExtendedNavigation();
const navigation = useExtendedNavigation();
const promptRef = useRef<PromptPasswordConfirmationModalHandle>(null);
const handleOnCreateFakeStorageButtonPressed = async () => {
dispatch({ type: SET_LOADING, payload: true });
dispatch({ type: SET_MODAL_TYPE, payload: MODAL_TYPES.CREATE_FAKE_STORAGE });
promptRef.current?.present();
await promptRef.current?.present();
};
const handleConfirmationSuccess = async (password: string) => {
@ -73,8 +74,9 @@ const PlausibleDeniability: React.FC = () => {
dispatch({ type: SET_MODAL_TYPE, payload: MODAL_TYPES.SUCCESS });
success = true;
setTimeout(() => {
navigate('WalletsList');
setTimeout(async () => {
const popToTop = StackActions.popToTop();
navigation.dispatch(popToTop);
}, 3000);
} catch {
success = false;

View File

@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { RouteProp, useRoute } from '@react-navigation/native';
import { RouteProp, StackActions, useRoute } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { Avatar, Badge, Icon, ListItem as RNElementsListItem } from '@rneui/themed';
import {
@ -17,7 +17,6 @@ import {
View,
} from 'react-native';
import * as RNLocalize from 'react-native-localize';
import debounce from '../../blue_modules/debounce';
import { BlueSpacing10, BlueSpacing20 } from '../../BlueComponents';
import { TWallet, Utxo } from '../../class/wallets/types';
@ -35,6 +34,7 @@ import loc, { formatBalance } from '../../loc';
import { BitcoinUnit } from '../../models/bitcoinUnits';
import { SendDetailsStackParamList } from '../../navigation/SendDetailsStackParamList';
import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
import { useKeyboard } from '../../hooks/useKeyboard';
import TipBox from '../../components/TipBox';
type NavigationProps = NativeStackNavigationProp<SendDetailsStackParamList, 'CoinControl'>;
@ -315,6 +315,7 @@ const CoinControl: React.FC = () => {
const [output, setOutput] = useState<Utxo | undefined>();
const [loading, setLoading] = useState<boolean>(true);
const [selected, setSelected] = useState<string[]>([]);
const { isVisible } = useKeyboard();
// save frozen status. Because effect called on each event, debounce it.
const debouncedSaveFronen = useRef(
@ -373,14 +374,8 @@ const CoinControl: React.FC = () => {
const handleUseCoin = async (u: Utxo[]) => {
setOutput(undefined);
// @ts-ignore navigation WTF
navigation.navigate('SendDetailsRoot', {
screen: 'SendDetails',
params: {
utxos: u,
},
merge: true,
});
const popToAction = StackActions.popTo('SendDetails', { walletID, utxos: u }, true);
navigation.dispatch(popToAction);
};
const handleMassFreeze = () => {
@ -525,15 +520,17 @@ const CoinControl: React.FC = () => {
contentContainerStyle={styles.modalMinHeight}
footer={
<View style={mStyles.buttonContainer}>
<Button
testID="UseCoin"
title={loc.cc.use_coin}
onPress={async () => {
if (!output) throw new Error('output is not set');
await bottomModalRef.current?.dismiss();
handleUseCoin([output]);
}}
/>
{!isVisible && (
<Button
testID="UseCoin"
title={loc.cc.use_coin}
onPress={async () => {
if (!output) throw new Error('output is not set');
await bottomModalRef.current?.dismiss();
handleUseCoin([output]);
}}
/>
)}
</View>
}
>

View File

@ -1,4 +1,4 @@
import { useFocusEffect, useIsFocused, useNavigation, useRoute } from '@react-navigation/native';
import { StackActions, useFocusEffect, useIsFocused, useRoute } from '@react-navigation/native';
import * as bitcoin from 'bitcoinjs-lib';
import createHash from 'create-hash';
import React, { useCallback, useEffect, useState } from 'react';
@ -13,6 +13,7 @@ import { useTheme } from '../../components/themes';
import { isCameraAuthorizationStatusGranted } from '../../helpers/scan-qr';
import loc from '../../loc';
import { useSettings } from '../../hooks/context/useSettings';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import CameraScreen from '../../components/CameraScreen';
import SafeArea from '../../components/SafeArea';
import presentAlert from '../../components/Alert';
@ -34,7 +35,7 @@ const styles = StyleSheet.create({
height: 60,
backgroundColor: 'rgba(0,0,0,0.01)',
position: 'absolute',
top: 10,
top: 60,
left: '50%',
transform: [{ translateX: -30 }],
},
@ -53,13 +54,13 @@ const styles = StyleSheet.create({
const ScanQRCode = () => {
const [isLoading, setIsLoading] = useState(false);
const { setIsDrawerShouldHide } = useSettings();
const navigation = useNavigation();
const navigation = useExtendedNavigation();
const route = useRoute();
const navigationState = navigation.getState();
const previousRoute = navigationState.routes[navigationState.routes.length - 2];
const defaultLaunchedBy = previousRoute ? previousRoute.name : undefined;
const { launchedBy = defaultLaunchedBy, onBarScanned, showFileImportButton } = route.params || {};
const { launchedBy = defaultLaunchedBy, showFileImportButton } = route.params || {};
const scannedCache = {};
const { colors } = useTheme();
const isFocused = useIsFocused();
@ -110,16 +111,15 @@ const ScanQRCode = () => {
decoder = false; // nullify for future use (?)
if (launchedBy) {
const merge = true;
navigation.navigate({ name: launchedBy, params: { onBarScanned: data }, merge });
} else {
onBarScanned && onBarScanned({ data });
const popToAction = StackActions.popTo(launchedBy, { onBarScanned: data }, merge);
navigation.dispatch(popToAction);
}
} else {
setUrTotal(100);
setUrHave(Math.floor(decoder.estimatedPercentComplete() * 100));
}
} catch (error) {
console.warn(error);
setIsLoading(true);
presentAlert({
title: loc.send.scan_error,
@ -154,15 +154,14 @@ const ScanQRCode = () => {
}
if (launchedBy) {
const merge = true;
navigation.navigate({ name: launchedBy, params: { onBarScanned: data }, merge });
} else {
onBarScanned && onBarScanned({ data });
const popToAction = StackActions.popTo(launchedBy, { onBarScanned: data }, merge);
navigation.dispatch(popToAction);
}
} else {
setAnimatedQRCodeData(animatedQRCodeData);
}
} catch (error) {
console.warn(error);
setIsLoading(true);
presentAlert({
@ -211,28 +210,25 @@ const ScanQRCode = () => {
const hex = Base43.decode(ret.data);
bitcoin.Psbt.fromHex(hex); // if it doesnt throw - all good
const data = Buffer.from(hex, 'hex').toString('base64');
if (launchedBy) {
const merge = true;
navigation.navigate({ name: launchedBy, params: { onBarScanned: data }, merge });
} else {
onBarScanned && onBarScanned({ data });
const popToAction = StackActions.popTo(launchedBy, { onBarScanned: data }, merge);
navigation.dispatch(popToAction);
}
return;
} catch (_) {}
if (!isLoading) {
setIsLoading(true);
try {
if (launchedBy) {
} catch (_) {
if (!isLoading) {
setIsLoading(true);
try {
const merge = true;
navigation.navigate({ name: launchedBy, params: { onBarScanned: ret.data }, merge });
} else {
onBarScanned && onBarScanned(ret.data);
const popToAction = StackActions.popTo(launchedBy, { onBarScanned: ret.data }, merge);
navigation.dispatch(popToAction);
} catch (e) {
console.log(e);
}
} catch (e) {
console.log(e);
}
}
setIsLoading(false);

View File

@ -359,9 +359,7 @@ const SendDetails = () => {
useCallback(() => {
setIsLoading(false);
setDumb(v => !v);
return () => {
feeModalRef.current?.dismiss();
};
return () => {};
}, []),
);
@ -1185,11 +1183,11 @@ const SendDetails = () => {
const renderWalletSelectionOrCoinsSelected = () => {
if (isVisible) return null;
if (utxos !== null) {
if (utxos && utxos?.length > 0) {
return (
<View style={styles.select}>
<CoinsSelected
number={utxos?.length || 0}
number={utxos?.length}
onContainerPress={handleCoinControl}
onClose={() => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);

View File

@ -15,6 +15,7 @@ import loc from '../../loc';
import { BitcoinUnit } from '../../models/bitcoinUnits';
import { useStorage } from '../../hooks/context/useStorage';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import { combinePSBTs } from '../../utils/combinePSBTs';
const PsbtMultisig = () => {
const { wallets } = useStorage();
@ -24,7 +25,25 @@ const PsbtMultisig = () => {
const { walletID, psbtBase64, memo, receivedPSBTBase64, launchedBy } = useRoute().params;
/** @type MultisigHDWallet */
const wallet = wallets.find(w => w.getID() === walletID);
const [psbt, setPsbt] = useState(bitcoin.Psbt.fromBase64(psbtBase64));
const [psbt, setPsbt] = useState(() => {
try {
const initial = bitcoin.Psbt.fromBase64(psbtBase64);
return initial;
} catch (error) {
console.error('Error loading initial PSBT:', error);
presentAlert({ message: loc.send.invalid_psbt });
return null;
}
});
useEffect(() => {
if (receivedPSBTBase64) {
_combinePSBT();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [receivedPSBTBase64]);
const data = new Array(wallet.getM());
const stylesHook = StyleSheet.create({
root: {
@ -65,6 +84,10 @@ const PsbtMultisig = () => {
},
});
const [isFiltered, setIsFiltered] = useState(true);
if (!psbt) return null;
// if useFilter is true, include only non-owned addresses.
const getDestinationData = (useFilter = true) => {
const addresses = [];
@ -86,8 +109,6 @@ const PsbtMultisig = () => {
const targets = filteredData.targets;
const [isFiltered, setIsFiltered] = useState(true);
const displayData = isFiltered ? filteredData : unfilteredData;
const displayTotalBtc = new BigNumber(displayData.totalSat).dividedBy(100000000).toNumber();
const displayTotalFiat = satoshiToLocalCurrency(displayData.totalSat);
@ -153,28 +174,25 @@ const PsbtMultisig = () => {
);
};
useEffect(() => {
if (receivedPSBTBase64) {
_combinePSBT();
setParams({ receivedPSBTBase64: undefined });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [receivedPSBTBase64]);
const _combinePSBT = () => {
try {
const receivedPSBT = bitcoin.Psbt.fromBase64(receivedPSBTBase64);
const newPsbt = psbt.combine(receivedPSBT);
setPsbt(newPsbt);
} catch (error) {
presentAlert({ message: error });
if (receivedPSBTBase64 && receivedPSBTBase64 !== psbt.toBase64()) {
try {
const combined = combinePSBTs({ psbtBase64: psbt.toBase64(), newPSBTBase64: receivedPSBTBase64 });
setPsbt(combined);
setParams({ receivedPSBTBase64: undefined });
} catch (error) {
console.error('Error during PSBT combination:', error);
presentAlert({ message: error.message });
}
}
};
const onConfirm = () => {
try {
psbt.finalizeAllInputs();
} catch (_) {} // ignore if it is already finalized
} catch (err) {
console.warn('Finalize error (ignored if already finalized):', err);
}
if (launchedBy) {
// we must navigate back to the screen who requested psbt (instead of broadcasting it ourselves)
@ -277,7 +295,8 @@ const PsbtMultisig = () => {
const footer = null;
const onLayout = event => {
setFlatListHeight(event.nativeEvent.layout.height);
const newHeight = event.nativeEvent.layout.height;
setFlatListHeight(newHeight);
};
return (
@ -295,6 +314,7 @@ const PsbtMultisig = () => {
data={data}
renderItem={_renderItem}
keyExtractor={(_item, index) => `${index}`}
extraData={psbt} // Ensure FlatList updates when psbt changes
ListHeaderComponent={header}
ListFooterComponent={footer}
onLayout={onLayout}
@ -305,7 +325,9 @@ const PsbtMultisig = () => {
accessibilityRole="button"
testID="ExportSignedPsbt"
style={[styles.provideSignatureButton, stylesHook.provideSignatureButton]}
onPress={navigateToPSBTMultisigQRCode}
onPress={() => {
navigateToPSBTMultisigQRCode();
}}
>
<Text style={[styles.provideSignatureButtonText, stylesHook.provideSignatureButtonText]}>
{loc.multisig.export_signed_psbt}

View File

@ -1,4 +1,4 @@
import { useIsFocused, useNavigation, useRoute } from '@react-navigation/native';
import { StackActions, useIsFocused, useNavigation, useRoute } from '@react-navigation/native';
import * as bitcoin from 'bitcoinjs-lib';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { ActivityIndicator, ScrollView, StyleSheet, View } from 'react-native';
@ -48,16 +48,17 @@ const PsbtMultisigQRCode = () => {
if (!ret.data) ret = { data: ret };
if (ret.data.toUpperCase().startsWith('UR')) {
presentAlert({ message: 'BC-UR not decoded. This should never happen' });
} else if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1 && ret.data.indexOf('=') === -1) {
} else if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1) {
presentAlert({ message: loc.wallets.import_error });
// this looks like NOT base64, so maybe its transaction's hex
// we dont support it in this flow
presentAlert({ message: loc.wallets.import_error });
} else {
// psbt base64?
navigation.navigate({ name: 'PsbtMultisig', params: { receivedPSBTBase64: ret.data }, merge: true });
const popToAction = StackActions.popTo('PsbtMultisig', { psbtBase64, receivedPSBTBase64: ret.data, ...params }, true);
navigation.dispatch(popToAction);
}
},
[navigation],
[navigation, psbtBase64, params],
);
useEffect(() => {

View File

@ -1,5 +1,5 @@
import Clipboard from '@react-native-clipboard/clipboard';
import { useIsFocused, useRoute } from '@react-navigation/native';
import { StackActions, useIsFocused, useRoute } from '@react-navigation/native';
import * as bitcoin from 'bitcoinjs-lib';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { ActivityIndicator, Linking, Platform, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native';
@ -85,7 +85,8 @@ const PsbtWithHardwareWallet = () => {
if (launchedBy) {
// we must navigate back to the screen who requested psbt (instead of broadcasting it ourselves)
// most likely for LN channel opening
navigation.navigate({ name: launchedBy, params: { psbt }, merge: true });
const popToAction = StackActions.popTo(launchedBy, { psbt }, true);
navigation.dispatch(popToAction);
// ^^^ we just use `psbt` variable sinse it was finalized in the above _combinePSBT()
// (passed by reference)
}
@ -225,7 +226,7 @@ const PsbtWithHardwareWallet = () => {
useEffect(() => {
const data = route.params.onBarScanned;
if (data) {
onBarScanned(data);
onBarScanned({ data });
navigation.setParams({ onBarScanned: undefined });
}
}, [navigation, onBarScanned, route.params.onBarScanned]);

View File

@ -10,10 +10,10 @@ import PromptPasswordConfirmationModal, {
MODAL_TYPES,
PromptPasswordConfirmationModalHandle,
} from '../../components/PromptPasswordConfirmationModal';
import { popToTop } from '../../NavigationService';
import presentAlert from '../../components/Alert';
import { Header } from '../../components/Header';
import { BlueSpacing20 } from '../../BlueComponents';
import { StackActions } from '@react-navigation/native';
enum ActionType {
SetLoading = 'SET_LOADING',
@ -65,7 +65,7 @@ const EncryptStorage = () => {
const { isStorageEncrypted, encryptStorage, decryptStorage, saveToDisk } = useStorage();
const { isDeviceBiometricCapable, biometricEnabled, setBiometricUseEnabled, deviceBiometricType } = useBiometrics();
const [state, dispatch] = useReducer(reducer, initialState);
const { navigate } = useExtendedNavigation();
const navigation = useExtendedNavigation();
const { colors } = useTheme();
const promptRef = useRef<PromptPasswordConfirmationModalHandle>(null);
@ -134,7 +134,12 @@ const EncryptStorage = () => {
};
const navigateToPlausibleDeniability = () => {
navigate('PlausibleDeniability');
navigation.navigate('PlausibleDeniability');
};
const popToTop = () => {
const action = StackActions.popToTop();
navigation.dispatch(action);
};
return (
@ -178,6 +183,7 @@ const EncryptStorage = () => {
onValueChange: onEncryptStorageSwitch,
value: state.storageIsEncryptedSwitchEnabled,
disabled: state.currentLoadingSwitch !== null,
testID: 'EncyptedAndPasswordProtectedSwitch',
}}
isLoading={state.currentLoadingSwitch === 'encrypt' && state.isLoading}
containerStyle={[styles.row, styleHooks.root]}
@ -212,7 +218,7 @@ const EncryptStorage = () => {
await decryptStorage(password);
await saveToDisk();
popToTop();
success = true;
return true;
} catch (error) {
success = false;
}

View File

@ -289,7 +289,7 @@ export default class SelfTest extends Component {
<BlueSpacing20 />
{this.state.isLoading ? (
<BlueLoading />
<BlueLoading testID="SelfTestLoading" />
) : (
(() => {
if (this.state.isOk) {

View File

@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useState } from 'react';
import Clipboard from '@react-native-clipboard/clipboard';
import { RouteProp, useFocusEffect, useRoute } from '@react-navigation/native';
import { RouteProp, useFocusEffect, usePreventRemove, useRoute } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import assert from 'assert';
import dayjs from 'dayjs';
@ -60,7 +60,7 @@ type NavigationProps = NativeStackNavigationProp<DetailViewStackParamList, 'Tran
type RouteProps = RouteProp<DetailViewStackParamList, 'TransactionDetails'>;
const TransactionDetails = () => {
const { addListener, navigate } = useExtendedNavigation<NavigationProps>();
const { navigate } = useExtendedNavigation<NavigationProps>();
const { hash, walletID } = useRoute<RouteProps>().params;
const { saveToDisk, txMetadata, counterpartyMetadata, wallets, getTransactions } = useStorage();
const { selectedBlockExplorer } = useSettings();
@ -97,13 +97,9 @@ const TransactionDetails = () => {
}
}, [tx, txMetadata, memo, counterpartyLabel, paymentCode, saveToDisk, counterpartyMetadata]);
useEffect(() => {
const unsubscribe = addListener('beforeRemove', () => {
saveTransactionDetails();
});
return unsubscribe;
}, [addListener, saveTransactionDetails]);
usePreventRemove(false, () => {
saveTransactionDetails();
});
useFocusEffect(
useCallback(() => {

View File

@ -1,8 +1,7 @@
import { DrawerContentScrollView } from '@react-navigation/drawer';
import { NavigationProp, ParamListBase, useIsFocused } from '@react-navigation/native';
import { useIsFocused } from '@react-navigation/native';
import React, { memo, useCallback, useEffect, useMemo, useReducer, useRef } from 'react';
import { InteractionManager, LayoutAnimation, StyleSheet, View, ViewStyle } from 'react-native';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import { TWallet } from '../../class/wallets/types';
import { Header } from '../../components/Header';
@ -12,6 +11,7 @@ import loc from '../../loc';
import { useStorage } from '../../hooks/context/useStorage';
import TotalWalletsBalance from '../../components/TotalWalletsBalance';
import { useSettings } from '../../hooks/context/useSettings';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
enum WalletActionType {
SetWallets = 'SET_WALLETS',
@ -58,10 +58,6 @@ interface SelectWalletAction {
type WalletAction = SetWalletsAction | SelectWalletAction | SetFocusAction | NavigateAction;
interface DrawerListProps {
navigation: NavigationProp<ParamListBase>;
}
const walletReducer = (state: WalletState, action: WalletAction): WalletState => {
switch (action.type) {
case WalletActionType.SetWallets: {
@ -78,12 +74,14 @@ const walletReducer = (state: WalletState, action: WalletAction): WalletState =>
}
};
const DrawerList: React.FC<DrawerListProps> = memo(({ navigation }) => {
const DrawerList: React.FC = memo(() => {
const initialState: WalletState = {
wallets: [],
isFocused: false,
};
const navigation = useExtendedNavigation();
const [state, dispatch] = useReducer(walletReducer, initialState);
const walletsCarousel = useRef(null);
const { wallets, selectedWalletID } = useStorage();
@ -118,8 +116,6 @@ const DrawerList: React.FC<DrawerListProps> = memo(({ navigation }) => {
params: { walletID, walletType },
});
});
} else {
navigation.navigate('AddWalletRoot');
}
},
[navigation],

View File

@ -126,7 +126,7 @@ const ImportCustomDerivationPath: React.FC = () => {
if (wallets[path] === WRONG_PATH) return;
addAndSaveWallet(wallets[path][type]);
// @ts-ignore: Navigation
navigation.getParent().pop();
navigation.getParent()?.goBack();
};
const renderItem = ({ item }: { item: TItem }) => {

View File

@ -74,7 +74,7 @@ const ImportSpeed = () => {
}
await wallet.fetchBalance();
// @ts-ignore: navigation
navigation.getParent().pop();
navigation.getParent()?.goBack();
addAndSaveWallet(wallet);
} catch (e: any) {
presentAlert({ message: e.message });

View File

@ -78,12 +78,16 @@ const ImportWallet = () => {
console.error('Failed to clear clipboard:', error);
}
}
Keyboard.dismiss();
navigation.navigate('ImportWalletDiscovery', {
importText: text,
askPassphrase: askPassphraseMenuState,
searchAccounts: searchAccountsMenuState,
});
},
[askPassphraseMenuState, clearClipboardMenuState, navigation, searchAccountsMenuState],
);

View File

@ -1,6 +1,6 @@
import React, { useEffect, useMemo, useState } from 'react';
import Clipboard from '@react-native-clipboard/clipboard';
import { RouteProp, useRoute } from '@react-navigation/native';
import { RouteProp, StackActions, useRoute } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import assert from 'assert';
import createHash from 'create-hash';
@ -179,16 +179,23 @@ export default function PaymentCodesList() {
};
const _navigateToSend = (pc: string) => {
navigation.navigate('SendDetailsRoot', {
screen: 'SendDetails',
params: {
const previousRoute = state.routes[state.routes.length - 2];
if (previousRoute.name === ('SendDetails' as string)) {
const popToAction = StackActions.popTo('SendDetails', {
walletID,
addRecipientParams: {
address: pc,
},
},
merge: true,
});
merge: true,
});
navigation.dispatch(popToAction);
} else {
navigation.navigate('SendDetailsRoot', {
paymentCode: pc,
walletID,
});
}
};
const renderItem = (pc: string, index: number) => {

View File

@ -35,7 +35,7 @@ const PleaseBackup: React.FC = () => {
const handleBackButton = useCallback(() => {
// @ts-ignore: Ignore
navigation.getParent()?.pop();
navigation.getParent()?.goBack();
return true;
}, [navigation]);

View File

@ -1,5 +1,5 @@
import React, { useEffect, useReducer, useState } from 'react';
import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
import { RouteProp, StackActions, useNavigation, useRoute } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { Icon } from '@rneui/themed';
import BN from 'bignumber.js';
@ -326,7 +326,10 @@ const ProvideEntropy = () => {
/* Convert Buffer to hex string before navigating as React Navigation
does not support passing Buffer objects between screens
*/
navigation.navigate('AddWallet', { entropy: buf.toString('hex'), words });
const popTo = StackActions.popTo('AddWallet', { entropy: buf.toString('hex'), words }, true);
navigation.dispatch(popTo);
},
style: 'default',
},

View File

@ -11,28 +11,19 @@ import { useStorage } from '../../hooks/context/useStorage';
import WalletsCarousel from '../../components/WalletsCarousel';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import { TWallet } from '../../class/wallets/types';
import { CloseButtonPosition } from '../../components/navigationStyle';
import { pop } from '../../NavigationService';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { SendDetailsStackParamList } from '../../navigation/SendDetailsStackParamList';
type SelectWalletRouteProp = RouteProp<
{
SelectWallet: {
chainType?: Chain;
onWalletSelect?: (wallet: TWallet, navigation: any) => void;
availableWallets?: TWallet[];
noWalletExplanationText?: string;
onChainRequireSend?: boolean;
};
},
'SelectWallet'
>;
type NavigationProps = NativeStackNavigationProp<SendDetailsStackParamList, 'SelectWallet'>;
type RouteProps = RouteProp<SendDetailsStackParamList, 'SelectWallet'>;
const SelectWallet: React.FC = () => {
const route = useRoute<SelectWalletRouteProp>();
const route = useRoute<RouteProps>();
const { chainType, onWalletSelect, availableWallets, noWalletExplanationText, onChainRequireSend = false } = route.params;
const [isLoading, setIsLoading] = useState(true);
const navigation = useExtendedNavigation();
const { navigate, setOptions } = navigation;
const navigation = useExtendedNavigation<NavigationProps>();
const { wallets } = useStorage();
const { colors } = useTheme();
const isModal = useNavigationState(state => state.routes.length === 1);
@ -67,23 +58,24 @@ const SelectWallet: React.FC = () => {
}, [availableWallets, chainType, onChainRequireSend, wallets]);
useEffect(() => {
setOptions({
navigation.setOptions({
statusBarStyle: isLoading || (availableWallets || filterWallets()).length === 0 ? 'light' : 'auto',
});
}, [isLoading, availableWallets, setOptions, filterWallets]);
}, [isLoading, availableWallets, filterWallets, navigation]);
useEffect(() => {
if (!isModal) {
setOptions({ CloseButtonPosition: CloseButtonPosition.None });
navigation.setOptions({ headerBackVisible: false });
}
}, [isModal, setOptions]);
}, [isModal, navigation]);
const onPress = (item: TWallet) => {
triggerHapticFeedback(HapticFeedbackTypes.Selection);
if (onWalletSelect) {
onWalletSelect(item, { navigation: { pop, navigate } });
onWalletSelect(item, { navigation: { pop, navigation: navigation.navigate } });
} else {
navigate(previousRouteName, { walletID: item.getID(), merge: true });
// @ts-ignore: fix later
navigation.popTo(previousRouteName, { walletID: item.getID(), merge: true });
}
};

View File

@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { CommonActions, RouteProp, useFocusEffect, useRoute } from '@react-navigation/native';
import { RouteProp, useFocusEffect, useRoute, usePreventRemove, CommonActions } from '@react-navigation/native';
import {
ActivityIndicator,
Alert,
@ -50,8 +50,8 @@ import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
import { useSettings } from '../../hooks/context/useSettings';
import { ViewEditMultisigCosignersStackParamList } from '../../navigation/ViewEditMultisigCosignersStack';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { navigationRef } from '../../NavigationService';
import SafeArea from '../../components/SafeArea';
import { TWallet } from '../../class/wallets/types';
type RouteParams = RouteProp<ViewEditMultisigCosignersStackParamList, 'ViewEditMultisigCosigners'>;
type NavigationProp = NativeStackNavigationProp<ViewEditMultisigCosignersStackParamList, 'ViewEditMultisigCosigners'>;
@ -62,7 +62,7 @@ const ViewEditMultisigCosigners: React.FC = () => {
const { wallets, setWalletsWithNewOrder } = useStorage();
const { isBiometricUseCapableAndEnabled } = useBiometrics();
const { isElectrumDisabled, isPrivacyBlurEnabled } = useSettings();
const { navigate, dispatch, addListener, setParams, setOptions } = useExtendedNavigation<NavigationProp>();
const { navigate, dispatch, setParams, setOptions } = useExtendedNavigation<NavigationProp>();
const openScannerButtonRef = useRef();
const route = useRoute<RouteParams>();
const { walletID } = route.params;
@ -82,7 +82,7 @@ const ViewEditMultisigCosigners: React.FC = () => {
const [vaultKeyData, setVaultKeyData] = useState({ keyIndex: 1, xpub: '', seed: '', passphrase: '', path: '', fp: '', isLoading: false }); // string rendered in modal
const [isVaultKeyIndexDataLoading, setIsVaultKeyIndexDataLoading] = useState<number | undefined>(undefined);
const [askPassphrase, setAskPassphrase] = useState(false);
const data = useRef<any[]>();
const [walletData, setWalletData] = useState<TWallet[]>([]);
/* discardChangesRef is only so the action sheet can be shown on mac catalyst when a
user tries to leave the screen with unsaved changes.
Why the container view ? It was the easiest to get the ref for. No other reason.
@ -116,53 +116,37 @@ const ViewEditMultisigCosigners: React.FC = () => {
color: colors.buttonTextColor,
},
});
useFocusEffect(
useCallback(() => {
const unsubscribe = addListener('beforeRemove', (e: { preventDefault: () => void; data: { action: any } }) => {
// Check if there are unsaved changes
if (isSaveButtonDisabled) {
// If there are no unsaved changes, let the user leave the screen
return;
}
// Prevent the default action (going back)
e.preventDefault();
// Show an alert asking the user to discard changes or cancel
if (isDesktop) {
if (!discardChangesRef.current) return dispatch(e.data.action);
const anchor = findNodeHandle(discardChangesRef.current);
if (!anchor) return dispatch(e.data.action);
ActionSheet.showActionSheetWithOptions(
{
options: [loc._.cancel, loc._.ok],
cancelButtonIndex: 0,
title: loc._.discard_changes,
message: loc._.discard_changes_explain,
anchor,
},
buttonIndex => {
if (buttonIndex === 1) {
dispatch(e.data.action);
}
},
);
} else {
Alert.alert(loc._.discard_changes, loc._.discard_changes_explain, [
{ text: loc._.cancel, style: 'cancel', onPress: () => {} },
{
text: loc._.ok,
style: 'default',
// If the user confirms, then we dispatch the action we blocked earlier
onPress: () => dispatch(e.data.action),
},
]);
}
});
return unsubscribe;
}, [isSaveButtonDisabled, addListener, dispatch]),
);
usePreventRemove(!isSaveButtonDisabled, ({ data }) => {
if (isDesktop) {
if (!discardChangesRef.current) return dispatch(data.action);
const anchor = findNodeHandle(discardChangesRef.current);
if (!anchor) return dispatch(data.action);
ActionSheet.showActionSheetWithOptions(
{
options: [loc._.cancel, loc._.ok],
cancelButtonIndex: 0,
title: loc._.discard_changes,
message: loc._.discard_changes_explain,
anchor,
},
buttonIndex => {
if (buttonIndex === 1) {
dispatch(data.action);
}
},
);
} else {
Alert.alert(loc._.discard_changes, loc._.discard_changes_explain, [
{ text: loc._.cancel, style: 'cancel', onPress: () => {} },
{
text: loc._.ok,
style: 'default',
onPress: () => dispatch(data.action),
},
]);
}
});
const onSave = async () => {
if (!wallet) {
@ -193,7 +177,7 @@ const ViewEditMultisigCosigners: React.FC = () => {
setIsSaveButtonDisabled(true);
setWalletsWithNewOrder(newWallets);
setTimeout(() => {
navigationRef.dispatch(
dispatch(
CommonActions.navigate({ name: 'WalletTransactions', params: { walletID: wallet.getID(), walletType: MultisigHDWallet.type } }),
);
}, 500);
@ -214,7 +198,7 @@ const ViewEditMultisigCosigners: React.FC = () => {
w.current.setNativeSegwit();
} else {
tempWallet.current.setSecret(w.current.getSecret());
data.current = new Array(tempWallet.current.getN());
setWalletData(new Array(tempWallet.current.getN()));
setWallet(tempWallet.current);
}
hasLoaded.current = true;
@ -318,8 +302,7 @@ const ViewEditMultisigCosigners: React.FC = () => {
leftText = `${secret[0]}...${secret[secret.length - 1]}`;
}
// @ts-ignore not sure which one is correct
const length = data?.length ?? data.current?.length ?? 0;
const length = walletData.length;
return (
<View>
@ -473,7 +456,34 @@ const ViewEditMultisigCosigners: React.FC = () => {
);
};
const handleUseMnemonicPhrase = async () => {
const _handleUseMnemonicPhrase = useCallback(
(mnemonic: string, passphrase?: string) => {
if (!wallet || !currentlyEditingCosignerNum) {
// failsafe
return;
}
const hd = new HDSegwitBech32Wallet();
hd.setSecret(mnemonic);
if (!hd.validateMnemonic()) return presentAlert({ message: loc.multisig.invalid_mnemonics });
try {
wallet.replaceCosignerXpubWithSeed(currentlyEditingCosignerNum, hd.getSecret(), passphrase);
} catch (e: any) {
console.log(e);
return presentAlert({ message: e.message });
}
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setWallet(wallet);
provideMnemonicsModalRef.current?.dismiss();
setIsSaveButtonDisabled(false);
setImportText('');
setAskPassphrase(false);
},
[wallet, currentlyEditingCosignerNum],
);
const handleUseMnemonicPhrase = useCallback(async () => {
let passphrase;
if (askPassphrase) {
try {
@ -487,29 +497,7 @@ const ViewEditMultisigCosigners: React.FC = () => {
}
}
return _handleUseMnemonicPhrase(importText, passphrase);
};
const _handleUseMnemonicPhrase = (mnemonic: string, passphrase?: string) => {
if (!wallet || !currentlyEditingCosignerNum) {
// failsafe
return;
}
const hd = new HDSegwitBech32Wallet();
hd.setSecret(mnemonic);
if (!hd.validateMnemonic()) return presentAlert({ message: loc.multisig.invalid_mnemonics });
try {
wallet.replaceCosignerXpubWithSeed(currentlyEditingCosignerNum, hd.getSecret(), passphrase);
} catch (e: any) {
console.log(e);
return presentAlert({ message: e.message });
}
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setWallet(wallet);
provideMnemonicsModalRef.current?.dismiss();
resetModalData();
};
}, [askPassphrase, importText, _handleUseMnemonicPhrase]);
const xpubInsteadOfSeed = (index: number): Promise<void> => {
return new Promise((resolve, reject) => {
@ -537,10 +525,9 @@ const ViewEditMultisigCosigners: React.FC = () => {
const scannedData = route.params.onBarScanned;
if (scannedData) {
setImportText(String(scannedData));
setParams({ onBarScanned: undefined });
provideMnemonicsModalRef.current?.present();
handleUseMnemonicPhrase();
}
}, [route.params.onBarScanned, setParams]);
}, [route.params.onBarScanned, setParams, handleUseMnemonicPhrase]);
const hideProvideMnemonicsModal = () => {
Keyboard.dismiss();
@ -583,10 +570,18 @@ const ViewEditMultisigCosigners: React.FC = () => {
) : (
<Button disabled={importText.trim().length === 0} title={loc.wallets.import_do_import} onPress={handleUseMnemonicPhrase} />
)}
<>
<BlueButtonLink ref={openScannerButtonRef} disabled={isLoading} onPress={scanOrOpenFile} title={loc.wallets.import_scan_qr} />
<BlueSpacing20 />
</>
{!isLoading && (
<>
<BlueButtonLink
ref={openScannerButtonRef}
disabled={isLoading}
onPress={scanOrOpenFile}
title={loc.wallets.import_scan_qr}
/>
<BlueSpacing20 />
</>
)}
</>
}
>
@ -594,7 +589,7 @@ const ViewEditMultisigCosigners: React.FC = () => {
<BlueTextCentered>{loc.multisig.type_your_mnemonics}</BlueTextCentered>
<BlueSpacing20 />
<View style={styles.multiLineTextInput}>
<BlueFormMultiInput value={importText} onChangeText={setImportText} />
<BlueFormMultiInput editable={!isLoading} value={importText} onChangeText={setImportText} />
</View>
</>
</BottomModal>
@ -666,7 +661,7 @@ const ViewEditMultisigCosigners: React.FC = () => {
<View style={[styles.root, stylesHook.root]} ref={discardChangesRef}>
<FlatList
ListHeaderComponent={tipKeys}
data={data.current}
data={walletData}
extraData={vaultKeyData}
renderItem={_renderKeyItem}
automaticallyAdjustKeyboardInsets

View File

@ -37,7 +37,7 @@ import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import loc, { formatBalanceWithoutSuffix } from '../../loc';
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
import { useStorage } from '../../hooks/context/useStorage';
import { useFocusEffect, useRoute, RouteProp } from '@react-navigation/native';
import { useFocusEffect, useRoute, RouteProp, usePreventRemove } from '@react-navigation/native';
import { LightningTransaction, Transaction, TWallet } from '../../class/wallets/types';
import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList';
import HeaderMenuButton from '../../components/HeaderMenuButton';
@ -66,7 +66,7 @@ const WalletDetails: React.FC = () => {
const [hideTransactionsInWalletsList, setHideTransactionsInWalletsList] = useState<boolean>(
wallet.getHideTransactionsInWalletsList ? !wallet.getHideTransactionsInWalletsList() : true,
);
const { setOptions, navigate, addListener } = useExtendedNavigation();
const { setOptions, navigate } = useExtendedNavigation();
const { colors } = useTheme();
const [walletName, setWalletName] = useState<string>(wallet.getLabel());
@ -281,12 +281,6 @@ const WalletDetails: React.FC = () => {
},
});
useEffect(() => {
setOptions({
headerBackTitleVisible: true,
});
}, [setOptions]);
useEffect(() => {
if (wallets.some(w => w.getID() === walletID)) {
setSelectedWalletID(walletID);
@ -408,13 +402,9 @@ const WalletDetails: React.FC = () => {
}
}, [wallet, walletName, saveToDisk]);
useEffect(() => {
const subscribe = addListener('beforeRemove', () => {
walletNameTextInputOnBlur();
});
return subscribe;
}, [addListener, walletName, walletNameTextInputOnBlur]);
usePreventRemove(false, () => {
walletNameTextInputOnBlur();
});
const onViewMasterFingerPrintPress = () => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
@ -650,7 +640,7 @@ const WalletDetails: React.FC = () => {
{wallet.allowXpub && wallet.allowXpub() && (
<>
<BlueSpacing20 />
<SecondButton onPress={navigateToXPub} testID="XPub" title={loc.wallets.details_show_xpub} />
<SecondButton onPress={navigateToXPub} testID="XpubButton" title={loc.wallets.details_show_xpub} />
</>
)}
{wallet.allowSignVerifyMessage && wallet.allowSignVerifyMessage() && (

View File

@ -53,7 +53,6 @@ const WalletsAddMultisig: React.FC = () => {
});
const onLetsStartPress = () => {
bottomModalRef.current?.dismiss();
navigate('WalletsAddMultisigStep2', { m, n, format, walletLabel });
};
@ -233,7 +232,7 @@ const styles = StyleSheet.create({
flex: 0.8,
},
modalContentShort: {
padding: 24,
padding: 20,
},
borderRadius6: {
borderRadius: 6,
@ -294,7 +293,7 @@ const styles = StyleSheet.create({
rowCenter: {
flexDirection: 'row',
justifyContent: 'center',
paddingVertical: 40,
paddingVertical: 30,
},
});

View File

@ -170,12 +170,14 @@ const WalletsList: React.FC = () => {
useEffect(() => {
// new wallet added
if (wallets.length > walletsCount.current) {
walletsCarousel.current?.scrollToItem({ item: wallets[walletsCount.current], viewPosition: 0.3 });
}
if (!isLargeScreen) {
if (wallets.length > walletsCount.current) {
walletsCarousel.current?.scrollToItem({ item: wallets[walletsCount.current], viewPosition: 0.3 });
}
walletsCount.current = wallets.length;
}, [wallets]);
walletsCount.current = wallets.length;
}
}, [isLargeScreen, wallets]);
const onBarScanned = useCallback(
(value: any) => {

View File

@ -4,6 +4,7 @@ import {
ActivityIndicator,
FlatList,
I18nManager,
InteractionManager,
Keyboard,
LayoutAnimation,
Platform,
@ -38,6 +39,7 @@ import {
DoneAndDismissKeyboardInputAccessory,
DoneAndDismissKeyboardInputAccessoryViewID,
} from '../../components/DoneAndDismissKeyboardInputAccessory';
import Clipboard from '@react-native-clipboard/clipboard';
import MultipleStepsListItem, {
MultipleStepsListItemButtonType,
MultipleStepsListItemDashType,
@ -46,10 +48,10 @@ import MultipleStepsListItem, {
const staticCache = {};
const WalletsAddMultisigStep2 = () => {
const { addWallet, saveToDisk, isElectrumDisabled, sleep, currentSharedCosigner, setSharedCosigner } = useStorage();
const { addAndSaveWallet, isElectrumDisabled, sleep, currentSharedCosigner, setSharedCosigner } = useStorage();
const { colors } = useTheme();
const { navigate, navigateToWalletsList, setParams, setOptions } = useExtendedNavigation();
const navigation = useExtendedNavigation();
const params = useRoute().params;
const { m, n, format, walletLabel } = params;
const [cosigners, setCosigners] = useState([]); // array of cosigners user provided. if format [cosigner, fp, path]
@ -92,19 +94,7 @@ const WalletsAddMultisigStep2 = () => {
}, [currentSharedCosigner]);
const handleOnHelpPress = async () => {
await dismissAllModals();
navigate('WalletsAddMultisigHelp');
};
const dismissAllModals = async () => {
try {
await mnemonicsModalRef.current?.dismiss();
await provideMnemonicsModalRef.current?.dismiss();
await renderCosignersXpubModalRef.current?.dismiss();
} catch (e) {
// in rare occasions trying to dismiss non visible modals can error out
console.debug('dismissAllModals error', e);
}
navigation.navigate('WalletsAddMultisigHelp');
};
const stylesHook = StyleSheet.create({
@ -139,13 +129,13 @@ const WalletsAddMultisigStep2 = () => {
const onCreate = async () => {
setIsLoading(true);
setOptions({ headerBackVisible: false });
navigation.setOptions({ headerBackVisible: false });
await sleep(100);
try {
await _onCreate(); // this can fail with "Duplicate fingerprint" error or other
} catch (e) {
setIsLoading(false);
setOptions({ headerBackVisible: true });
navigation.setOptions({ headerBackVisible: true });
presentAlert({ message: e.message });
console.log('create MS wallet error', e);
}
@ -181,30 +171,10 @@ const WalletsAddMultisigStep2 = () => {
await w.fetchBalance();
}
addWallet(w);
await saveToDisk();
addAndSaveWallet(w);
A(A.ENUM.CREATED_WALLET);
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
navigateToWalletsList();
};
const generateNewKey = () => {
const w = new HDSegwitBech32Wallet();
w.generate().then(() => {
const cosignersCopy = [...cosigners];
cosignersCopy.push([w.getSecret(), false, false]);
if (Platform.OS !== 'android') LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setCosigners(cosignersCopy);
setVaultKeyData({ keyIndex: cosignersCopy.length, seed: w.getSecret(), xpub: w.getXpub(), isLoading: false });
setIsLoading(true);
mnemonicsModalRef.current.present();
setTimeout(() => {
// filling cache
setXpubCacheForMnemonics(w.getSecret());
setFpCacheForMnemonics(w.getSecret());
setIsLoading(false);
}, 500);
});
navigation.getParent()?.goBack();
};
const getPath = useCallback(() => {
@ -227,6 +197,36 @@ const WalletsAddMultisigStep2 = () => {
return path;
}, [format]);
const setXpubCacheForMnemonics = useCallback(
(seed, passphrase) => {
const path = getPath();
const w = new MultisigHDWallet();
w.setDerivationPath(path);
staticCache[seed + path + passphrase] = w.convertXpubToMultisignatureXpub(MultisigHDWallet.seedToXpub(seed, path, passphrase));
return staticCache[seed + path + passphrase];
},
[getPath],
);
const generateNewKey = () => {
const w = new HDSegwitBech32Wallet();
w.generate().then(() => {
const cosignersCopy = [...cosigners];
cosignersCopy.push([w.getSecret(), false, false]);
if (Platform.OS !== 'android') LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setCosigners(cosignersCopy);
setVaultKeyData({ keyIndex: cosignersCopy.length, seed: w.getSecret(), xpub: w.getXpub(), isLoading: false });
setIsLoading(true);
mnemonicsModalRef.current.present();
setTimeout(() => {
// filling cache
setXpubCacheForMnemonics(w.getSecret());
setFpCacheForMnemonics(w.getSecret());
setIsLoading(false);
}, 500);
});
};
const viewKey = cosigner => {
if (MultisigHDWallet.isXpubValid(cosigner[0])) {
setCosignerXpub(MultisigCosigner.exportToJson(cosigner[1], cosigner[0], cosigner[2]));
@ -245,18 +245,13 @@ const WalletsAddMultisigStep2 = () => {
}
};
const getXpubCacheForMnemonics = (seed, passphrase) => {
const path = getPath();
return staticCache[seed + path + passphrase] || setXpubCacheForMnemonics(seed, passphrase);
};
const setXpubCacheForMnemonics = (seed, passphrase) => {
const path = getPath();
const w = new MultisigHDWallet();
w.setDerivationPath(path);
staticCache[seed + path + passphrase] = w.convertXpubToMultisignatureXpub(MultisigHDWallet.seedToXpub(seed, path, passphrase));
return staticCache[seed + path + passphrase];
};
const getXpubCacheForMnemonics = useCallback(
(seed, passphrase) => {
const path = getPath();
return staticCache[seed + path + passphrase] || setXpubCacheForMnemonics(seed, passphrase);
},
[getPath, setXpubCacheForMnemonics],
);
const getFpCacheForMnemonics = (seed, passphrase) => {
return staticCache[seed + (passphrase ?? '')] || setFpCacheForMnemonics(seed, passphrase);
@ -274,7 +269,6 @@ const WalletsAddMultisigStep2 = () => {
const tryUsingXpub = useCallback(
async (xpub, fp, path) => {
if (!MultisigHDWallet.isXpubForMultisig(xpub)) {
provideMnemonicsModalRef.current.dismiss();
setIsLoading(false);
setImportText('');
setAskPassphrase(false);
@ -308,7 +302,6 @@ const WalletsAddMultisigStep2 = () => {
}
}
provideMnemonicsModalRef.current.dismiss();
setIsLoading(false);
setImportText('');
setAskPassphrase(false);
@ -321,7 +314,134 @@ const WalletsAddMultisigStep2 = () => {
[cosigners, getPath],
);
const useMnemonicPhrase = async () => {
const isValidMnemonicSeed = mnemonicSeed => {
const hd = new HDSegwitBech32Wallet();
hd.setSecret(mnemonicSeed);
return hd.validateMnemonic();
};
const onBarScanned = useCallback(
async ret => {
if (!ret.data) ret = { data: ret };
try {
let retData = JSON.parse(ret.data);
if (Array.isArray(retData) && retData.length === 1) {
// UR:CRYPTO-ACCOUNT now parses as an array of accounts, even if it is just one,
// so in case of cosigner data its gona be an array of 1 cosigner account. lets pop it for
// the code that expects it
retData = retData.pop();
ret.data = JSON.stringify(retData);
}
} catch (e) {
console.debug('JSON parsing failed for ret.data:', e);
}
if (ret.data.toUpperCase().startsWith('UR')) {
presentAlert({ message: 'BC-UR not decoded. This should never happen' });
} else if (isValidMnemonicSeed(ret.data)) {
setImportText(ret.data);
setTimeout(async () => {
await provideMnemonicsModalRef.current.present();
}, 100);
} else {
if (MultisigHDWallet.isXpubValid(ret.data) && !MultisigHDWallet.isXpubForMultisig(ret.data)) {
return presentAlert({ message: loc.multisig.not_a_multisignature_xpub });
}
if (MultisigHDWallet.isXpubValid(ret.data)) {
return tryUsingXpub(ret.data);
}
let cosigner = new MultisigCosigner(ret.data);
if (!cosigner.isValid()) {
return presentAlert({ message: loc.multisig.invalid_cosigner });
}
if (cosigner.howManyCosignersWeHave() > 1) {
// lets look for the correct cosigner. thats probably gona be the one with specific corresponding path,
// for example m/48'/0'/0'/2' if user chose to setup native segwit in BW
for (const cc of cosigner.getAllCosigners()) {
switch (format) {
case MultisigHDWallet.FORMAT_P2WSH:
if (cc.getPath().startsWith('m/48') && cc.getPath().endsWith("/2'")) {
// found it
cosigner = cc;
}
break;
case MultisigHDWallet.FORMAT_P2SH_P2WSH:
case MultisigHDWallet.FORMAT_P2SH_P2WSH_ALT:
if (cc.getPath().startsWith('m/48') && cc.getPath().endsWith("/1'")) {
// found it
cosigner = cc;
}
break;
case MultisigHDWallet.FORMAT_P2SH:
if (cc.getPath().startsWith('m/45')) {
// found it
cosigner = cc;
}
break;
default:
console.error('Unexpected format:', format);
throw new Error('This should never happen');
}
}
}
for (const existingCosigner of cosigners) {
let existingXpub = existingCosigner[0];
if (!MultisigHDWallet.isXpubValid(existingXpub)) {
// derive the xpub from mnemonic-based cosigner
existingXpub = getXpubCacheForMnemonics(existingCosigner[0], existingCosigner[3]);
}
if (existingXpub === cosigner.getXpub()) {
return presentAlert({ message: loc.multisig.this_cosigner_is_already_imported });
}
}
// now, validating that cosigner is in correct format:
let correctFormat = false;
switch (format) {
case MultisigHDWallet.FORMAT_P2WSH:
if (cosigner.getPath().startsWith('m/48') && cosigner.getPath().endsWith("/2'")) {
correctFormat = true;
}
break;
case MultisigHDWallet.FORMAT_P2SH_P2WSH:
case MultisigHDWallet.FORMAT_P2SH_P2WSH_ALT:
if (cosigner.getPath().startsWith('m/48') && cosigner.getPath().endsWith("/1'")) {
correctFormat = true;
}
break;
case MultisigHDWallet.FORMAT_P2SH:
if (cosigner.getPath().startsWith('m/45')) {
correctFormat = true;
}
break;
default:
console.error('Unexpected format:', format);
throw new Error('This should never happen');
}
if (!correctFormat) {
return presentAlert({ message: loc.formatString(loc.multisig.invalid_cosigner_format, { format }) });
}
const cosignersCopy = [...cosigners];
cosignersCopy.push([cosigner.getXpub(), cosigner.getFp(), cosigner.getPath()]);
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setCosigners(cosignersCopy);
}
},
[cosigners, format, getXpubCacheForMnemonics, tryUsingXpub],
);
const scanOrOpenFile = async () => {
await provideMnemonicsModalRef.current.dismiss();
navigation.navigate('ScanQRCode', { showFileImportButton: true });
};
const utilizeMnemonicPhrase = useCallback(async () => {
try {
await provideMnemonicsModalRef.current.dismiss();
} catch {}
setIsLoading(true);
if (MultisigHDWallet.isXpubValid(importText)) {
@ -366,133 +486,21 @@ const WalletsAddMultisigStep2 = () => {
if (Platform.OS !== 'android') LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setCosigners(cosignersCopy);
provideMnemonicsModalRef.current.dismiss();
setIsLoading(false);
setImportText('');
setAskPassphrase(false);
};
const isValidMnemonicSeed = mnemonicSeed => {
const hd = new HDSegwitBech32Wallet();
hd.setSecret(mnemonicSeed);
return hd.validateMnemonic();
};
const onBarScanned = useCallback(
ret => {
if (!ret.data) ret = { data: ret };
try {
let retData = JSON.parse(ret.data);
if (Array.isArray(retData) && retData.length === 1) {
// UR:CRYPTO-ACCOUNT now parses as an array of accounts, even if it is just one,
// so in case of cosigner data its gona be an array of 1 cosigner account. lets pop it for
// the code that expects it
retData = retData.pop();
ret.data = JSON.stringify(retData);
}
} catch (_) {}
if (ret.data.toUpperCase().startsWith('UR')) {
presentAlert({ message: 'BC-UR not decoded. This should never happen' });
} else if (isValidMnemonicSeed(ret.data)) {
setImportText(ret.data);
setTimeout(() => {
provideMnemonicsModalRef.current.present().then(() => {});
}, 100);
} else {
if (MultisigHDWallet.isXpubValid(ret.data) && !MultisigHDWallet.isXpubForMultisig(ret.data)) {
return presentAlert({ message: loc.multisig.not_a_multisignature_xpub });
}
if (MultisigHDWallet.isXpubValid(ret.data)) {
return tryUsingXpub(ret.data);
}
let cosigner = new MultisigCosigner(ret.data);
if (!cosigner.isValid()) return presentAlert({ message: loc.multisig.invalid_cosigner });
provideMnemonicsModalRef.current.dismiss();
if (cosigner.howManyCosignersWeHave() > 1) {
// lets look for the correct cosigner. thats probably gona be the one with specific corresponding path,
// for example m/48'/0'/0'/2' if user chose to setup native segwit in BW
for (const cc of cosigner.getAllCosigners()) {
switch (format) {
case MultisigHDWallet.FORMAT_P2WSH:
if (cc.getPath().startsWith('m/48') && cc.getPath().endsWith("/2'")) {
// found it
cosigner = cc;
}
break;
case MultisigHDWallet.FORMAT_P2SH_P2WSH:
case MultisigHDWallet.FORMAT_P2SH_P2WSH_ALT:
if (cc.getPath().startsWith('m/48') && cc.getPath().endsWith("/1'")) {
// found it
cosigner = cc;
}
break;
case MultisigHDWallet.FORMAT_P2SH:
if (cc.getPath().startsWith('m/45')) {
// found it
cosigner = cc;
}
break;
default:
console.error('Unexpected format:', format);
throw new Error('This should never happen');
}
}
}
for (const existingCosigner of cosigners) {
if (existingCosigner[0] === cosigner.getXpub()) return presentAlert({ message: loc.multisig.this_cosigner_is_already_imported });
}
// now, validating that cosigner is in correct format:
let correctFormat = false;
switch (format) {
case MultisigHDWallet.FORMAT_P2WSH:
if (cosigner.getPath().startsWith('m/48') && cosigner.getPath().endsWith("/2'")) {
correctFormat = true;
}
break;
case MultisigHDWallet.FORMAT_P2SH_P2WSH:
case MultisigHDWallet.FORMAT_P2SH_P2WSH_ALT:
if (cosigner.getPath().startsWith('m/48') && cosigner.getPath().endsWith("/1'")) {
correctFormat = true;
}
break;
case MultisigHDWallet.FORMAT_P2SH:
if (cosigner.getPath().startsWith('m/45')) {
correctFormat = true;
}
break;
default:
console.error('Unexpected format:', format);
throw new Error('This should never happen');
}
if (!correctFormat) return presentAlert({ message: loc.formatString(loc.multisig.invalid_cosigner_format, { format }) });
const cosignersCopy = [...cosigners];
cosignersCopy.push([cosigner.getXpub(), cosigner.getFp(), cosigner.getPath()]);
if (Platform.OS !== 'android') LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setCosigners(cosignersCopy);
}
},
[cosigners, format, tryUsingXpub],
);
const scanOrOpenFile = async () => {
await provideMnemonicsModalRef.current.dismiss();
navigate('ScanQRCode');
};
}, [askPassphrase, cosigners, importText, tryUsingXpub]);
useEffect(() => {
const scannedData = params.onBarScanned;
if (scannedData) {
onBarScanned(scannedData);
setParams({ onBarScanned: undefined });
}
}, [onBarScanned, params.onBarScanned, setParams]);
InteractionManager.runAfterInteractions(() => {
const scannedData = params.onBarScanned;
if (scannedData) {
onBarScanned(scannedData);
navigation.setParams({ onBarScanned: undefined });
}
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [navigation, params.onBarScanned]);
const dashType = ({ index, lastIndex, isChecked, isFocus }) => {
if (isChecked) {
@ -588,6 +596,10 @@ const WalletsAddMultisigStep2 = () => {
return component;
};
const dismissMnemonicsModal = async () => {
await mnemonicsModalRef.current.dismiss();
};
const renderMnemonicsModal = () => {
return (
<BottomModal
@ -599,11 +611,7 @@ const WalletsAddMultisigStep2 = () => {
backgroundColor={colors.modal}
footer={
<View style={styles.modalFooterBottomPadding}>
{isLoading ? (
<ActivityIndicator />
) : (
<Button title={loc.send.success_done} onPress={() => mnemonicsModalRef.current.dismiss()} />
)}
{isLoading ? <ActivityIndicator /> : <Button title={loc.send.success_done} onPress={dismissMnemonicsModal} />}
</View>
}
>
@ -635,38 +643,40 @@ const WalletsAddMultisigStep2 = () => {
}, [askPassphrase]);
const renderProvideMnemonicsModal = () => {
const opacity = isVisible ? 0 : 1;
return (
<BottomModal
footer={
!isVisible && (
<View style={styles.modalFooterBottomPadding}>
{isLoading ? (
<ActivityIndicator />
) : (
<>
<Button
testID="DoImportKeyButton"
disabled={importText.trim().length === 0}
title={loc.wallets.import_do_import}
onPress={useMnemonicPhrase}
/>
<BlueButtonLink
testID="ScanOrOpenFile"
ref={openScannerButton}
disabled={isLoading}
onPress={scanOrOpenFile}
title={loc.wallets.import_scan_qr}
/>
</>
)}
</View>
)
<View style={[styles.modalFooterBottomPadding, { opacity }]} pointerEvents={isVisible ? 'none' : 'auto'}>
{isLoading ? (
<ActivityIndicator />
) : (
<>
<Button
testID="DoImportKeyButton"
disabled={importText.trim().length === 0}
title={loc.wallets.import_do_import}
onPress={utilizeMnemonicPhrase}
/>
<View style={styles.height16} />
<BlueButtonLink
testID="ScanOrOpenFile"
ref={openScannerButton}
disabled={isLoading}
onPress={scanOrOpenFile}
title={loc.wallets.import_scan_qr}
/>
</>
)}
</View>
}
keyboardMode="auto"
ref={provideMnemonicsModalRef}
backgroundColor={colors.modal}
contentContainerStyle={styles.provideMnemonicsModalStyle}
isGrabberVisible={false}
showCloseButton={true}
sizes={[Platform.OS === 'ios' ? 'auto' : '80%']}
sizes={[Platform.OS === 'ios' ? 'auto' : 420]}
onDismiss={() => {
Keyboard.dismiss();
setImportText('');
@ -696,8 +706,15 @@ const WalletsAddMultisigStep2 = () => {
inputAccessoryViewID={DoneAndDismissKeyboardInputAccessoryViewID}
/>
{Platform.select({
ios: <DoneAndDismissKeyboardInputAccessory />,
android: isVisible && <DoneAndDismissKeyboardInputAccessory />,
ios: (
<DoneAndDismissKeyboardInputAccessory
onClearTapped={() => setImportText('')}
onPasteTapped={async () => {
const paste = await Clipboard.getString();
setImportText(paste);
}}
/>
),
})}
<BlueSpacing20 />
@ -765,7 +782,7 @@ const WalletsAddMultisigStep2 = () => {
<View style={[styles.root, stylesHook.root]}>
{renderHelp()}
<View style={styles.wrapBox}>
<FlatList data={data.current} renderItem={_renderKeyItem} keyExtractor={(_item, index) => `${index}`} />
<FlatList data={data.current} renderItem={_renderKeyItem} keyExtractor={(_item, index) => `${index}`} extraData={cosigners} />
</View>
{renderMnemonicsModal()}
@ -773,6 +790,7 @@ const WalletsAddMultisigStep2 = () => {
{renderCosignersXpubModal()}
{footer}
<BlueSpacing20 />
</View>
);
};
@ -786,6 +804,9 @@ const styles = StyleSheet.create({
flex: 1,
marginVertical: 24,
},
height16: {
height: 16,
},
buttonBottom: {
marginHorizontal: 20,
flex: 0.12,
@ -853,6 +874,9 @@ const styles = StyleSheet.create({
fontWeight: 'bold',
marginLeft: 8,
},
provideMnemonicsModalStyle: {
minHeight: 420,
},
});
export default WalletsAddMultisigStep2;

View File

@ -22,10 +22,13 @@ const PleaseBackupLNDHub = () => {
const [qrCodeSize, setQRCodeSize] = useState(90);
const { isPrivacyBlurEnabled } = useSettings();
const handleBackButton = useCallback(() => {
navigation.getParent().pop();
return true;
const dismiss = useCallback(() => {
navigation.getParent().goBack();
}, [navigation]);
const handleBackButton = useCallback(() => {
dismiss();
return true;
}, [dismiss]);
const styles = StyleSheet.create({
root: {
backgroundColor: colors.elevated,
@ -49,8 +52,6 @@ const PleaseBackupLNDHub = () => {
};
}, [handleBackButton, isPrivacyBlurEnabled]);
const pop = () => navigation.getParent().pop();
const onLayout = e => {
const { height, width } = e.nativeEvent.layout;
setQRCodeSize(height > width ? width - 40 : e.nativeEvent.layout.width / 1.5);
@ -66,7 +67,7 @@ const PleaseBackupLNDHub = () => {
<QRCodeComponent value={wallet.getSecret()} size={qrCodeSize} />
<CopyTextToClipboard text={wallet.getSecret()} />
<BlueSpacing20 />
<Button onPress={pop} title={loc.pleasebackup.ok_lnd} />
<Button onPress={dismiss} title={loc.pleasebackup.ok_lnd} />
</ScrollView>
</SafeArea>
);

View File

@ -1,9 +1,29 @@
import assert from 'assert';
import * as bitcoin from 'bitcoinjs-lib';
import { expectToBeVisible, extractTextFromElementById, hashIt, helperCreateWallet, helperDeleteWallet, sleep, sup, yo } from './helperz';
import {
expectToBeVisible,
extractTextFromElementById,
hashIt,
helperCreateWallet,
helperDeleteWallet,
sleep,
sup,
tapAndTapAgainIfElementIsNotVisible,
tapIfPresent,
tapIfTextPresent,
yo,
} from './helperz';
import { element } from 'detox';
// if loglevel is set to `error`, this kind of logging will still get through
console.warn = console.log = (...args) => {
let output = '';
args.map(arg => (output += String(arg)));
process.stdout.write(output + '\n');
};
/**
* this testsuite is for test cases that require no wallets to be present
*/
@ -22,7 +42,7 @@ describe('BlueWallet UI Tests - no wallets', () => {
await element(by.id('SettingsButton')).tap();
await element(by.id('AboutButton')).tap();
await element(by.id('AboutScrollView')).swipe('up', 'fast', 1); // in case emu screen is small and it doesnt fit
await element(by.id('RunSelfTestButton')).tap();
await tapAndTapAgainIfElementIsNotVisible('RunSelfTestButton', 'SelfTestLoading');
await waitFor(element(by.id('SelfTestOk')))
.toBeVisible()
.withTimeout(300 * 1000);
@ -132,8 +152,7 @@ describe('BlueWallet UI Tests - no wallets', () => {
if (await expectToBeVisible('NotificationSettings')) {
await element(by.id('NotificationSettings')).tap();
await element(by.id('NotificationsSwitch')).tap();
await sup('OK');
await element(by.text('OK')).tap();
await sleep(3_000);
await element(by.id('NotificationsSwitch')).tap();
await device.pressBack();
await device.pressBack();
@ -183,8 +202,7 @@ describe('BlueWallet UI Tests - no wallets', () => {
await device.launchApp({ newInstance: true });
await yo('WalletsList');
await expect(element(by.id('cr34t3d'))).toBeVisible();
await element(by.id('cr34t3d')).tap();
await yo('ReceiveButton');
await tapAndTapAgainIfElementIsNotVisible('cr34t3d', 'ReceiveButton');
await element(by.id('ReceiveButton')).tap();
await element(by.text('Yes, I have.')).tap();
try {
@ -197,9 +215,9 @@ describe('BlueWallet UI Tests - no wallets', () => {
await yo('CopyTextToClipboard');
await element(by.id('SetCustomAmountButton')).tap();
await element(by.id('BitcoinAmountInput')).replaceText('1');
await element(by.id('CustomAmountDescription')).typeText('test');
await element(by.id('CustomAmountDescription')).replaceText('test');
await element(by.id('CustomAmountDescription')).tapReturnKey();
await element(by.id('CustomAmountSaveButton')).tap();
await tapAndTapAgainIfElementIsNotVisible('CustomAmountSaveButton', 'CustomAmountDescriptionText');
await expect(element(by.id('CustomAmountDescriptionText'))).toHaveText('test');
await expect(element(by.id('BitcoinAmountText'))).toHaveText('1 BTC');
@ -239,35 +257,33 @@ describe('BlueWallet UI Tests - no wallets', () => {
// lets encrypt the storage.
// first, trying to mistype second password:
await element(by.type('android.widget.CompoundButton')).tap(); // thats a switch lol. lets tap it
await element(by.id('EncyptedAndPasswordProtectedSwitch')).tap();
await element(by.id('IUnderstandButton')).tap();
if (process.env.TRAVIS) await sleep(3000); // hopefully helps prevent crash
await element(by.id('PasswordInput')).typeText('08902');
await element(by.id('PasswordInput')).replaceText('08902');
await element(by.id('PasswordInput')).tapReturnKey();
await element(by.id('ConfirmPasswordInput')).typeText('666');
await element(by.id('ConfirmPasswordInput')).replaceText('666');
await element(by.id('ConfirmPasswordInput')).tapReturnKey();
await element(by.id('OKButton')).tap();
if (process.env.TRAVIS) await sleep(3000); // hopefully helps prevent crash
// now, lets put correct passwords and encrypt the storage
await element(by.id('PasswordInput')).clearText();
await element(by.id('PasswordInput')).typeText('qqq');
await element(by.id('PasswordInput')).replaceText('qqq');
await element(by.id('PasswordInput')).tapReturnKey();
await element(by.id('ConfirmPasswordInput')).clearText();
await element(by.id('ConfirmPasswordInput')).typeText('qqq');
await element(by.id('ConfirmPasswordInput')).replaceText('qqq');
await element(by.id('ConfirmPasswordInput')).tapReturnKey();
await element(by.id('OKButton')).tap();
if (process.env.TRAVIS) await sleep(3000); // hopefully helps prevent crash
await tapIfPresent('OKButton'); // might not always work the first time
await sleep(3000); // propagate
// relaunch app
await device.launchApp({ newInstance: true });
await waitFor(element(by.text('OK')))
.toBeVisible()
.withTimeout(33000);
// trying to decrypt with incorrect password
await expect(element(by.text('Your storage is encrypted. Password is required to decrypt it.'))).toBeVisible();
await sup('Your storage is encrypted. Password is required to decrypt it.');
await element(by.type('android.widget.EditText')).typeText('wrong');
await element(by.text('OK')).tap();
await expect(element(by.text('Incorrect password. Please try again.'))).toBeVisible();
@ -295,12 +311,13 @@ describe('BlueWallet UI Tests - no wallets', () => {
if (process.env.TRAVIS) await sleep(3000); // hopefully helps prevent crash
// trying MAIN password: should fail, obviously
await element(by.id('PasswordInput')).typeText('qqq');
await element(by.id('PasswordInput')).replaceText('qqq');
await element(by.id('PasswordInput')).tapReturnKey();
await element(by.id('ConfirmPasswordInput')).typeText('qqq');
await element(by.id('ConfirmPasswordInput')).replaceText('qqq');
await element(by.id('ConfirmPasswordInput')).tapReturnKey();
await element(by.id('OKButton')).tap();
if (process.env.TRAVIS) await sleep(3000); // hopefully helps prevent crash
await tapIfPresent('OKButton'); // first time might not always work
await sleep(3000); // propagate
await expect(element(by.text('Password is currently in use. Please try a different password.'))).toBeVisible();
if (process.env.TRAVIS) await sleep(3000); // hopefully helps prevent crash
await element(by.text('OK')).tap();
@ -309,23 +326,24 @@ describe('BlueWallet UI Tests - no wallets', () => {
// trying new password, but will mistype
if (process.env.TRAVIS) await sleep(3000); // hopefully helps prevent crash
await element(by.id('PasswordInput')).clearText();
await element(by.id('PasswordInput')).typeText('passwordForFakeStorage');
await element(by.id('PasswordInput')).replaceText('passwordForFakeStorage');
await element(by.id('PasswordInput')).tapReturnKey();
await element(by.id('ConfirmPasswordInput')).clearText();
await element(by.id('ConfirmPasswordInput')).typeText('passwordForFakeStorageWithTypo'); // retyping with typo
await element(by.id('ConfirmPasswordInput')).replaceText('passwordForFakeStorageWithTypo'); // retyping with typo
await element(by.id('ConfirmPasswordInput')).tapReturnKey();
await element(by.id('OKButton')).tap();
if (process.env.TRAVIS) await sleep(3000); // hopefully helps prevent crash
// trying new password
await element(by.id('PasswordInput')).clearText();
await element(by.id('PasswordInput')).typeText('passwordForFakeStorage');
await element(by.id('PasswordInput')).replaceText('passwordForFakeStorage');
await element(by.id('PasswordInput')).tapReturnKey();
await element(by.id('ConfirmPasswordInput')).clearText();
await element(by.id('ConfirmPasswordInput')).typeText('passwordForFakeStorage'); // retyping
await element(by.id('ConfirmPasswordInput')).replaceText('passwordForFakeStorage'); // retyping
await element(by.id('ConfirmPasswordInput')).tapReturnKey();
await element(by.id('OKButton')).tap();
if (process.env.TRAVIS) await sleep(3000); // hopefully helps prevent crash
await tapIfPresent('OKButton'); // first time might not always work
await sleep(3_000); // propagate
// created fake storage.
// creating a wallet inside this fake storage
@ -335,11 +353,8 @@ describe('BlueWallet UI Tests - no wallets', () => {
// relaunch app
await device.launchApp({ newInstance: true });
await waitFor(element(by.text('OK')))
.toBeVisible()
.withTimeout(33000);
//
await expect(element(by.text('Your storage is encrypted. Password is required to decrypt it.'))).toBeVisible();
await sup('Your storage is encrypted. Password is required to decrypt it.');
await element(by.type('android.widget.EditText')).typeText('qqq');
await element(by.text('OK')).tap();
await yo('WalletsList');
@ -349,9 +364,8 @@ describe('BlueWallet UI Tests - no wallets', () => {
// relaunch app
await device.launchApp({ newInstance: true });
await sleep(3000);
//
await expect(element(by.text('Your storage is encrypted. Password is required to decrypt it.'))).toBeVisible();
await sup('Your storage is encrypted. Password is required to decrypt it.');
await element(by.type('android.widget.EditText')).typeText('passwordForFakeStorage');
await element(by.text('OK')).tap();
await yo('WalletsList');
@ -364,12 +378,13 @@ describe('BlueWallet UI Tests - no wallets', () => {
await element(by.id('SecurityButton')).tap();
// correct password
await element(by.type('android.widget.CompoundButton')).tap(); // thats a switch lol
await element(by.id('EncyptedAndPasswordProtectedSwitch')).tap();
await element(by.text('OK')).tap();
await element(by.id('PasswordInput')).typeText('passwordForFakeStorage');
await element(by.id('PasswordInput')).replaceText('passwordForFakeStorage');
await element(by.id('PasswordInput')).tapReturnKey();
await element(by.id('OKButton')).tap();
if (process.env.TRAVIS) await sleep(3000); // hopefully helps prevent crash
await tapIfPresent('OKButton'); // in case it didnt work first time
await sleep(3000); // propagate
await helperDeleteWallet('fake_wallet');
@ -393,38 +408,37 @@ describe('BlueWallet UI Tests - no wallets', () => {
// lets encrypt the storage.
// lets put correct passwords and encrypt the storage
await element(by.type('android.widget.CompoundButton')).tap(); // thats a switch lol
await element(by.id('EncyptedAndPasswordProtectedSwitch')).tap();
if (process.env.TRAVIS) await sleep(3000); // hopefully helps prevent crash
await element(by.id('IUnderstandButton')).tap();
if (process.env.TRAVIS) await sleep(3000); // hopefully helps prevent crash
await element(by.id('PasswordInput')).typeText('pass');
await element(by.id('PasswordInput')).replaceText('pass');
await element(by.id('PasswordInput')).tapReturnKey();
await element(by.id('ConfirmPasswordInput')).typeText('pass');
await element(by.id('ConfirmPasswordInput')).replaceText('pass');
await element(by.id('ConfirmPasswordInput')).tapReturnKey();
await element(by.id('OKButton')).tap();
if (process.env.TRAVIS) await sleep(3000); // hopefully helps prevent crash
await tapIfPresent('OKButton'); // might not always work first time
await sleep(3000); // propagate
await element(by.id('PlausibleDeniabilityButton')).tap();
// trying to enable plausible denability
await element(by.id('CreateFakeStorageButton')).tap();
if (process.env.TRAVIS) await sleep(3000); // hopefully helps prevent crash
await element(by.id('PasswordInput')).typeText('fake');
await element(by.id('PasswordInput')).replaceText('fake');
await element(by.id('PasswordInput')).tapReturnKey();
await element(by.id('ConfirmPasswordInput')).typeText('fake'); // retyping
await element(by.id('ConfirmPasswordInput')).replaceText('fake'); // retyping
await element(by.id('ConfirmPasswordInput')).tapReturnKey();
await element(by.id('OKButton')).tap();
if (process.env.TRAVIS) await sleep(3000); // hopefully helps prevent crash
await tapIfPresent('OKButton'); // might not always work first time
await sleep(3000); // propagate
// created fake storage.
// creating a wallet inside this fake storage
await helperCreateWallet('fake_wallet');
// relaunch app
await device.launchApp({ newInstance: true });
await waitFor(element(by.text('OK')))
.toBeVisible()
.withTimeout(33000);
//
await expect(element(by.text('Your storage is encrypted. Password is required to decrypt it.'))).toBeVisible();
await sup('Your storage is encrypted. Password is required to decrypt it.');
await element(by.type('android.widget.EditText')).typeText('pass');
await element(by.text('OK')).tap();
await yo('WalletsList');
@ -437,18 +451,20 @@ describe('BlueWallet UI Tests - no wallets', () => {
await element(by.id('SecurityButton')).tap();
// putting FAKE storage password. should not succeed
await element(by.type('android.widget.CompoundButton')).tap(); // thats a switch lol
await element(by.id('EncyptedAndPasswordProtectedSwitch')).tap();
await element(by.text('OK')).tap();
await element(by.id('PasswordInput')).typeText('fake');
await element(by.id('PasswordInput')).replaceText('fake');
await element(by.id('PasswordInput')).tapReturnKey();
await element(by.id('OKButton')).tap();
if (process.env.TRAVIS) await sleep(3000); // hopefully helps prevent crash
await tapIfPresent('OKButton'); // might not always work first time
await sleep(3000); // propagate
// correct password
await element(by.id('PasswordInput')).clearText();
await element(by.id('PasswordInput')).typeText('pass');
await element(by.id('PasswordInput')).replaceText('pass');
await element(by.id('PasswordInput')).tapReturnKey();
await element(by.id('OKButton')).tap();
if (process.env.TRAVIS) await sleep(3000); // hopefully helps prevent crash
await tapIfPresent('OKButton'); // might not always work first time
await sleep(3000); // propagate
// relaunch app
await device.launchApp({ newInstance: true });
@ -467,8 +483,7 @@ describe('BlueWallet UI Tests - no wallets', () => {
await element(by.id('WalletsList')).swipe('left', 'fast', 1); // in case emu screen is small and it doesnt fit
await sleep(200); // Wait until bounce animation finishes.
// going to Import Wallet screen and importing Vault
await element(by.id('CreateAWallet')).tap();
await yo('ActivateVaultButton');
await tapAndTapAgainIfElementIsNotVisible('CreateAWallet', 'ActivateVaultButton');
await element(by.id('ActivateVaultButton')).tap();
await element(by.id('Create')).tap();
// vault settings:
@ -486,6 +501,7 @@ describe('BlueWallet UI Tests - no wallets', () => {
await element(by.id('ScanOrOpenFile')).tap();
await sleep(5000); // wait for camera screen to initialize
await yo('ScanQrBackdoorButton');
for (let c = 0; c <= 5; c++) {
await element(by.id('ScanQrBackdoorButton')).tap();
}
@ -511,11 +527,12 @@ describe('BlueWallet UI Tests - no wallets', () => {
// when xpub - it automatically closes the modal, so no need to tap the button
await element(by.id('CreateButton')).tap();
await sup('OK');
await tapIfTextPresent('OK');
await yo('Multisig Vault');
await element(by.id('Multisig Vault')).tap(); // go inside the wallet
await yo('ReceiveButton');
await element(by.id('ReceiveButton')).tap();
await element(by.text('Yes, I have.')).tap();
try {
// in case emulator has no google services and doesnt support pushes
// we just dont show this popup
@ -545,7 +562,7 @@ describe('BlueWallet UI Tests - no wallets', () => {
await element(by.id('WalletsList')).swipe('left', 'fast', 1); // in case emu screen is small and it doesnt fit
await sleep(200); // Wait until bounce animation finishes.
// going to Import Wallet screen and importing mnemonic
await element(by.id('CreateAWallet')).tap();
await tapAndTapAgainIfElementIsNotVisible('CreateAWallet', 'ImportWallet');
await element(by.id('ImportWallet')).tap();
await element(by.id('ScanImport')).tap();
@ -590,9 +607,9 @@ describe('BlueWallet UI Tests - no wallets', () => {
await element(by.id('chooseFee')).tap();
await element(by.id('feeCustom')).tap();
await element(by.type('android.widget.EditText')).typeText(feeRate + '\n');
await sleep(1_000); // propagate
await element(by.text('OK')).tap();
if (process.env.TRAVIS) await sleep(5000);
try {
await element(by.id('CreateTransactionButton')).tap();
} catch (_) {}
@ -602,7 +619,7 @@ describe('BlueWallet UI Tests - no wallets', () => {
await element(by.id('ProvideSignature')).tap();
await element(by.id('PsbtMultisigQRCodeScrollView')).swipe('up', 'fast', 1); // in case emu screen is small and it doesnt fit
await element(by.id('CosignedScanOrImportFile')).tap();
await tapAndTapAgainIfElementIsNotVisible('CosignedScanOrImportFile', 'ScanQrBackdoorButton');
const ursSignedByPassport = [
'UR:CRYPTO-PSBT/22-4/LPCMAACFAXPLCYZTVYVOPKHDWPHKAXPYJOIHIDHNJSATRTSWEYGUHDURWYDECAGLAAHTTBHTFZFPWDRTLROXLUEHCXAHJTIHTEHDHKTEVTOTIOWFSKGEOSCFFLDRGLFTCYKELSRDNSHYGLLEVYIDGYZOEEDAAOENHGASFDHFVWNSATVYCFETATZSFROXFPMHGUJNWDSPNYMHHGPAIMGYURAYCXLEZEZSCLKBJZLFSRAOOYMSYNCEHDOSPYGTTDSODRSKLALBCAVYBNOLOEGSOYVOVLMWFDPFHGBAVDAEAEAEADADWMDTGDPTADAEADADSTENFYASFDTBCLDINBAOHFHYTPPKWYMSSNDKHKKNUOIELPDRKTOYHPCFCSWNFXPKFZNEPKVOIOCNAOAXMNPSKPLTGYFLRHLOHGUYKISWBWVEGUGMLAAYDLLDLSAAVDTDSADLIDFXYLKKFYURMTOXLKMDRSTYTERSJNHSBDPSGOGWJKJESTWLZCTKGE',
@ -626,7 +643,7 @@ describe('BlueWallet UI Tests - no wallets', () => {
await element(by.id('ProvideSignature')).tap();
await element(by.id('PsbtMultisigQRCodeScrollView')).swipe('up', 'fast', 1); // in case emu screen is small and it doesnt fit
await element(by.id('CosignedScanOrImportFile')).tap();
await tapAndTapAgainIfElementIsNotVisible('CosignedScanOrImportFile', 'ScanQrBackdoorButton');
const urSignedByPassportAndKeystone = [
'UR:CRYPTO-PSBT/105-2/LPCSINAOCFAXOLCYSBLUFDHSHKADTEHKAXOTJOJKIDJYZMADAEKIAOAEAEAEADSGMHIEQDIAFLKPVABAJEHLLNVLRKKPCPDAHYNSOTTSOYBTIMMUCYAASSMDAMDAMKAEAEAEAEAEZMZMZMZMAOGDSRAEAEAEAEAEAECMAEBBKBOTLPWFGMRNINIMPFYNWLGEBAVTVLSWTYPAGEGUWFVLAEAEAEAEAEAECPAECXHEJTWTTTWEPDFMMDSNYKDPRYZMRLBARSPAAYLETDCLGDJKIHBNTYFXCHNNWTRHFEAEAEAEAEAEADAEWDAOAEAEAEAEADADSTENIYASISYLVDVLGWCXRPBWVYVSDALOTLCESKTTFEJTWDTBPTECMOMNLNDABSDTADAEAEAEAEADAEAELAAOGDPTADAEAEAEAEAECPAECXCLSWSSWSCPVTGTESFWSBCTCSETNSPYNLBKJLZTUEMWSOMSNNTYGSLSFPNEPKVOIOONMOAOAEAEAEAEAECMAEBBMNAMRTRKDYGUHDURWSVOLGDRRLESMEDLOLGWLYNTAOFLDYFYAOCXDYYTJOMYYAUELEDYKIYLADUROYFNURDRGLFTCYKEKEFEIAOYGSTNCPIDGYZOEEDAAOCXHGCAENYKHNJLGOHEJOGMRLBNTDWYGWJOPFPYFMKKTISROXGMIMGYURAYCXLEUOZSADCLAOJPBGWSASPTIATTPMLECMPRIHSTMDJYLOYKTKRTHHTLSTFZKPOYWKBKROASBGBAVDAEAEAEAEADADDNGDPTADAEAEAEAEAECPAECXCLSWSSWSCPVTGTESFWSBCTCSETNSPYNLBKJLZTUEMWSOMSNNTYGSLSFPNEPKVOIOCPAOAXFTRPWPCPGYBKEHRTTTFDCTNTRHKGFGCXSAHSRHWDMDTONBRLROWMSFCPBTHNTIAAFLDYFYAOCXISRERKHDRDGAPMATEHJLFL',
@ -679,9 +696,9 @@ describe('BlueWallet UI Tests - no wallets', () => {
await yo('WalletsList');
await element(by.id('WalletsList')).swipe('left', 'fast', 1); // in case emu screen is small and it doesnt fit
await sleep(200); // Wait until bounce animation finishes.
await sleep(500); // Wait until bounce animation finishes.
// going to Import Wallet screen and importing mnemonic
await element(by.id('CreateAWallet')).tap();
await tapAndTapAgainIfElementIsNotVisible('CreateAWallet', 'ScrollView');
await element(by.id('ScrollView')).swipe('up', 'fast', 0.9); // in case emu screen is small and it doesnt fit
await element(by.id('ImportWallet')).tap();
await element(by.id('MnemonicInput')).replaceText(

View File

@ -1,7 +1,18 @@
import assert from 'assert';
import * as bitcoin from 'bitcoinjs-lib';
import { extractTextFromElementById, getSwitchValue, hashIt, helperImportWallet, sleep, sup, yo } from './helperz';
import {
extractTextFromElementById,
getSwitchValue,
hashIt,
helperImportWallet,
sleep,
sup,
tapAndTapAgainIfElementIsNotVisible,
tapAndTapAgainIfTextIsNotVisible,
tapIfTextPresent,
yo,
} from './helperz';
/**
* in this suite each test requires that there is one specific wallet present, thus, we import it
@ -434,9 +445,8 @@ describe('BlueWallet UI Tests - import BIP84 wallet', () => {
await element(by.text('Imported HD SegWit (BIP84 Bech32 Native)')).tap();
await element(by.id('WalletDetails')).tap();
await element(by.id('WalletDetailsScroll')).swipe('up', 'fast', 1); // in case emu screen is small and it doesnt fit
await element(by.text('Contacts')).tap();
await tapAndTapAgainIfTextIsNotVisible('Contacts', 'Add Contact');
await expect(element(by.text('Add Contact'))).toBeVisible();
await expect(element(by.id('ContactListItem0'))).not.toBeVisible();
await element(by.text('Add Contact')).tap();
await element(by.type('android.widget.EditText')).replaceText('13HaCAB4jf7FYSZexJxoczyDDnutzZigjS');
@ -475,10 +485,10 @@ describe('BlueWallet UI Tests - import BIP84 wallet', () => {
await element(by.text('Imported HD SegWit (BIP84 Bech32 Native)')).tap();
await yo('SendButton');
await element(by.id('SendButton')).tap();
await tapAndTapAgainIfElementIsNotVisible('SendButton', 'HeaderMenuButton');
await element(by.id('HeaderMenuButton')).tap();
await element(by.text('Insert Contact')).tap();
await element(by.id('ContactListItem0')).tap();
await tapAndTapAgainIfElementIsNotVisible('ContactListItem0', 'BitcoinAmountInput');
await element(by.id('BitcoinAmountInput')).typeText('0.0001\n');
await element(by.id('HeaderMenuButton')).tap();
@ -529,6 +539,7 @@ describe('BlueWallet UI Tests - import BIP84 wallet', () => {
// rename test
await element(by.id('WalletNameInput')).replaceText('testname');
await element(by.id('WalletNameInput')).typeText('\n'); // newline is what triggers saving the wallet
await device.pressBack();
await sup('testname');
await expect(element(by.id('WalletLabel'))).toHaveText('testname');
@ -536,6 +547,7 @@ describe('BlueWallet UI Tests - import BIP84 wallet', () => {
// rename back
await element(by.id('WalletNameInput')).replaceText('Imported HD SegWit (BIP84 Bech32 Native)');
await element(by.id('WalletNameInput')).typeText('\n'); // newline is what triggers saving the wallet
await device.pressBack();
await sup('Imported HD SegWit (BIP84 Bech32 Native)');
await expect(element(by.id('WalletLabel'))).toHaveText('Imported HD SegWit (BIP84 Bech32 Native)');
@ -543,15 +555,14 @@ describe('BlueWallet UI Tests - import BIP84 wallet', () => {
// wallet export
await element(by.id('WalletDetailsScroll')).swipe('up', 'fast', 1);
await element(by.id('WalletExport')).tap();
await tapAndTapAgainIfElementIsNotVisible('WalletExport', 'WalletExportScroll');
await element(by.id('WalletExportScroll')).swipe('up', 'fast', 1);
await expect(element(by.id('Secret'))).toHaveText(process.env.HD_MNEMONIC_BIP84);
await device.pressBack();
// XPUB
await element(by.id('WalletDetailsScroll')).swipe('up', 'fast', 1);
await element(by.id('XPub')).tap();
await expect(element(by.id('CopyTextToClipboard'))).toBeVisible();
await tapAndTapAgainIfElementIsNotVisible('XpubButton', 'CopyTextToClipboard');
await device.pressBack();
process.env.TRAVIS && require('fs').writeFileSync(lockFile, '1');
@ -616,7 +627,7 @@ describe('BlueWallet UI Tests - import BIP84 wallet', () => {
await element(by.text('0.00069909')).atIndex(0).tap();
await element(by.text('Details')).tap();
await expect(element(by.text('8b0ab2c7196312e021e0d3dc73f801693826428782970763df6134457bd2ec20'))).toBeVisible();
await element(by.type('android.widget.EditText')).typeText('test1');
await element(by.type('android.widget.EditText')).replaceText('test1');
await element(by.type('android.widget.EditText')).tapReturnKey();
// Terminate and reopen the app to confirm the note is persisted
@ -650,9 +661,11 @@ describe('BlueWallet UI Tests - import BIP84 wallet', () => {
// setting fee rate:
await element(by.id('chooseFee')).tap();
await element(by.id('feeCustom')).tap();
await element(by.type('android.widget.EditText')).typeText('1\n');
await element(by.type('android.widget.EditText')).replaceText('1');
await element(by.type('android.widget.EditText')).tapReturnKey();
await element(by.text('OK')).tap();
if (process.env.TRAVIS) await sleep(5000);
await tapIfTextPresent('OK'); // in case it didnt work first time
await sleep(3000);
await element(by.id('CreateTransactionButton')).tap();
await element(by.id('TransactionDetailsButton')).tap();

View File

@ -1,15 +1,45 @@
import createHash from 'create-hash';
import { element } from 'detox';
export function yo(id, timeout = 33000) {
return waitFor(element(by.id(id)))
.toBeVisible()
.withTimeout(timeout);
export async function yo(id, timeout = 33000) {
try {
await waitFor(element(by.id(id)))
.toBeVisible()
.withTimeout(timeout / 2);
} catch (_) {
// nop
}
try {
await waitFor(element(by.id(id)))
.toBeVisible()
.withTimeout(timeout / 2);
return true;
} catch (_) {
const msg = `Assertion failed: testID ${id} is not visible`;
throw new Error(msg);
}
}
export function sup(text, timeout = 33000) {
return waitFor(element(by.text(text)))
.toBeVisible()
.withTimeout(timeout);
export async function sup(text, timeout = 33000) {
try {
await waitFor(element(by.text(text)))
.toBeVisible()
.withTimeout(timeout / 2);
return true;
} catch (_) {
// nop
}
try {
await waitFor(element(by.text(text)))
.toBeVisible()
.withTimeout(timeout / 2);
return true;
} catch (_) {
const msg = `Assertion failed: text "${text}" is not visible`;
throw new Error(msg);
}
}
export async function getSwitchValue(switchId) {
@ -25,9 +55,9 @@ export async function helperImportWallet(importText, walletType, expectedWalletL
await yo('WalletsList');
await element(by.id('WalletsList')).swipe('left', 'fast', 1); // in case emu screen is small and it doesnt fit
await sleep(200); // Wait until bounce animation finishes.
await sleep(500); // Wait until bounce animation finishes.
// going to Import Wallet screen and importing mnemonic
await element(by.id('CreateAWallet')).tap();
await tapAndTapAgainIfElementIsNotVisible('CreateAWallet', 'ImportWallet');
await element(by.id('ImportWallet')).tap();
// tapping 5 times invisible button is a backdoor:
for (let c = 0; c < 5; c++) {
@ -126,15 +156,14 @@ export const expectToBeVisible = async id => {
export async function helperCreateWallet(walletName) {
await element(by.id('WalletsList')).swipe('left', 'fast', 1); // in case emu screen is small and it doesnt fit
await sleep(200); // Wait until bounce animation finishes.
await element(by.id('CreateAWallet')).tap();
await tapAndTapAgainIfElementIsNotVisible('CreateAWallet', 'WalletNameInput');
await element(by.id('WalletNameInput')).replaceText(walletName || 'cr34t3d');
await yo('ActivateBitcoinButton');
await element(by.id('ActivateBitcoinButton')).tap();
await element(by.id('ActivateBitcoinButton')).tap();
// why tf we need 2 taps for it to work..? mystery
await element(by.id('Create')).tap();
await tapAndTapAgainIfElementIsNotVisible('Create', 'PleaseBackupScrollView');
await yo('PleaseBackupScrollView');
await element(by.id('PleaseBackupScrollView')).swipe('up', 'fast', 1); // in case emu screen is small and it doesnt fit
await yo('PleasebackupOk');
@ -143,3 +172,59 @@ export async function helperCreateWallet(walletName) {
await element(by.id('WalletsList')).swipe('right', 'fast', 1); // in case emu screen is small and it doesnt fit
await expect(element(by.id(walletName || 'cr34t3d'))).toBeVisible();
}
export async function tapAndTapAgainIfElementIsNotVisible(idToTap, idToCheckVisible) {
// tap
await element(by.id(idToTap)).tap();
// check if visible
try {
await waitFor(element(by.id(idToCheckVisible)))
.toBeVisible()
.withTimeout(3_000);
return; // did not throw? its visible, return
} catch (_) {}
// did not return so its not visible, lets tap again
await element(by.id(idToTap)).tap();
// check visibility again, this time no try-catch, if it fails it fails
await waitFor(element(by.id(idToCheckVisible)))
.toBeVisible()
.withTimeout(3_000);
}
export async function tapAndTapAgainIfTextIsNotVisible(textToTap, textToCheckVisible) {
// tap
await element(by.text(textToTap)).tap();
// check if visible
try {
await waitFor(element(by.text(textToCheckVisible)))
.toBeVisible()
.withTimeout(3_000);
return; // did not throw? its visible, return
} catch (_) {}
// did not return so its not visible, lets tap again
await element(by.text(textToTap)).tap();
// check visibility again, this time no try-catch, if it fails it fails
await waitFor(element(by.text(textToCheckVisible)))
.toBeVisible()
.withTimeout(3_000);
}
export async function tapIfPresent(id) {
try {
await element(by.id(id)).tap();
} catch (_) {}
// no need to check for visibility, just silently ignore exception if such testID is not present
}
export async function tapIfTextPresent(text) {
try {
await element(by.text(text)).tap();
} catch (_) {}
// no need to check for visibility, just silently ignore exception if such testID is not present
}

View File

@ -1,10 +1,10 @@
import assert from 'assert';
import React from 'react';
import TestRenderer from 'react-test-renderer';
import { render } from '@testing-library/react-native';
import { NavigationContainer } from '@react-navigation/native';
import { Header } from '../../components/Header';
import SelfTest from '../../screen/settings/SelfTest';
import Settings from '../../screen/settings/Settings';
import { BlueDefaultTheme } from '../../components/themes';
jest.mock('../../blue_modules/BlueElectrum', () => {
return {
@ -12,37 +12,33 @@ jest.mock('../../blue_modules/BlueElectrum', () => {
};
});
const Wrapper = ({ children }) => <NavigationContainer theme={BlueDefaultTheme}>{children}</NavigationContainer>;
it('Header works', () => {
const rendered = TestRenderer.create(<Header />).toJSON();
expect(rendered).toBeTruthy();
const { toJSON } = render(
<Wrapper>
<Header />
</Wrapper>,
);
expect(toJSON()).toBeTruthy();
});
// eslint-disable-next-line jest/no-disabled-tests
it.skip('Settings work', () => {
const rendered = TestRenderer.create(<Settings />).toJSON();
expect(rendered).toBeTruthy();
const { toJSON } = render(
<Wrapper>
<Settings />
</Wrapper>,
);
expect(toJSON()).toBeTruthy();
});
it('SelfTest work', () => {
const component = TestRenderer.create(<SelfTest />);
const root = component.root;
const rendered = component.toJSON();
expect(rendered).toBeTruthy();
// console.log((root.findAllByType('Text')[0].props));
let okFound = false;
const allTests = [];
for (const v of root.findAllByType('Text')) {
let text = v.props.children;
if (text.join) {
text = text.join('');
}
if (text === 'OK') {
okFound = true;
}
allTests.push(text);
// console.log(text);
}
assert.ok(okFound, 'OK not found. Got: ' + allTests.join('; '));
const { toJSON, getByText } = render(
<Wrapper>
<SelfTest />
</Wrapper>,
);
expect(toJSON()).toBeTruthy();
expect(getByText('OK')).toBeTruthy();
});

27
utils/combinePSBTs.ts Normal file
View File

@ -0,0 +1,27 @@
import * as bitcoin from 'bitcoinjs-lib';
/**
* Combines two PSBTs and returns the combined PSBT.
* @param {string} psbtBase64 - The base64 string of the first PSBT.
* @param {string} newPSBTBase64 - The base64 string of the new PSBT to combine.
* @returns {bitcoin.Psbt} - The combined PSBT.
*/
interface CombinePSBTsParams {
psbtBase64: string;
newPSBTBase64: string;
}
export const combinePSBTs = ({ psbtBase64, newPSBTBase64 }: CombinePSBTsParams): bitcoin.Psbt => {
if (psbtBase64 === newPSBTBase64) {
return bitcoin.Psbt.fromBase64(psbtBase64);
}
try {
const psbt = bitcoin.Psbt.fromBase64(psbtBase64);
const newPsbt = bitcoin.Psbt.fromBase64(newPSBTBase64);
psbt.combine(newPsbt);
return psbt;
} catch (err) {
console.error('Error combining PSBTs:', err);
throw err;
}
};