Merge branch 'master' into marcosrdz-patch-3

This commit is contained in:
Marcos Rodriguez Vélez 2025-01-07 15:15:18 -04:00 committed by GitHub
commit e30c0b17ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 595 additions and 411 deletions

View File

@ -125,6 +125,13 @@
"symbol": "£",
"country": "United Kingdom (British Pound)"
},
"HKD": {
"endPointKey": "HKD",
"locale": "zh-HK",
"source": "CoinGecko",
"symbol": "HK$",
"country": "Hong Kong (Hong Kong Dollar)"
},
"HRK": {
"endPointKey": "HRK",
"locale": "hr-HR",

View File

@ -58,13 +58,20 @@
style="@style/WidgetTextPrimary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:autoSizeMaxTextSize="24sp"
android:autoSizeMinTextSize="12sp"
android:autoSizeStepGranularity="2sp"
android:autoSizeTextType="uniform"
android:duplicateParentState="false"
android:editable="false"
android:lines="1"
android:text="Loading..."
android:textSize="24sp"
android:textStyle="bold"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:visibility="gone"
android:layout_gravity="end"/>
android:visibility="gone" />
<LinearLayout
android:id="@+id/price_arrow_container"
android:layout_width="wrap_content"

View File

@ -132,13 +132,19 @@ export const showImagePickerAndReadImage = async (): Promise<string | undefined>
return undefined;
} else if (response.errorCode) {
throw new Error(response.errorMessage);
} else if (response.assets?.[0]?.uri) {
} else if (response.assets) {
try {
const result = await RNQRGenerator.detect({ uri: decodeURI(response.assets[0].uri.toString()) });
return result?.values[0];
const uri = response.assets[0].uri;
if (uri) {
const result = await RNQRGenerator.detect({ uri: decodeURI(uri.toString()) });
if (result?.values.length > 0) {
return result?.values[0];
}
}
throw new Error(loc.send.qr_error_no_qrcode);
} catch (error) {
console.error(error);
throw new Error(loc.send.qr_error_no_qrcode);
presentAlert({ message: loc.send.qr_error_no_qrcode });
}
}
@ -187,9 +193,11 @@ export const showFilePickerAndReadFile = async function (): Promise<{ data: stri
if (result) {
return { data: result.values[0], uri: fileCopyUri };
}
presentAlert({ message: loc.send.qr_error_no_qrcode });
return { data: false, uri: false };
} catch (error) {
console.error(error);
presentAlert({ message: loc.send.qr_error_no_qrcode });
return { data: false, uri: false };
}
}

View File

@ -14,6 +14,8 @@ export const NOTIFICATIONS_NO_AND_DONT_ASK_FLAG = 'NOTIFICATIONS_NO_AND_DONT_ASK
let alreadyConfigured = false;
let baseURI = groundControlUri;
const deepClone = obj => JSON.parse(JSON.stringify(obj));
const checkAndroidNotificationPermission = async () => {
try {
const { status } = await checkNotifications();
@ -323,8 +325,8 @@ export const configureNotifications = async onProcessNotifications => {
};
const handleNotification = async notification => {
// Deep clone to avoid modifying the original object
const payload = structuredClone({
// Deep clone to avoid modifying the original object
const payload = deepClone({
...notification,
...notification.data,
});

218
components/CameraScreen.tsx Normal file
View File

@ -0,0 +1,218 @@
import React, { useState, useRef } from 'react';
import { Animated, ImageURISource, SafeAreaView, StatusBar, StyleSheet, TouchableOpacity, View } from 'react-native';
import { Camera, CameraApi, CameraType, Orientation } from 'react-native-camera-kit';
import loc from '../loc';
import { Icon } from '@rneui/base';
interface CameraScreenProps {
onCancelButtonPress: () => void;
showImagePickerButton?: boolean;
showFilePickerButton?: boolean;
onImagePickerButtonPress?: () => void;
onFilePickerButtonPress?: () => void;
torchOnImage?: ImageURISource;
torchOffImage?: ImageURISource;
onReadCode?: (event: any) => void;
cameraFlipImage?: ImageURISource;
}
const CameraScreen: React.FC<CameraScreenProps> = ({
onCancelButtonPress,
showImagePickerButton,
showFilePickerButton,
onImagePickerButtonPress,
onFilePickerButtonPress,
torchOnImage,
torchOffImage,
onReadCode,
cameraFlipImage,
}) => {
const cameraRef = useRef<CameraApi>(null);
const [torchMode, setTorchMode] = useState(false);
const [cameraType, setCameraType] = useState(CameraType.Back);
const [zoom, setZoom] = useState<number | undefined>();
const [orientationAnim] = useState(new Animated.Value(3));
const onSwitchCameraPressed = () => {
const direction = cameraType === CameraType.Back ? CameraType.Front : CameraType.Back;
setCameraType(direction);
setZoom(1); // When changing camera type, reset to default zoom for that camera
};
const onSetTorch = () => {
setTorchMode(!torchMode);
};
// Counter-rotate the icons to indicate the actual orientation of the captured photo.
// For this example, it'll behave incorrectly since UI orientation is allowed (and already-counter rotates the entire screen)
// For real phone apps, lock your UI orientation using a library like 'react-native-orientation-locker'
const rotateUi = true;
const uiRotation = orientationAnim.interpolate({
inputRange: [1, 4],
outputRange: ['180deg', '-90deg'],
});
const uiRotationStyle = rotateUi ? { transform: [{ rotate: uiRotation }] } : undefined;
function rotateUiTo(rotationValue: number) {
Animated.timing(orientationAnim, {
toValue: rotationValue,
useNativeDriver: true,
duration: 200,
isInteraction: false,
}).start();
}
return (
<View style={styles.screen}>
<StatusBar hidden />
<SafeAreaView style={styles.topButtons}>
<TouchableOpacity style={styles.topButton} onPress={onSetTorch}>
<Animated.Image
source={torchMode ? torchOnImage : torchOffImage}
resizeMode="contain"
style={[styles.topButtonImg, uiRotationStyle]}
/>
</TouchableOpacity>
<View style={styles.rightButtonsContainer}>
{showImagePickerButton && (
<TouchableOpacity
accessibilityRole="button"
accessibilityLabel={loc._.pick_image}
style={[styles.topButton, styles.spacing, uiRotationStyle]}
onPress={onImagePickerButtonPress}
>
<Icon name="image" type="font-awesome" color="#ffffff" />
</TouchableOpacity>
)}
{showFilePickerButton && (
<TouchableOpacity
accessibilityRole="button"
accessibilityLabel={loc._.pick_file}
style={[styles.topButton, styles.spacing, uiRotationStyle]}
onPress={onFilePickerButtonPress}
>
<Icon name="file-import" type="font-awesome-5" color="#ffffff" />
</TouchableOpacity>
)}
</View>
</SafeAreaView>
<View style={styles.cameraContainer}>
<Camera
ref={cameraRef}
style={styles.cameraPreview}
cameraType={cameraType}
resetFocusWhenMotionDetected
zoom={zoom}
maxZoom={10}
onZoom={e => {
console.debug('zoom', e.nativeEvent.zoom);
setZoom(e.nativeEvent.zoom);
}}
onReadCode={onReadCode}
torchMode={torchMode ? 'on' : 'off'}
shutterPhotoSound
maxPhotoQualityPrioritization="quality"
onOrientationChange={e => {
// We recommend locking the camera UI to portrait (using a different library)
// and rotating the UI elements counter to the orientation
// However, we include onOrientationChange so you can match your UI to what the camera does
switch (e.nativeEvent.orientation) {
case Orientation.PORTRAIT_UPSIDE_DOWN:
console.debug('orientationChange', 'PORTRAIT_UPSIDE_DOWN');
rotateUiTo(1);
break;
case Orientation.LANDSCAPE_LEFT:
console.debug('orientationChange', 'LANDSCAPE_LEFT');
rotateUiTo(2);
break;
case Orientation.PORTRAIT:
console.debug('orientationChange', 'PORTRAIT');
rotateUiTo(3);
break;
case Orientation.LANDSCAPE_RIGHT:
console.debug('orientationChange', 'LANDSCAPE_RIGHT');
rotateUiTo(4);
break;
default:
console.debug('orientationChange', e.nativeEvent);
break;
}
}}
/>
</View>
<SafeAreaView style={styles.bottomButtons}>
<TouchableOpacity onPress={onCancelButtonPress}>
<Animated.Text style={[styles.backTextStyle, uiRotationStyle]}>{loc._.cancel}</Animated.Text>
</TouchableOpacity>
<TouchableOpacity style={styles.bottomButton} onPress={onSwitchCameraPressed}>
<Animated.Image source={cameraFlipImage as ImageURISource} resizeMode="contain" style={[styles.topButtonImg, uiRotationStyle]} />
</TouchableOpacity>
</SafeAreaView>
</View>
);
};
export default CameraScreen;
const styles = StyleSheet.create({
screen: {
height: '100%',
backgroundColor: '#000000',
},
topButtons: {
margin: 10,
zIndex: 10,
flexDirection: 'row',
justifyContent: 'space-between',
},
topButton: {
backgroundColor: '#222',
width: 44,
height: 44,
borderRadius: 22,
justifyContent: 'center',
alignItems: 'center',
},
topButtonImg: {
margin: 10,
width: 24,
height: 24,
},
cameraContainer: {
justifyContent: 'center',
flex: 1,
},
cameraPreview: {
width: '100%',
height: '100%',
},
bottomButtons: {
margin: 10,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
backTextStyle: {
padding: 10,
color: 'white',
fontSize: 20,
},
rightButtonsContainer: {
flexDirection: 'row',
alignItems: 'center',
},
bottomButton: {
backgroundColor: '#222',
width: 44,
height: 44,
borderRadius: 22,
justifyContent: 'center',
alignItems: 'center',
marginLeft: 10,
},
spacing: {
marginLeft: 20,
},
});

View File

@ -183,6 +183,9 @@ const CompanionDelegates = () => {
if (fileName && /\.(jpe?g|png)$/i.test(fileName)) {
try {
if (!decodedUrl) {
throw new Error(loc.send.qr_error_no_qrcode);
}
const values = await RNQRGenerator.detect({
uri: decodedUrl,
});
@ -200,11 +203,12 @@ const CompanionDelegates = () => {
},
);
} else {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({ message: loc.send.qr_error_no_qrcode });
throw new Error(loc.send.qr_error_no_qrcode);
}
} catch (error) {
console.error('Error detecting QR code:', error);
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({ message: loc.send.qr_error_no_qrcode });
}
} else {
DeeplinkSchemaMatch.navigationRouteFor(event, (value: [string, any]) => navigationRef.navigate(...value), {

View File

@ -6,6 +6,7 @@ import { WalletCarouselItem } from './WalletsCarousel';
import { TransactionListItem } from './TransactionListItem';
import { useTheme } from './themes';
import { BitcoinUnit } from '../models/bitcoinUnits';
import { TouchableOpacityWrapper } from './ListItem';
enum ItemType {
WalletSection = 'wallet',
@ -29,11 +30,14 @@ interface ManageWalletsListItemProps {
isDraggingDisabled: boolean;
drag?: () => void;
isPlaceHolder?: boolean;
onPressIn?: () => void;
onPressOut?: () => void;
state: { wallets: TWallet[]; searchQuery: string };
navigateToWallet: (wallet: TWallet) => void;
renderHighlightedText: (text: string, query: string) => JSX.Element;
handleDeleteWallet: (wallet: TWallet) => void;
handleToggleHideBalance: (wallet: TWallet) => void;
isActive?: boolean;
}
interface SwipeContentProps {
@ -67,6 +71,9 @@ const ManageWalletsListItem: React.FC<ManageWalletsListItemProps> = ({
renderHighlightedText,
handleDeleteWallet,
handleToggleHideBalance,
onPressIn,
onPressOut,
isActive,
}) => {
const { colors } = useTheme();
@ -110,6 +117,10 @@ const ManageWalletsListItem: React.FC<ManageWalletsListItemProps> = ({
containerStyle={{ backgroundColor: colors.background }}
leftContent={leftContent}
rightContent={rightContent}
Component={TouchableOpacityWrapper}
onPressOut={onPressOut}
onPressIn={onPressIn}
style={isActive ? styles.activeItem : undefined}
>
<ListItem.Content
style={{
@ -121,6 +132,8 @@ const ManageWalletsListItem: React.FC<ManageWalletsListItemProps> = ({
item={item.data}
handleLongPress={isDraggingDisabled ? undefined : drag}
onPress={onPress}
onPressIn={onPressIn}
onPressOut={onPressOut}
animationsEnabled={false}
searchQuery={state.searchQuery}
isPlaceHolder={isPlaceHolder}
@ -164,6 +177,9 @@ const styles = StyleSheet.create({
alignItems: 'center',
backgroundColor: 'red',
},
activeItem: {
backgroundColor: 'rgba(0, 0, 0, 0.1)',
},
});
export { ManageWalletsListItem, LeftSwipeContent, RightSwipeContent };

View File

@ -155,9 +155,13 @@ const MultipleStepsListItem = props => {
style={[styles.rowPartialRightButton, stylesHook.provideKeyButton, rightButtonOpacity]}
onPress={props.button.onPress}
>
<Text style={[styles.provideKeyButtonText, stylesHook.provideKeyButtonText, styles.rightButton]}>
{props.button.text}
</Text>
{props.button.showActivityIndicator ? (
<ActivityIndicator />
) : (
<Text style={[styles.provideKeyButtonText, stylesHook.provideKeyButtonText, styles.rightButton]}>
{props.button.text}
</Text>
)}
</TouchableOpacity>
</View>
)}
@ -171,7 +175,11 @@ const MultipleStepsListItem = props => {
style={styles.rightButton}
onPress={props.rightButton.onPress}
>
<Text style={[styles.provideKeyButtonText, stylesHook.provideKeyButtonText]}>{props.rightButton.text}</Text>
{props.rightButton.showActivityIndicator ? (
<ActivityIndicator />
) : (
<Text style={[styles.provideKeyButtonText, stylesHook.provideKeyButtonText]}>{props.rightButton.text}</Text>
)}
</TouchableOpacity>
</View>
)}
@ -194,11 +202,13 @@ MultipleStepsListItem.propTypes = {
disabled: PropTypes.bool,
buttonType: PropTypes.number,
leftText: PropTypes.string,
showActivityIndicator: PropTypes.bool,
}),
rightButton: PropTypes.shape({
text: PropTypes.string,
onPress: PropTypes.func,
disabled: PropTypes.bool,
showActivityIndicator: PropTypes.bool,
}),
};

View File

@ -172,6 +172,8 @@ interface WalletCarouselItemProps {
searchQuery?: string;
renderHighlightedText?: (text: string, query: string) => JSX.Element;
animationsEnabled?: boolean;
onPressIn?: () => void;
onPressOut?: () => void;
}
export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
@ -186,6 +188,8 @@ export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
renderHighlightedText,
animationsEnabled = true,
isPlaceHolder = false,
onPressIn,
onPressOut,
}) => {
const scaleValue = useRef(new Animated.Value(1.0)).current;
const { colors } = useTheme();
@ -203,7 +207,8 @@ export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
tension: 100,
}).start();
}
}, [scaleValue, animationsEnabled]);
if (onPressIn) onPressIn();
}, [scaleValue, animationsEnabled, onPressIn]);
const onPressedOut = useCallback(() => {
if (animationsEnabled) {
@ -214,7 +219,8 @@ export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
tension: 100,
}).start();
}
}, [scaleValue, animationsEnabled]);
if (onPressOut) onPressOut();
}, [scaleValue, animationsEnabled, onPressOut]);
const handlePress = useCallback(() => {
onPressedOut();

View File

@ -3,6 +3,7 @@ import { navigationRef } from '../NavigationService';
import { presentWalletExportReminder } from '../helpers/presentWalletExportReminder';
import { unlockWithBiometrics, useBiometrics } from './useBiometrics';
import { useStorage } from './context/useStorage';
import { requestCameraAuthorization } from '../helpers/scan-qr';
// List of screens that require biometrics
const requiresBiometrics = ['WalletExportRoot', 'WalletXpubRoot', 'ViewEditMultisigCosignersRoot', 'ExportMultisigCoordinationSetupRoot'];
@ -90,6 +91,10 @@ export const useExtendedNavigation = <T extends NavigationProp<ParamListBase>>()
return; // Prevent proceeding with the original navigation if the reminder is shown
}
}
if (screenName === 'ScanQRCode') {
await requestCameraAuthorization();
}
proceedWithNavigation();
})();
};

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 /* (null) in Frameworks */ = {isa = PBXBuildFile; };
C978A716948AB7DEC5B6F677 /* BuildFile 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 /* (null) in Frameworks */,
C978A716948AB7DEC5B6F677 /* BuildFile in Frameworks */,
17CDA0718F42DB2CE856C872 /* libPods-BlueWallet.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;

View File

@ -56,7 +56,5 @@
<key>apiKey</key>
<string>17ba9059f676f1cc4f45d98182388b01</string>
</dict>
<key>WKCompanionAppBundleIdentifier</key>
<string>io.bluewallet.bluewallet</string>
</dict>
</plist>

View File

@ -1315,7 +1315,7 @@ PODS:
- Yoga
- react-native-ios-context-menu (1.15.3):
- React-Core
- react-native-menu (1.1.7):
- react-native-menu (1.2.0):
- React
- react-native-randombytes (3.6.1):
- React-Core
@ -1588,8 +1588,27 @@ PODS:
- React-logger (= 0.75.4)
- React-perflogger (= 0.75.4)
- React-utils (= 0.75.4)
- ReactNativeCameraKit (13.0.0):
- ReactNativeCameraKit (14.1.0):
- DoubleConversion
- glog
- hermes-engine
- RCT-Folly (= 2024.01.01.00)
- RCTRequired
- RCTTypeSafety
- React-Core
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-ImageManager
- React-NativeModulesApple
- React-RCTFabric
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- RealmJS (20.1.0):
- React
- RNCAsyncStorage (2.1.0):
@ -1650,7 +1669,7 @@ PODS:
- Yoga
- RNLocalize (3.3.0):
- React-Core
- RNPermissions (5.2.1):
- RNPermissions (5.2.2):
- React-Core
- RNQrGenerator (1.4.2):
- React
@ -1680,7 +1699,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- RNReanimated (3.16.5):
- RNReanimated (3.16.6):
- DoubleConversion
- glog
- hermes-engine
@ -1700,10 +1719,10 @@ PODS:
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- RNReanimated/reanimated (= 3.16.5)
- RNReanimated/worklets (= 3.16.5)
- RNReanimated/reanimated (= 3.16.6)
- RNReanimated/worklets (= 3.16.6)
- Yoga
- RNReanimated/reanimated (3.16.5):
- RNReanimated/reanimated (3.16.6):
- DoubleConversion
- glog
- hermes-engine
@ -1723,9 +1742,9 @@ PODS:
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- RNReanimated/reanimated/apple (= 3.16.5)
- RNReanimated/reanimated/apple (= 3.16.6)
- Yoga
- RNReanimated/reanimated/apple (3.16.5):
- RNReanimated/reanimated/apple (3.16.6):
- DoubleConversion
- glog
- hermes-engine
@ -1746,7 +1765,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- RNReanimated/worklets (3.16.5):
- RNReanimated/worklets (3.16.6):
- DoubleConversion
- glog
- hermes-engine
@ -2226,7 +2245,7 @@ SPEC CHECKSUMS:
react-native-document-picker: 530879d9e89b490f0954bcc4ab697c5b5e35d659
react-native-image-picker: 19a8d8471a239890675726f88f9c18dd213656d5
react-native-ios-context-menu: 986da6dcba70094bcc2a8049f68410fe7d25aff1
react-native-menu: 5779f6bd7a4e58d457ca5e0a6f164651dd26cd7f
react-native-menu: 74230a5879e0ca697e98ee7c3087297dc774bf06
react-native-randombytes: 3c8f3e89d12487fd03a2f966c288d495415fc116
react-native-safe-area-context: 758e894ca5a9bd1868d2a9cfbca7326a2b6bf9dc
react-native-screen-capture: 7b6121f529681ed2fde36cdedadd0bb39e9a3796
@ -2258,7 +2277,7 @@ SPEC CHECKSUMS:
React-utils: 02526ea15628a768b8db9517b6017a1785c734d2
ReactCodegen: 8b5341ecb61898b8bd40a73ebc443c6bf2d14423
ReactCommon: 36d48f542b4010786d6b2bcee615fe5f906b7105
ReactNativeCameraKit: f058d47e0b1e55fd819bb55ee16505a2e0ca53db
ReactNativeCameraKit: e72b838dac4ea2da19b7eb5d00b23125072790fd
RealmJS: 9fd51c849eb552ade9f7b11db42a319b4f6cab4c
RNCAsyncStorage: c91d753ede6dc21862c4922cd13f98f7cfde578e
RNCClipboard: dbcf25b8f666b4685c02eeb65be981d30198e505
@ -2270,12 +2289,12 @@ SPEC CHECKSUMS:
RNHandoff: bc8af5a86853ff13b033e7ba1114c3c5b38e6385
RNKeychain: 4df48b5186ca2b6a99f5ead69ad587154e084a32
RNLocalize: d024afa9204c13885e61dc88b8190651bcaabac9
RNPermissions: 979aa94a1a2091e3b2c3e7130ef0a1a6e331e05a
RNPermissions: 6f08c623b0c8ca7d95faa71c3956b159b34f25c3
RNQrGenerator: 7c604c0eb608af64ff586ab0c040796a04eff247
RNQuickAction: c2c8f379e614428be0babe4d53a575739667744d
RNRate: 7641919330e0d6688ad885a985b4bd697ed7d14c
RNReactNativeHapticFeedback: 00ba111b82aa266bb3ee1aa576831c2ea9a9dfad
RNReanimated: ae56eba247f82fa0d8bbf52bb0e7a34a218482de
RNReanimated: 000b758cfbcd9c20c15b7ef305f98f036b288feb
RNScreens: 35bb8e81aeccf111baa0ea01a54231390dbbcfd9
RNShare: 6204e6a1987ba3e7c47071ef703e5449a0e3548a
RNSVG: 3421710ac15f4f2dc47e5c122f2c2e4282116830

View File

@ -192,7 +192,7 @@
"outdated_rate": "Rate was last updated: {date}",
"psbt_tx_open": "Open Signed Transaction",
"psbt_tx_scan": "Scan Signed Transaction",
"qr_error_no_qrcode": "We were unable to find a QR Code in the selected image. Make sure the image contains only a QR Code and no additional content such as text or buttons.",
"qr_error_no_qrcode": "We were unable to find a valid QR Code in the selected image. Make sure the image contains only a QR Code and no additional content such as text or buttons.",
"reset_amount": "Reset Amount",
"reset_amount_confirm": "Would you like to reset the amount?",
"success_done": "Done",

View File

@ -28,6 +28,7 @@
"enter_amount": "Ingresa la cantidad",
"qr_custom_input_button": "Pulsa 10 veces para ingresar una entrada personalizada",
"unlock": "Desbloquear",
"port": "Puerto",
"suggested": "Sugerido"
},
"azteco": {
@ -74,6 +75,7 @@
"please_pay": "Pagar por favor",
"preimage": "Imagen previa",
"sats": "sats.",
"date_time": "Fecha y hora",
"wasnt_paid_and_expired": "Esta factura no se pagó y ha caducado."
},
"plausibledeniability": {
@ -190,7 +192,7 @@
"outdated_rate": "La tarifa se actualizó por última vez: {date}",
"psbt_tx_open": "Abrir transacción firmada",
"psbt_tx_scan": "Escanear transacción firmada",
"qr_error_no_qrcode": "No pudimos encontrar un código QR en la imagen seleccionada. Asegúrate de que la imagen contenga solo un código QR y ningún contenido adicional como texto o botones.",
"qr_error_no_qrcode": "No pudimos encontrar un código QR válido en la imagen seleccionada. Asegúrate de que la imagen contenga solo un código QR y ningún contenido adicional, como texto o botones.",
"reset_amount": "Restablecer monto",
"reset_amount_confirm": "¿Te gustaría restablecer la cantidad?",
"success_done": "Hecho",
@ -215,6 +217,7 @@
"block_explorer_invalid_custom_url": "La URL proporcionada no es válida. Ingresa una URL válida que comience con http:// o https://.",
"about_selftest_electrum_disabled": "La autocomprobación no está disponible con el modo sin conexión de Electrum. Desactiva el modo sin conexión y vuelve a intentarlo. ",
"about_selftest_ok": "Todas las pruebas internas han pasado satisfactoriamente. La billetera funciona bien.",
"about_sm_github": "GitHub",
"about_sm_discord": "Servidor Discord",
"about_sm_telegram": "Chat de Telegram ",
@ -249,15 +252,14 @@
"electrum_settings_server": "Servidor Electrum",
"electrum_status": "Estado",
"electrum_preferred_server": "Servidor preferido",
"electrum_preferred_server_description": "Introduce el servidor que deseas que tu billetera utilice para todas las actividades de Bitcoin. Una vez configurado, tu billetera utilizará exclusivamente este servidor para comprobar saldos, enviar transacciones y obtener datos de la red. Asegúrate de que confías en este servidor antes de configurarlo.",
"electrum_clear_alert_title": "¿Borrar historial?",
"electrum_preferred_server_description": "Introduce el servidor que deseas que tu billetera utilice para todas las actividades de Bitcoin. Una vez configurado, tu billetera utilizará exclusivamente este servidor para comprobar saldos, enviar transacciones y obtener datos de la red. Asegúrate de que confías en este servidor antes de configurarlo.", "electrum_clear_alert_title": "¿Borrar historial?",
"electrum_clear_alert_message": "¿Quieres borrar el historial de los servidores de Electrum?",
"electrum_clear_alert_cancel": "Cancelar",
"electrum_clear_alert_ok": "Ok",
"electrum_reset": "Restablecer a predeterminado",
"only_use_preferred": "Conectarse únicamente al servidor preferido",
"electrum_unable_to_connect": "No se puede conectar al {server}.",
"electrum_history": "Historial",
"electrum_reset_to_default": "¿Estás seguro de querer restablecer la configuración de Electrum a los valores predeterminados?",
"electrum_reset_to_default": "Esto permitirá que BlueWallet elija aleatoriamente un servidor de la lista sugerida y del historial. El historial de tu servidor permanecerá sin cambios",
"electrum_reset": "Restablecer a predeterminado",
"electrum_clear": "Borrar historial",
"encrypt_decrypt": "Descifrar Almacenamiento",
"encrypt_decrypt_q": "¿Estás seguro de que deseas descifrar tu almacenamiento? Esto permitirá acceder a tus billeteras sin una contraseña.",
@ -272,6 +274,7 @@
"encrypt_title": "Seguridad",
"encrypt_tstorage": "Almacenamiento",
"encrypt_use": "Usar {type}",
"set_as_preferred": "Establecer como preferido",
"encrypted_feature_disabled": "Esta función no se puede utilizar con el almacenamiento cifrado habilitado.",
"encrypt_use_expl": "{type} se utilizará para confirmar tu identidad antes de realizar una transacción, desbloquear, exportar o eliminar una billetera. {type} no se utilizará para desbloquear el almacenamiento encriptado.",
"biometrics_fail": "Si {type} no está activado o no se desbloquea, puedes utilizar el código de acceso de tu dispositivo como alternativa.",
@ -291,6 +294,7 @@
"network": "Red",
"network_broadcast": "Publicar transacción",
"network_electrum": "Servidor Electrum",
"electrum_suggested_description": "Cuando no se establece un servidor preferido, se seleccionará un servidor sugerido para su uso al azar.",
"not_a_valid_uri": "URI inválido",
"notifications": "Notificaciones",
"open_link_in_explorer": "Abrir enlace en el explorador",
@ -323,7 +327,7 @@
"permission_denied_message": "Has denegado el envío de notificaciones. Si deseas recibirlas, actívalas en la configuración de tu dispositivo."
},
"transactions": {
"cancel_explain": "Reemplazaremos esta transacción con una que te pague y tenga tarifas más altas. Esto cancela efectivamente la transacción actual. Esto se llama RBF—Replace by Fee.",
"cancel_explain": "Reemplazaremos esta transacción con una que te pague y tenga tarifas más altas. Esto cancela efectivamente la transacción actual. Esto se llama RBF (Replace by Fee).",
"cancel_no": "Esta transacción no es reemplazable.",
"cancel_title": "Cancelar ésta transacción (RBF)",
"transaction_loading_error": "Se ha producido un problema al cargar la transacción. Vuelve a intentarlo más tarde.",
@ -332,7 +336,7 @@
"copy_link": "Copiar enlace",
"expand_note": "Expandir Nota",
"cpfp_create": "Crear",
"cpfp_exp": "Crearemos otra transacción que gaste tu transacción no confirmada. La tarifa total será más alta que la tarifa de la transacción original, por lo que debería extraerse más rápido. Esto se llama CPFP — Child Pays for Parent.",
"cpfp_exp": "Crearemos otra transacción que gaste tu transacción no confirmada. La tarifa total será más alta que la tarifa de la transacción original, por lo que debería extraerse más rápido. Esto se llama CPFP (Child Pays for Parent).",
"cpfp_no_bump": "Esta transacción no se puede acelerar.",
"cpfp_title": "Aumentar Comisión (CPFP)",
"details_balance_hide": "Ocultar Balance",
@ -367,7 +371,7 @@
"list_title": "Transacciones",
"transaction": "Transacción",
"open_url_error": "No se puede abrir el enlace con el navegador predeterminado. Cambia tu navegador predeterminado y vuelve a intentarlo.",
"rbf_explain": "Reemplazaremos esta transacción con una con una tarifa más alta para que se extraiga más rápido. Esto se llama RBF—Replace by Fee.",
"rbf_explain": "Reemplazaremos esta transacción con una con una tarifa más alta para que se extraiga más rápido. Esto se llama RBF (Replace by Fee)",
"rbf_title": "Aumentar Comisión (RBF)",
"status_bump": "Aumentar Comisión",
"status_cancel": "Cancelar Transacción",
@ -567,7 +571,7 @@
"ms_help_title": "Cómo funcionan las Bóvedas Multifirma: Consejos y Trucos",
"ms_help_text": "Una billetera con varias llaves para mayor seguridad o custodia compartida",
"ms_help_title1": "Se recomiendan varios dispositivos.",
"ms_help_1": "La Bóveda funcionará con otras apps de BlueWallet instalada en otros dispositivos y billeteras compatibles con PSBT, como Electrum, Spectre, Coldcard, Cobo Vault, etc.",
"ms_help_1": "La Bóveda funcionará con BlueWallet instalada en otros dispositivos y billeteras compatibles con PSBT, como Electrum, Spectre, Coldcard, Keystone, etc.",
"ms_help_title2": "Editar Claves",
"ms_help_2": "Puedes crear todas las claves de la Bóveda en este dispositivo y eliminarlas o editarlas después. Tener todas las claves en el mismo dispositivo tiene la seguridad equivalente a la de un monedero de Bitcoin normal.",
"ms_help_title3": "Copias de seguridad de la Bóveda",
@ -654,6 +658,8 @@
"bip47": {
"payment_code": "Código de pago",
"contacts": "Contactos",
"bip47_explain": "Código reutilizable y compartible",
"bip47_explain_subtitle": "BIP47",
"purpose": "Código reutilizable y compartible (BIP47)",
"pay_this_contact": "Paga a este contacto",
"rename_contact": "Renombrar contacto",
@ -667,7 +673,7 @@
"notification_tx_unconfirmed": "La transacción de notificación aún no está confirmada, espera",
"failed_create_notif_tx": "No se pudo crear una transacción en cadena",
"onchain_tx_needed": "Se necesita transacción en cadena",
"notif_tx_sent": "Transacción de notificación enviada. Espera a que se confirme",
"notif_tx_sent" : "Transacción de notificación enviada. Espera a que se confirme",
"notif_tx": "Transacción de notificación",
"not_found": "Código de pago no encontrado"
}

View File

@ -125,6 +125,13 @@
"symbol": "£",
"country": "United Kingdom (British Pound)"
},
"HKD": {
"endPointKey": "HKD",
"locale": "zh-HK",
"source": "CoinGecko",
"symbol": "HK$",
"country": "Hong Kong (Hong Kong Dollar)"
},
"HRK": {
"endPointKey": "HRK",
"locale": "hr-HR",

View File

@ -9,7 +9,6 @@ export type ScanQRCodeParamList = {
urTotal?: number;
urHave?: number;
backdoorText?: string;
onDismiss?: () => void;
onBarScanned?: (data: string) => void;
showFileImportButton?: boolean;
backdoorVisible?: boolean;

36
package-lock.json generated
View File

@ -63,12 +63,12 @@
"react-native": "0.75.4",
"react-native-biometrics": "3.0.1",
"react-native-blue-crypto": "github:BlueWallet/react-native-blue-crypto#3cb5442",
"react-native-camera-kit": "13.0.0",
"react-native-camera-kit": "14.1.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": "13.2.0",
"react-native-document-picker": "9.3.1",
"react-native-draggable-flatlist": "github:BlueWallet/react-native-draggable-flatlist#3a61627",
"react-native-draglist": "github:BlueWallet/react-native-draglist#a4af02f",
"react-native-fs": "2.20.0",
"react-native-gesture-handler": "2.21.2",
"react-native-handoff": "github:BlueWallet/react-native-handoff#v0.0.4",
@ -78,7 +78,7 @@
"react-native-keychain": "9.1.0",
"react-native-linear-gradient": "2.8.3",
"react-native-localize": "3.3.0",
"react-native-permissions": "5.2.1",
"react-native-permissions": "5.2.2",
"react-native-prompt-android": "github:BlueWallet/react-native-prompt-android#ed168d66fed556bc2ed07cf498770f058b78a376",
"react-native-push-notification": "8.1.1",
"react-native-qrcode-svg": "6.3.2",
@ -20530,12 +20530,12 @@
}
},
"node_modules/react-native-camera-kit": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/react-native-camera-kit/-/react-native-camera-kit-13.0.0.tgz",
"integrity": "sha512-fnkyivCG2xzS+14/doP8pCAYNafYaTyg5J0t+JJltJdgKSHf328OG44Rd+fnbbEOydZxgy/bcuLB24R0kCbynw==",
"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==",
"license": "MIT",
"dependencies": {
"lodash": "^4.14.2"
"engines": {
"node": ">=18"
},
"peerDependencies": {
"react": "*",
@ -20615,17 +20615,13 @@
}
}
},
"node_modules/react-native-draggable-flatlist": {
"version": "4.0.1",
"resolved": "git+ssh://git@github.com/BlueWallet/react-native-draggable-flatlist.git#3a61627474a4e35198ae961310c77fb305507509",
"node_modules/react-native-draglist": {
"version": "3.8.0",
"resolved": "git+ssh://git@github.com/BlueWallet/react-native-draglist.git#a4af02fec803b75508a8136e35eca564bbb1d644",
"license": "MIT",
"dependencies": {
"@babel/preset-typescript": "^7.17.12"
},
"peerDependencies": {
"react-native": ">=0.64.0",
"react-native-gesture-handler": ">=2.0.0",
"react-native-reanimated": ">=2.8.0"
"react": ">=17.0.1",
"react-native": ">=0.64.0"
}
},
"node_modules/react-native-fs": {
@ -20747,9 +20743,9 @@
}
},
"node_modules/react-native-permissions": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/react-native-permissions/-/react-native-permissions-5.2.1.tgz",
"integrity": "sha512-F8CaDVi+zYjl2pO4Fwh5n5SCi5s0TvvRzQnczb8nwkzHJTv6HqlO6Sj1ZmqrAeuCN06b5Ysu9BEWhK4qT4ekXg==",
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/react-native-permissions/-/react-native-permissions-5.2.2.tgz",
"integrity": "sha512-Mae5VKT8bjliksONZ+jMYTPf90wxuhn1H1FiH/kRfw0Y5tW5WIV1P8t/KiEHKZRvimnrInimuCr+EpRzK0IPWQ==",
"license": "MIT",
"peerDependencies": {
"react": ">=18.1.0",

View File

@ -59,7 +59,7 @@
"android:clean": "cd android; ./gradlew clean ; cd .. ; npm run android",
"ios": "react-native run-ios",
"postinstall": "rn-nodeify --install buffer,events,process,stream,inherits,path,assert,crypto --hack; npm run releasenotes2json; npm run branch2json; npm run patches",
"patches": "patch -p1 < scripts/react-native-camera-kit.patch;",
"patches": "",
"test": "npm run tslint && npm run lint && npm run unit && npm run jest",
"jest": "jest tests/integration/*",
"e2e:debug-build": "detox build -c android.debug",
@ -127,12 +127,12 @@
"react-native": "0.75.4",
"react-native-biometrics": "3.0.1",
"react-native-blue-crypto": "github:BlueWallet/react-native-blue-crypto#3cb5442",
"react-native-camera-kit": "13.0.0",
"react-native-camera-kit": "14.1.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": "13.2.0",
"react-native-document-picker": "9.3.1",
"react-native-draggable-flatlist": "github:BlueWallet/react-native-draggable-flatlist#3a61627",
"react-native-draglist": "github:BlueWallet/react-native-draglist#a4af02f",
"react-native-fs": "2.20.0",
"react-native-gesture-handler": "2.21.2",
"react-native-handoff": "github:BlueWallet/react-native-handoff#v0.0.4",
@ -142,7 +142,7 @@
"react-native-keychain": "9.1.0",
"react-native-linear-gradient": "2.8.3",
"react-native-localize": "3.3.0",
"react-native-permissions": "5.2.1",
"react-native-permissions": "5.2.2",
"react-native-prompt-android": "github:BlueWallet/react-native-prompt-android#ed168d66fed556bc2ed07cf498770f058b78a376",
"react-native-push-notification": "8.1.1",
"react-native-qrcode-svg": "6.3.2",

View File

@ -2,23 +2,18 @@ import { useFocusEffect, useIsFocused, useNavigation, useRoute } from '@react-na
import * as bitcoin from 'bitcoinjs-lib';
import createHash from 'create-hash';
import React, { useCallback, useEffect, useState } from 'react';
import { Alert, Image, Platform, StyleSheet, TextInput, TouchableOpacity, View } from 'react-native';
import { CameraScreen } from 'react-native-camera-kit';
import { Icon } from '@rneui/themed';
import { launchImageLibrary } from 'react-native-image-picker';
import { Alert, Platform, StyleSheet, TextInput, TouchableOpacity, View } from 'react-native';
import Base43 from '../../blue_modules/base43';
import * as fs from '../../blue_modules/fs';
import { BlueURDecoder, decodeUR, extractSingleWorkload } from '../../blue_modules/ur';
import { BlueLoading, BlueSpacing40, BlueText } from '../../BlueComponents';
import { openPrivacyDesktopSettings } from '../../class/camera';
import presentAlert from '../../components/Alert';
import Button from '../../components/Button';
import { useTheme } from '../../components/themes';
import { isCameraAuthorizationStatusGranted } from '../../helpers/scan-qr';
import loc from '../../loc';
import { useSettings } from '../../hooks/context/useSettings';
import RNQRGenerator from 'rn-qr-generator';
import CameraScreen from '../../components/CameraScreen';
let decoder = false;
@ -27,39 +22,6 @@ const styles = StyleSheet.create({
flex: 1,
backgroundColor: '#000000',
},
closeTouch: {
width: 40,
height: 40,
backgroundColor: 'rgba(0,0,0,0.4)',
justifyContent: 'center',
borderRadius: 20,
position: 'absolute',
left: 16,
top: 55,
},
closeImage: {
alignSelf: 'center',
},
imagePickerTouch: {
width: 40,
height: 40,
backgroundColor: 'rgba(0,0,0,0.4)',
justifyContent: 'center',
borderRadius: 20,
position: 'absolute',
left: 24,
bottom: 48,
},
filePickerTouch: {
width: 40,
height: 40,
backgroundColor: 'rgba(0,0,0,0.4)',
justifyContent: 'center',
borderRadius: 20,
position: 'absolute',
left: 96,
bottom: 48,
},
openSettingsContainer: {
flex: 1,
justifyContent: 'center',
@ -71,6 +33,9 @@ const styles = StyleSheet.create({
height: 60,
backgroundColor: 'rgba(0,0,0,0.01)',
position: 'absolute',
top: 10,
left: '50%',
transform: [{ translateX: -30 }],
},
backdoorInputWrapper: { position: 'absolute', left: '5%', top: '0%', width: '90%', height: '70%', backgroundColor: 'white' },
progressWrapper: { position: 'absolute', alignSelf: 'center', alignItems: 'center', top: '50%', padding: 8, borderRadius: 8 },
@ -93,7 +58,7 @@ const ScanQRCode = () => {
const previousRoute = navigationState.routes[navigationState.routes.length - 2];
const defaultLaunchedBy = previousRoute ? previousRoute.name : undefined;
const { launchedBy = defaultLaunchedBy, onBarScanned, onDismiss, showFileImportButton } = route.params || {};
const { launchedBy = defaultLaunchedBy, onBarScanned, showFileImportButton } = route.params || {};
const scannedCache = {};
const { colors } = useTheme();
const isFocused = useIsFocused();
@ -292,59 +257,19 @@ const ScanQRCode = () => {
setIsLoading(false);
};
const showImagePicker = () => {
const onShowImagePickerButtonPress = () => {
if (!isLoading) {
setIsLoading(true);
launchImageLibrary(
{
title: null,
mediaType: 'photo',
takePhotoButtonTitle: null,
maxHeight: 800,
maxWidth: 600,
selectionLimit: 1,
},
response => {
if (response.didCancel) {
setIsLoading(false);
} else {
const asset = response.assets[0];
if (asset.uri) {
RNQRGenerator.detect({
uri: decodeURI(asset.uri.toString()),
})
.then(result => {
if (result) {
onBarCodeRead({ data: result.values[0] });
}
})
.catch(error => {
console.error(error);
presentAlert({ message: loc.send.qr_error_no_qrcode });
})
.finally(() => {
setIsLoading(false);
});
} else {
setIsLoading(false);
}
}
},
);
fs.showImagePickerAndReadImage()
.then(data => {
if (data) onBarCodeRead({ data });
})
.finally(() => setIsLoading(false));
}
};
const dismiss = () => {
if (launchedBy) {
let merge = true;
if (typeof onBarScanned !== 'function') {
merge = false;
}
navigation.navigate({ name: launchedBy, params: {}, merge });
} else {
navigation.goBack();
}
if (onDismiss) onDismiss();
navigation.goBack();
};
const render = isLoading ? (
@ -365,29 +290,13 @@ const ScanQRCode = () => {
cameraFlipImage={require('../../img/camera-rotate-solid.png')}
onReadCode={event => onBarCodeRead({ data: event?.nativeEvent?.codeStringValue })}
showFrame={false}
showFilePickerButton={showFileImportButton}
showImagePickerButton={true}
onFilePickerButtonPress={showFilePicker}
onImagePickerButtonPress={onShowImagePickerButtonPress}
onCancelButtonPress={dismiss}
/>
) : null}
<TouchableOpacity accessibilityRole="button" accessibilityLabel={loc._.close} style={styles.closeTouch} onPress={dismiss}>
<Image style={styles.closeImage} source={require('../../img/close-white.png')} />
</TouchableOpacity>
<TouchableOpacity
accessibilityRole="button"
accessibilityLabel={loc._.pick_image}
style={styles.imagePickerTouch}
onPress={showImagePicker}
>
<Icon name="image" type="font-awesome" color="#ffffff" />
</TouchableOpacity>
{showFileImportButton && (
<TouchableOpacity
accessibilityRole="button"
accessibilityLabel={loc._.pick_file}
style={styles.filePickerTouch}
onPress={showFilePicker}
>
<Icon name="file-import" type="font-awesome-5" color="#ffffff" />
</TouchableOpacity>
)}
{urTotal > 0 && (
<View style={[styles.progressWrapper, stylesHook.progressWrapper]} testID="UrProgressBar">
<BlueText>{loc.wallets.please_continue_scanning}</BlueText>

View File

@ -100,28 +100,28 @@ const LightningSettings: React.FC = () => {
setURI(typeof setLndHubUrl === 'string' ? setLndHubUrl.trim() : value.trim());
};
const save = useCallback(async () => {
setIsLoading(true);
try {
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
if (URI) {
const normalizedURI = new URL(URI.replace(/([^:]\/)\/+/g, '$1')).toString();
await LightningCustodianWallet.isValidNodeAddress(normalizedURI);
const save = useCallback(async () => {
setIsLoading(true);
try {
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
if (URI) {
const normalizedURI = new URL(URI.replace(/([^:]\/)\/+/g, '$1')).toString();
await LightningCustodianWallet.isValidNodeAddress(normalizedURI);
await setLNDHub(normalizedURI);
} else {
await clearLNDHub();
await setLNDHub(normalizedURI);
} else {
await clearLNDHub();
}
presentAlert({ message: loc.settings.lightning_saved, type: AlertType.Toast });
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
} catch (error) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({ message: loc.settings.lightning_error_lndhub_uri });
console.log(error);
}
presentAlert({ message: loc.settings.lightning_saved, type: AlertType.Toast });
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
} catch (error) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({ message: loc.settings.lightning_error_lndhub_uri });
console.log(error);
}
setIsLoading(false);
}, [URI]);
setIsLoading(false);
}, [URI]);
const importScan = () => {
navigate('ScanQRCode', {

View File

@ -1,13 +1,5 @@
import React, { useEffect, useLayoutEffect, useReducer, useCallback, useMemo, useRef } from 'react';
import { StyleSheet, TouchableOpacity, Image, Text, Alert, I18nManager, Animated, LayoutAnimation } from 'react-native';
import {
NestableScrollContainer,
ScaleDecorator,
OpacityDecorator,
NestableDraggableFlatList,
RenderItem,
// @ts-expect-error: react-native-draggable-flatlist is not typed
} from 'react-native-draggable-flatlist';
import React, { useEffect, useLayoutEffect, useReducer, useCallback, useMemo, useRef, useState } from 'react';
import { StyleSheet, TouchableOpacity, Image, Text, Alert, I18nManager, Animated, LayoutAnimation, FlatList } from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { useFocusEffect, useNavigation } from '@react-navigation/native';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
@ -25,6 +17,7 @@ import prompt from '../../helpers/prompt';
import HeaderRightButton from '../../components/HeaderRightButton';
import { ManageWalletsListItem } from '../../components/ManageWalletsListItem';
import { useSettings } from '../../hooks/context/useSettings';
import DragList, { DragListRenderItemInfo } from 'react-native-draglist';
enum ItemType {
WalletSection = 'wallet',
@ -206,21 +199,24 @@ const ManageWallets: React.FC = () => {
color: colors.foregroundColor,
},
};
const [data, setData] = useState(state.tempOrder);
const listRef = useRef<FlatList<Item> | null>(null);
useEffect(() => {
dispatch({
type: SET_INITIAL_ORDER,
payload: { wallets: walletsRef.current, txMetadata },
});
setData(state.tempOrder);
}, [state.tempOrder]);
useEffect(() => {
dispatch({ type: SET_INITIAL_ORDER, payload: { wallets: walletsRef.current, txMetadata } });
}, [txMetadata]);
useEffect(() => {
if (debouncedSearchQuery) {
dispatch({ type: SET_FILTERED_ORDER, payload: debouncedSearchQuery });
} else {
dispatch({ type: SET_INITIAL_ORDER, payload: { wallets: walletsRef.current, txMetadata } });
dispatch({ type: SET_TEMP_ORDER, payload: state.order });
}
}, [debouncedSearchQuery, txMetadata]);
}, [debouncedSearchQuery, state.order]);
const handleClose = useCallback(() => {
if (state.searchQuery.length === 0 && !state.isSearchFocused) {
@ -244,6 +240,7 @@ const ManageWallets: React.FC = () => {
dispatch({ type: SET_IS_SEARCH_FOCUSED, payload: false });
}
}, [goBack, setWalletsWithNewOrder, state.searchQuery, state.isSearchFocused, state.tempOrder, navigation]);
const hasUnsavedChanges = useMemo(() => {
return JSON.stringify(walletsRef.current) !== JSON.stringify(state.tempOrder.map(item => item.data));
}, [state.tempOrder]);
@ -319,6 +316,14 @@ const ManageWallets: React.FC = () => {
}, [hasUnsavedChanges, navigation, setIsDrawerShouldHide]),
);
// Ensure the listener is re-added every time there are unsaved changes
useEffect(() => {
if (beforeRemoveListenerRef.current) {
navigation.removeListener('beforeRemove', beforeRemoveListenerRef.current);
navigation.addListener('beforeRemove', beforeRemoveListenerRef.current);
}
}, [hasUnsavedChanges, navigation]);
const renderHighlightedText = useCallback(
(text: string, query: string) => {
const parts = text.split(new RegExp(`(${query})`, 'gi'));
@ -425,60 +430,43 @@ const ManageWallets: React.FC = () => {
},
[goBack, navigate],
);
const renderWalletItem = useCallback(
({ item, drag, isActive }: RenderItem<Item>) => (
<ScaleDecorator drag={drag} activeScale={1.1}>
<OpacityDecorator activeOpacity={0.5}>
<ManageWalletsListItem
item={item}
isDraggingDisabled={state.searchQuery.length > 0 || state.isSearchFocused}
drag={drag}
state={state}
navigateToWallet={navigateToWallet}
renderHighlightedText={renderHighlightedText}
handleDeleteWallet={handleDeleteWallet}
handleToggleHideBalance={handleToggleHideBalance}
/>
</OpacityDecorator>
</ScaleDecorator>
),
const renderItem = useCallback(
(info: DragListRenderItemInfo<Item>) => {
const { item, onDragStart, onDragEnd, isActive } = info;
return (
<ManageWalletsListItem
item={item}
onPressIn={state.isSearchFocused || state.searchQuery.length > 0 ? undefined : onDragStart}
onPressOut={state.isSearchFocused || state.searchQuery.length > 0 ? undefined : onDragEnd}
isDraggingDisabled={state.searchQuery.length > 0 || state.isSearchFocused}
state={state}
navigateToWallet={navigateToWallet}
renderHighlightedText={renderHighlightedText}
handleDeleteWallet={handleDeleteWallet}
handleToggleHideBalance={handleToggleHideBalance}
isActive={isActive}
drag={state.isSearchFocused || state.searchQuery.length > 0 ? undefined : onDragStart}
/>
);
},
[state, navigateToWallet, renderHighlightedText, handleDeleteWallet, handleToggleHideBalance],
);
const renderPlaceholder = useCallback(
({ item, drag, isActive }: RenderItem<Item>) => (
<ManageWalletsListItem
item={item}
isDraggingDisabled={state.searchQuery.length > 0 || state.isSearchFocused}
state={state}
navigateToWallet={navigateToWallet}
renderHighlightedText={renderHighlightedText}
isPlaceHolder
handleDeleteWallet={handleDeleteWallet}
handleToggleHideBalance={handleToggleHideBalance}
/>
),
[handleDeleteWallet, handleToggleHideBalance, navigateToWallet, renderHighlightedText, state],
);
const onChangeOrder = useCallback(() => {
triggerHapticFeedback(HapticFeedbackTypes.ImpactMedium);
}, []);
const onDragBegin = useCallback(() => {
triggerHapticFeedback(HapticFeedbackTypes.Selection);
}, []);
const onRelease = useCallback(() => {
triggerHapticFeedback(HapticFeedbackTypes.ImpactLight);
}, []);
const onDragEnd = useCallback(
({ data }: { data: Item[] }) => {
const updatedWallets = data.filter((item): item is WalletItem => item.type === ItemType.WalletSection).map(item => item.data);
dispatch({ type: SET_INITIAL_ORDER, payload: { wallets: updatedWallets, txMetadata: state.txMetadata } });
const onReordered = useCallback(
(fromIndex: number, toIndex: number) => {
const copy = [...state.order];
const removed = copy.splice(fromIndex, 1);
copy.splice(toIndex, 0, removed[0]);
dispatch({ type: SET_TEMP_ORDER, payload: copy });
dispatch({
type: SET_INITIAL_ORDER,
payload: {
wallets: copy.filter(item => item.type === ItemType.WalletSection).map(item => item.data as TWallet),
txMetadata: state.txMetadata,
},
});
},
[state.txMetadata],
[state.order, state.txMetadata],
);
const keyExtractor = useCallback((item: Item, index: number) => index.toString(), []);
@ -499,39 +487,21 @@ const ManageWallets: React.FC = () => {
return (
<GestureHandlerRootView style={[{ backgroundColor: colors.background }, styles.root]}>
<NestableScrollContainer contentInsetAdjustmentBehavior="automatic" automaticallyAdjustContentInsets scrollEnabled>
<>
{renderHeader}
<NestableDraggableFlatList
data={state.tempOrder.filter((item): item is WalletItem => item.type === ItemType.WalletSection)}
extraData={state.tempOrder}
keyExtractor={keyExtractor}
renderItem={renderWalletItem}
onChangeOrder={onChangeOrder}
onDragBegin={onDragBegin}
onPlaceholderIndexChange={onChangeOrder}
onRelease={onRelease}
delayLongPress={150}
useNativeDriver={true}
dragItemOverflow
autoscrollThreshold={1}
renderPlaceholder={renderPlaceholder}
autoscrollSpeed={0.5}
contentInsetAdjustmentBehavior="automatic"
<DragList
automaticallyAdjustContentInsets
onDragEnd={onDragEnd}
containerStyle={styles.root}
/>
<NestableDraggableFlatList
data={state.tempOrder.filter((item): item is TransactionItem => item.type === ItemType.TransactionSection)}
keyExtractor={keyExtractor}
renderItem={renderWalletItem}
dragItemOverflow
containerStyle={styles.root}
automaticallyAdjustKeyboardInsets
automaticallyAdjustsScrollIndicatorInsets
contentInsetAdjustmentBehavior="automatic"
automaticallyAdjustContentInsets
useNativeDriver={true}
data={data}
containerStyle={[{ backgroundColor: colors.background }, styles.root]}
keyExtractor={keyExtractor}
onReordered={onReordered}
renderItem={renderItem}
ref={listRef}
/>
</NestableScrollContainer>
</>
</GestureHandlerRootView>
);
};

View File

@ -50,6 +50,7 @@ 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';
type RouteParams = RouteProp<ViewEditMultisigCosignersStackParamList, 'ViewEditMultisigCosigners'>;
type NavigationProp = NativeStackNavigationProp<ViewEditMultisigCosignersStackParamList, 'ViewEditMultisigCosigners'>;
@ -78,6 +79,7 @@ const ViewEditMultisigCosigners: React.FC = () => {
const [exportStringURv2, setExportStringURv2] = useState(''); // used in QR
const [exportFilename, setExportFilename] = useState('bw-cosigner.json');
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[]>();
/* discardChangesRef is only so the action sheet can be shown on mac catalyst when a
@ -275,6 +277,24 @@ const ViewEditMultisigCosigners: React.FC = () => {
);
};
const resetModalData = () => {
setVaultKeyData({
keyIndex: 1,
xpub: '',
seed: '',
passphrase: '',
path: '',
fp: '',
isLoading: false,
});
setImportText('');
setExportString('{}');
setExportStringURv2('');
setExportFilename('');
setIsSaveButtonDisabled(false);
setAskPassphrase(false);
};
const _renderKeyItem = (el: ListRenderItemInfo<any>) => {
if (!wallet) {
// failsafe
@ -312,29 +332,34 @@ const ViewEditMultisigCosigners: React.FC = () => {
buttonType: MultipleStepsListItemButtohType.partial,
leftText,
text: loc.multisig.view,
showActivityIndicator: isVaultKeyIndexDataLoading === el.index + 1,
disabled: vaultKeyData.isLoading,
onPress: () => {
const keyIndex = el.index + 1;
const xpub = wallet.getCosigner(keyIndex);
const fp = wallet.getFingerprint(keyIndex);
const path = wallet.getCustomDerivationPathForCosigner(keyIndex);
if (!path) {
presentAlert({ message: 'Cannot find derivation path for this cosigner' });
return;
}
setVaultKeyData({
keyIndex,
seed: '',
passphrase: '',
xpub,
fp,
path,
isLoading: false,
});
setExportString(MultisigCosigner.exportToJson(fp, xpub, path));
setExportStringURv2(encodeUR(MultisigCosigner.exportToJson(fp, xpub, path))[0]);
setExportFilename('bw-cosigner-' + fp + '.json');
mnemonicsModalRef.current?.present();
setIsVaultKeyIndexDataLoading(el.index + 1);
setTimeout(() => {
const keyIndex = el.index + 1;
const xpub = wallet.getCosigner(keyIndex);
const fp = wallet.getFingerprint(keyIndex);
const path = wallet.getCustomDerivationPathForCosigner(keyIndex);
if (!path) {
presentAlert({ message: 'Cannot find derivation path for this cosigner' });
return;
}
setVaultKeyData({
keyIndex,
seed: '',
passphrase: '',
xpub,
fp,
path,
isLoading: false,
});
setExportString(MultisigCosigner.exportToJson(fp, xpub, path));
setExportStringURv2(encodeUR(MultisigCosigner.exportToJson(fp, xpub, path))[0]);
setExportFilename('bw-cosigner-' + fp + '.json');
mnemonicsModalRef.current?.present();
setIsVaultKeyIndexDataLoading(undefined);
}, 100);
},
}}
dashes={MultipleStepsListItemDashType.topAndBottom}
@ -363,31 +388,36 @@ const ViewEditMultisigCosigners: React.FC = () => {
leftText,
text: loc.multisig.view,
disabled: vaultKeyData.isLoading,
showActivityIndicator: isVaultKeyIndexDataLoading === el.index + 1,
buttonType: MultipleStepsListItemButtohType.partial,
onPress: () => {
const keyIndex = el.index + 1;
const seed = wallet.getCosigner(keyIndex);
const passphrase = wallet.getCosignerPassphrase(keyIndex);
setVaultKeyData({
keyIndex,
seed,
xpub: '',
fp: '',
path: '',
passphrase: passphrase ?? '',
isLoading: false,
});
mnemonicsModalRef.current?.present();
const fp = wallet.getFingerprint(keyIndex);
const path = wallet.getCustomDerivationPathForCosigner(keyIndex);
if (!path) {
presentAlert({ message: 'Cannot find derivation path for this cosigner' });
return;
}
const xpub = wallet.convertXpubToMultisignatureXpub(MultisigHDWallet.seedToXpub(seed, path, passphrase));
setExportString(MultisigCosigner.exportToJson(fp, xpub, path));
setExportStringURv2(encodeUR(MultisigCosigner.exportToJson(fp, xpub, path))[0]);
setExportFilename('bw-cosigner-' + fp + '.json');
setIsVaultKeyIndexDataLoading(el.index + 1);
setTimeout(() => {
const keyIndex = el.index + 1;
const seed = wallet.getCosigner(keyIndex);
const passphrase = wallet.getCosignerPassphrase(keyIndex);
setVaultKeyData({
keyIndex,
seed,
xpub: '',
fp: '',
path: '',
passphrase: passphrase ?? '',
isLoading: false,
});
const fp = wallet.getFingerprint(keyIndex);
const path = wallet.getCustomDerivationPathForCosigner(keyIndex);
if (!path) {
presentAlert({ message: 'Cannot find derivation path for this cosigner' });
return;
}
const xpub = wallet.convertXpubToMultisignatureXpub(MultisigHDWallet.seedToXpub(seed, path, passphrase));
setExportString(MultisigCosigner.exportToJson(fp, xpub, path));
setExportStringURv2(encodeUR(MultisigCosigner.exportToJson(fp, xpub, path))[0]);
setExportFilename('bw-cosigner-' + fp + '.json');
mnemonicsModalRef.current?.present();
setIsVaultKeyIndexDataLoading(undefined);
}, 100);
},
}}
dashes={MultipleStepsListItemDashType.topAndBottom}
@ -442,6 +472,7 @@ const ViewEditMultisigCosigners: React.FC = () => {
await provideMnemonicsModalRef.current?.dismiss();
await shareModalRef.current?.dismiss();
await mnemonicsModalRef.current?.dismiss();
resetModalData();
};
const handleUseMnemonicPhrase = async () => {
let passphrase;
@ -478,9 +509,7 @@ const ViewEditMultisigCosigners: React.FC = () => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setWallet(wallet);
provideMnemonicsModalRef.current?.dismiss();
setIsSaveButtonDisabled(false);
setImportText('');
setAskPassphrase(false);
resetModalData();
};
const xpubInsteadOfSeed = (index: number): Promise<void> => {
@ -517,8 +546,7 @@ const ViewEditMultisigCosigners: React.FC = () => {
const hideProvideMnemonicsModal = () => {
Keyboard.dismiss();
provideMnemonicsModalRef.current?.dismiss();
setImportText('');
setAskPassphrase(false);
resetModalData();
};
const hideShareModal = () => {};
@ -583,13 +611,15 @@ const ViewEditMultisigCosigners: React.FC = () => {
backgroundColor={colors.elevated}
shareContent={{ fileName: exportFilename, fileContent: exportString }}
>
<View style={styles.alignItemsCenter}>
<Text style={[styles.headerText, stylesHook.textDestination]}>
{loc.multisig.this_is_cosigners_xpub} {Platform.OS === 'ios' ? loc.multisig.this_is_cosigners_xpub_airdrop : ''}
</Text>
<BlueSpacing20 />
<QRCodeComponent value={exportStringURv2} size={260} isLogoRendered={false} />
</View>
<SafeArea>
<View style={styles.alignItemsCenter}>
<Text style={[styles.headerText, stylesHook.textDestination]}>
{loc.multisig.this_is_cosigners_xpub} {Platform.OS === 'ios' ? loc.multisig.this_is_cosigners_xpub_airdrop : ''}
</Text>
<BlueSpacing20 />
<QRCodeComponent value={exportStringURv2} size={260} isLogoRendered={false} />
</View>
</SafeArea>
</BottomModal>
);
};

View File

@ -38,6 +38,10 @@ import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
import { useSettings } from '../../hooks/context/useSettings';
import { isDesktop } from '../../blue_modules/environment';
import { useKeyboard } from '../../hooks/useKeyboard';
import {
DoneAndDismissKeyboardInputAccessory,
DoneAndDismissKeyboardInputAccessoryViewID,
} from '../../components/DoneAndDismissKeyboardInputAccessory';
const staticCache = {};
@ -684,7 +688,16 @@ const WalletsAddMultisigStep2 = () => {
<BlueTextCentered>{loc.multisig.type_your_mnemonics}</BlueTextCentered>
<BlueSpacing20 />
<View style={styles.multiLineTextInput}>
<BlueFormMultiInput value={importText} onChangeText={setImportText} />
<BlueFormMultiInput
value={importText}
onChangeText={setImportText}
inputAccessoryViewID={DoneAndDismissKeyboardInputAccessoryViewID}
/>
{Platform.select({
ios: <DoneAndDismissKeyboardInputAccessory />,
android: isVisible && <DoneAndDismissKeyboardInputAccessory />,
})}
<BlueSpacing20 />
</View>
</BottomModal>

View File

@ -1,46 +0,0 @@
--- ../node_modules/react-native-camera-kit/android/src/main/java/com/rncamerakit/CKCamera.kt 2023-11-10 11:25:36
+++ ../node_modules/react-native-camera-kit/android/src/main/java/com/rncamerakit/CKCamera.kt 2023-11-10 11:25:42
@@ -180,7 +180,7 @@
orientationListener!!.enable()
val scaleDetector = ScaleGestureDetector(context, object: ScaleGestureDetector.SimpleOnScaleGestureListener() {
- override fun onScale(detector: ScaleGestureDetector?): Boolean {
+ override fun onScale(detector: ScaleGestureDetector): Boolean {
if (zoomMode == "off") return true
val cameraControl = camera?.cameraControl ?: return true
val zoom = camera?.cameraInfo?.zoomState?.value?.zoomRatio ?: return true
--- ../node_modules/react-native-camera-kit/dist/CameraScreen.js 2024-09-01 13:00:57
+++ ../node_modules/react-native-camera-kit/dist/CameraScreen.js 2024-09-01 13:00:46
@@ -61,14 +61,14 @@
</TouchableOpacity>));
}
renderTorchButton() {
- return (!this.isCaptureRetakeMode() && (<TouchableOpacity style={{ paddingHorizontal: 15 }} onPress={() => this.onSetTorch()}>
- <Image style={[{ flex: 1, justifyContent: 'center' }, this.props.torchImageStyle]} source={this.state.torchMode ? this.props.torchOnImage : this.props.torchOffImage} resizeMode="contain"/>
+ return (!this.isCaptureRetakeMode() && (<TouchableOpacity style={{ backgroundColor: '#FFFFFF', borderRadius: 20, height: 40, marginHorizontal: 15 }} onPress={() => this.onSetTorch()}>
+ <Image style={[{ width: 40, height: 40, justifyContent: 'center' }, this.props.torchImageStyle]} source={this.state.torchMode ? this.props.torchOnImage : this.props.torchOffImage} resizeMode="contain"/>
</TouchableOpacity>));
}
renderSwitchCameraButton() {
return (this.props.cameraFlipImage &&
- !this.isCaptureRetakeMode() && (<TouchableOpacity style={{ paddingHorizontal: 15 }} onPress={() => this.onSwitchCameraPressed()}>
- <Image style={{ flex: 1, justifyContent: 'center' }} source={this.props.cameraFlipImage} resizeMode="contain"/>
+ !this.isCaptureRetakeMode() && (<TouchableOpacity style={{ }} onPress={() => this.onSwitchCameraPressed()}>
+ <Image style={{ width: 40, height: 40, justifyContent: 'center' }} source={this.props.cameraFlipImage} resizeMode="contain"/>
</TouchableOpacity>));
}
renderTopButtons() {
\ No newline at end of file
@@ -228,8 +228,8 @@
flex: 1,
flexDirection: 'row',
justifyContent: 'space-between',
- paddingTop: 8,
- paddingBottom: 0,
+ paddingTop:44,
+ paddingHorizontal: 16,
},
cameraContainer: Object.assign({}, Platform.select({
android: {
\ No newline at end of file

View File

@ -87,17 +87,17 @@ const icons = {
ScanQR: { iconValue: Platform.OS === 'ios' ? 'qrcode.viewfinder' : 'ic_menu_camera' },
ChoosePhoto: { iconValue: Platform.OS === 'ios' ? 'photo.on.rectangle' : 'ic_menu_gallery' },
Clipboard: { iconValue: Platform.OS === 'ios' ? 'document.on.clipboard' : 'ic_menu_file' },
ExportPrivateKey: { iconValue: 'key' },
Share: { iconValue: 'square.and.arrow.up' },
Signature: { iconValue: 'signature' },
PasteFromClipboard: { iconValue: 'document.on.clipboard' },
ImportFile: { iconValue: 'document.viewfinder' },
Hide: { iconValue: 'eye.slash' },
ClearClipboard: { iconValue: 'clipboard' },
SortASC: { iconValue: 'arrow.down.to.line' },
SortDESC: { iconValue: 'arrow.up.to.line' },
SaveFile: { iconValue: 'square.and.arrow.down' },
Delete: { iconValue: 'trash' },
ExportPrivateKey: { iconValue: Platform.OS === 'ios' ? 'key' : 'ic_lock_idle_lock' },
Share: { iconValue: Platform.OS === 'ios' ? 'square.and.arrow.up' : 'ic_menu_share' },
Signature: { iconValue: Platform.OS === 'ios' ? 'signature' : 'ic_menu_edit' },
PasteFromClipboard: { iconValue: Platform.OS === 'ios' ? 'document.on.clipboard' : 'ic_menu_paste' },
ImportFile: { iconValue: Platform.OS === 'ios' ? 'document.viewfinder' : 'ic_menu_upload' },
Hide: { iconValue: Platform.OS === 'ios' ? 'eye.slash' : 'ic_menu_close_clear_cancel' },
ClearClipboard: { iconValue: Platform.OS === 'ios' ? 'clipboard' : 'ic_menu_delete' },
SortASC: { iconValue: Platform.OS === 'ios' ? 'arrow.down.to.line' : 'ic_menu_sort_alphabetically' },
SortDESC: { iconValue: Platform.OS === 'ios' ? 'arrow.up.to.line' : 'ic_menu_sort_by_size' },
SaveFile: { iconValue: Platform.OS === 'ios' ? 'square.and.arrow.down' : 'ic_menu_save' },
Delete: { iconValue: Platform.OS === 'ios' ? 'trash' : 'ic_menu_delete' },
} as const;
export type ToolTipAction = {