Merge branch 'master' into search

This commit is contained in:
Marcos Rodriguez Velez 2024-07-26 17:59:35 -04:00
commit fde4bc4f4b
No known key found for this signature in database
GPG Key ID: 6030B2F48CCE86D7
33 changed files with 3546 additions and 20019 deletions

View File

@ -34,7 +34,7 @@ jobs:
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '11'
java-version: '17'
cache: 'gradle'
- name: Install node_modules

View File

@ -66,9 +66,7 @@ jobs:
- name: Free Disk Space
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: false
tool-cache: true
android: false
dotnet: true
haskell: true
@ -141,7 +139,7 @@ jobs:
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-back none -camera-front none -partition-size 2047
arch: x86_64
script: npm run e2e:release-test -- --record-videos all --record-logs all --take-screenshots all --headless -d 200000 -R 3 --artifacts-location /mnt/artifacts
script: npm run e2e:release-test -- --record-videos all --record-logs all --take-screenshots all --headless -d 200000 -R 5 --artifacts-location /mnt/artifacts
- uses: actions/upload-artifact@v4
if: failure()

View File

@ -18,19 +18,19 @@ GEM
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.0)
aws-partitions (1.948.0)
aws-sdk-core (3.199.0)
aws-partitions (1.950.0)
aws-sdk-core (3.201.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.87.0)
aws-sdk-core (~> 3, >= 3.199.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.154.0)
aws-sdk-core (~> 3, >= 3.199.0)
aws-sdk-kms (1.88.0)
aws-sdk-core (~> 3, >= 3.201.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.156.0)
aws-sdk-core (~> 3, >= 3.201.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.8)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.8.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
@ -210,7 +210,7 @@ GEM
base64
mini_magick (4.13.1)
mini_mime (1.1.5)
minitest (5.24.0)
minitest (5.24.1)
molinillo (0.8.0)
multi_json (1.15.0)
multipart-post (2.4.1)

View File

@ -2,9 +2,9 @@ import { createNavigationContainerRef, NavigationAction, ParamListBase, StackAct
export const navigationRef = createNavigationContainerRef<ParamListBase>();
export function navigate(name: string, params?: ParamListBase) {
export function navigate(name: string, params?: ParamListBase, options?: { merge: boolean }) {
if (navigationRef.isReady()) {
navigationRef.current?.navigate(name, params);
navigationRef.current?.navigate({ name, params, merge: options?.merge });
}
}
@ -14,6 +14,10 @@ export function dispatch(action: NavigationAction) {
}
}
export function navigateToWalletsList() {
navigate('WalletsList');
}
export function reset() {
if (navigationRef.isReady()) {
navigationRef.current?.reset({

View File

@ -84,6 +84,11 @@ android {
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}
lintOptions {
abortOnError false
checkReleaseBuilds false
}
sourceSets {
main {
assets.srcDirs = ['src/main/assets', 'src/main/res/assets']
@ -125,7 +130,6 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'com.google.android.material:material:1.4.0'
}
apply plugin: 'com.google.gms.google-services' // Google Services plugin

View File

@ -4,9 +4,9 @@ buildscript {
ext {
minSdkVersion = 24
supportLibVersion = "28.0.0"
buildToolsVersion = "34.0.0"
compileSdkVersion = 33
targetSdkVersion = 33
buildToolsVersion = "33.0.0"
compileSdkVersion = 34
targetSdkVersion = 34
googlePlayServicesVersion = "16.+"
googlePlayServicesIidVersion = "16.0.1"
firebaseVersion = "17.3.4"
@ -15,7 +15,7 @@ buildscript {
// We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP.
ndkVersion = "23.1.7779620"
kotlin_version = '1.9.25'
kotlinVersion = '1.9.23'
kotlinVersion = '1.9.25'
}
repositories {
google()
@ -23,11 +23,11 @@ buildscript {
}
dependencies {
classpath("com.android.tools.build:gradle")
classpath("com.facebook.react:react-native-gradle-plugin")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
classpath("com.bugsnag:bugsnag-android-gradle-plugin:5.+")
classpath 'com.google.gms:google-services:4.4.2' // Google Services plugin
classpath("com.facebook.react:react-native-gradle-plugin")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
}
}
@ -56,14 +56,19 @@ allprojects {
google()
maven { url 'https://www.jitpack.io' }
}
configurations.all {
resolutionStrategy {
force 'androidx.activity:activity:1.5.1'
}
}
}
subprojects {
afterEvaluate {project ->
if (project.hasProperty("android")) {
android {
buildToolsVersion "34.0.0"
compileSdkVersion 33
buildToolsVersion "33.0.0"
compileSdkVersion 34
defaultConfig {
minSdkVersion 24
}
@ -78,4 +83,4 @@ subprojects {
}
}
}
}

View File

@ -10,7 +10,7 @@
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit

View File

@ -89,7 +89,7 @@ const defaultPeer = { host: 'electrum1.bluewallet.io', ssl: '443' };
export const hardcodedPeers: Peer[] = [
{ host: 'mainnet.foundationdevices.com', ssl: '50002' },
{ host: 'bitcoin.lukechilds.co', ssl: '50002' },
{ host: 'electrum.jochen-hoenicke.de', ssl: '50006' },
// { host: 'electrum.jochen-hoenicke.de', ssl: '50006' },
{ host: 'electrum1.bluewallet.io', ssl: '443' },
{ host: 'electrum.acinq.co', ssl: '50002' },
{ host: 'electrum.bitaroo.net', ssl: '50002' },
@ -887,7 +887,7 @@ export async function multiGetTransactionByTxid<T extends boolean>(
const tx = ret[txid];
// dont cache immature txs, but only for 'verbose', since its fully decoded tx jsons. non-verbose are just plain
// strings txhex
if (verbose && typeof tx !== 'string' && (!tx.confirmations || tx.confirmations < 7)) {
if (verbose && typeof tx !== 'string' && (!tx?.confirmations || tx.confirmations < 7)) {
continue;
}

View File

@ -1,5 +1,4 @@
import { Alert as RNAlert } from 'react-native';
import { Alert as RNAlert, Platform, ToastAndroid } from 'react-native';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../blue_modules/hapticFeedback';
import loc from '../loc';
@ -21,7 +20,14 @@ const presentAlert = ({
if (hapticFeedback) {
triggerHapticFeedback(hapticFeedback);
}
if (Platform.OS !== 'android') {
type = AlertType.Alert;
}
switch (type) {
case AlertType.Toast:
ToastAndroid.showWithGravity(message, ToastAndroid.LONG, ToastAndroid.BOTTOM);
break;
default:
RNAlert.alert(title ?? loc.alert.default, message);
break;

View File

@ -1,86 +1,130 @@
import React from 'react';
import { Platform, StyleSheet, useWindowDimensions, View } from 'react-native';
import Modal from 'react-native-modal';
import React, { forwardRef, useImperativeHandle, useRef, ReactElement, ComponentType, JSXElementConstructor, ReactNode } from 'react';
import { SheetSize, SizeInfo, TrueSheet, TrueSheetProps } from '@lodev09/react-native-true-sheet';
import { Keyboard, StyleSheet, View, TouchableOpacity, Platform, Image } from 'react-native';
import { BlueSpacing10 } from '../BlueComponents';
import loc from '../loc';
import Button from './Button';
import { useTheme } from './themes';
interface BottomModalProps extends TrueSheetProps {
children?: React.ReactNode;
onClose?: () => void;
name?: string;
isGrabberVisible?: boolean;
sizes?: SheetSize[] | undefined;
footer?: ReactElement | ComponentType<any>;
footerDefaultMargins?: boolean | number;
onPresent?: () => void;
onSizeChange?: (size: SizeInfo) => void;
showCloseButton?: boolean;
}
export interface BottomModalHandle {
present: () => Promise<void>;
dismiss: () => Promise<void>;
}
const styles = StyleSheet.create({
root: {
justifyContent: 'flex-end',
margin: 0,
footerContainer: {
alignItems: 'center',
justifyContent: 'center',
},
hasDoneButton: {
padding: 16,
paddingBottom: 24,
buttonContainer: {
position: 'absolute',
backgroundColor: 'lightgray',
justifyContent: 'center',
alignItems: 'center',
width: 30,
height: 30,
borderRadius: 15,
top: 20,
right: 20,
zIndex: 10,
},
});
interface BottomModalProps {
children?: React.ReactNode;
onBackButtonPress?: () => void;
onBackdropPress?: () => void;
onClose: () => void;
windowHeight?: number;
windowWidth?: number;
doneButton?: boolean;
avoidKeyboard?: boolean;
allowBackdropPress?: boolean;
isVisible: boolean;
coverScreen?: boolean;
propagateSwipe?: boolean;
}
const BottomModal: React.FC<BottomModalProps> = ({
onBackButtonPress,
onBackdropPress,
onClose,
windowHeight,
windowWidth,
doneButton,
isVisible,
avoidKeyboard = false,
allowBackdropPress = true,
coverScreen = true,
propagateSwipe,
...props
}) => {
const { height: valueWindowHeight, width: valueWindowWidth } = useWindowDimensions();
const handleBackButtonPress = onBackButtonPress ?? onClose;
const handleBackdropPress = allowBackdropPress ? onBackdropPress ?? onClose : undefined;
const { colors } = useTheme();
const stylesHook = StyleSheet.create({
hasDoneButton: {
backgroundColor: colors.elevated,
const BottomModal = forwardRef<BottomModalHandle, BottomModalProps>(
(
{
name,
onClose,
onPresent,
onSizeChange,
showCloseButton = true,
isGrabberVisible = true,
sizes = ['auto'],
footer,
footerDefaultMargins,
children,
...props
},
});
ref,
) => {
const trueSheetRef = useRef<TrueSheet>(null);
return (
<Modal
propagateSwipe={propagateSwipe}
style={styles.root}
deviceHeight={windowHeight ?? valueWindowHeight}
deviceWidth={windowWidth ?? valueWindowWidth}
onBackButtonPress={handleBackButtonPress}
onBackdropPress={handleBackdropPress}
isVisible={isVisible}
coverScreen={coverScreen}
{...props}
accessibilityViewIsModal
avoidKeyboard={avoidKeyboard}
useNativeDriverForBackdrop={Platform.OS === 'android'}
>
{props.children}
{doneButton && (
<View style={[styles.hasDoneButton, stylesHook.hasDoneButton]}>
<Button title={loc.send.input_done} onPress={onClose} testID="ModalDoneButton" />
<BlueSpacing10 />
</View>
)}
</Modal>
);
};
useImperativeHandle(ref, () => ({
present: async () => {
Keyboard.dismiss();
if (trueSheetRef.current?.present) {
await trueSheetRef.current.present();
} else {
return Promise.resolve();
}
},
dismiss: async () => {
Keyboard.dismiss();
if (trueSheetRef.current?.dismiss) {
await trueSheetRef.current.dismiss();
} else {
return Promise.resolve();
}
},
}));
const dismiss = () => {
trueSheetRef.current?.dismiss();
};
const renderTopRightButton = () =>
showCloseButton ? (
<TouchableOpacity style={styles.buttonContainer} onPress={dismiss} testID="ModalDoneButton">
<Image source={require('../img/close.png')} width={20} height={20} />
</TouchableOpacity>
) : null;
const renderFooter = (): ReactElement<any, string | JSXElementConstructor<any>> | ComponentType<unknown> | undefined => {
// Footer is not working correctly on Android yet.
if (!footer) return undefined;
if (React.isValidElement(footer)) {
return footerDefaultMargins ? <View style={styles.footerContainer}>{footer}</View> : footer;
} else if (typeof footer === 'function') {
// Render the footer component dynamically
const FooterComponent = footer as ComponentType<any>;
return <FooterComponent />;
}
return undefined;
};
const FooterComponent = Platform.OS !== 'android' && renderFooter();
return (
<TrueSheet
ref={trueSheetRef}
cornerRadius={24}
sizes={sizes}
onDismiss={onClose}
onPresent={onPresent}
onSizeChange={onSizeChange}
grabber={isGrabberVisible}
// Footer is not working correctly on Android yet.
FooterComponent={FooterComponent as ReactElement}
{...props}
blurTint="regular"
>
{children}
{renderTopRightButton()}
{Platform.OS === 'android' && (renderFooter() as ReactNode)}
</TrueSheet>
);
},
);
export default BottomModal;

View File

@ -231,7 +231,7 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
<TouchableOpacity style={styles.walletPreferredUnitView} onPress={changeWalletBalanceUnit}>
<Text style={styles.walletPreferredUnitText}>
{wallet.getPreferredBalanceUnit() === BitcoinUnit.LOCAL_CURRENCY
? preferredFiatCurrency?.endPointKey ?? FiatUnit.USD
? (preferredFiatCurrency?.endPointKey ?? FiatUnit.USD)
: wallet.getPreferredBalanceUnit()}
</Text>
</TouchableOpacity>

View File

@ -23,6 +23,8 @@ import { useIsLargeScreen } from '../hooks/useIsLargeScreen';
import loc, { formatBalance, transactionTimeToReadable } from '../loc';
import { BlurredBalanceView } from './BlurredBalanceView';
import { useTheme } from './themes';
import { useStorage } from '../hooks/context/useStorage';
import { WalletTransactionsStatus } from './Context/StorageProvider';
import { Transaction, TWallet } from '../class/wallets/types';
interface NewWalletPanelProps {
@ -103,128 +105,9 @@ interface WalletCarouselItemProps {
customStyle?: ViewStyle;
horizontal?: boolean;
isActive?: boolean;
searchQuery?: string;
renderHighlightedText?: (text: string, query: string) => JSX.Element;
allowOnPressAnimation?: boolean;
}
export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = ({
item,
onPress,
handleLongPress,
isSelectedWallet,
customStyle,
horizontal,
isActive,
searchQuery,
renderHighlightedText,
}) => {
const scaleValue = useRef(new Animated.Value(1.0)).current;
const { colors } = useTheme();
const { width } = useWindowDimensions();
const itemWidth = width * 0.82 > 375 ? 375 : width * 0.82;
const isLargeScreen = useIsLargeScreen();
const onPressedIn = useCallback(() => {
Animated.spring(scaleValue, {
toValue: 0.95,
useNativeDriver: true,
friction: 3,
tension: 100,
}).start();
}, [scaleValue]);
const onPressedOut = useCallback(() => {
Animated.spring(scaleValue, {
toValue: 1.0,
useNativeDriver: true,
friction: 3,
tension: 100,
}).start();
}, [scaleValue]);
const handlePress = useCallback(() => {
onPressedOut();
onPress(item);
}, [item, onPress, onPressedOut]);
const opacity = isSelectedWallet === false ? 0.5 : 1.0;
let image;
switch (item.type) {
case LightningLdkWallet.type:
case LightningCustodianWallet.type:
image = I18nManager.isRTL ? require('../img/lnd-shape-rtl.png') : require('../img/lnd-shape.png');
break;
case MultisigHDWallet.type:
image = I18nManager.isRTL ? require('../img/vault-shape-rtl.png') : require('../img/vault-shape.png');
break;
default:
image = I18nManager.isRTL ? require('../img/btc-shape-rtl.png') : require('../img/btc-shape.png');
}
const latestTransactionText =
item.getBalance() !== 0 && item.getLatestTransactionTime() === 0
? loc.wallets.pull_to_refresh
: item.getTransactions().find((tx: Transaction) => tx.confirmations === 0)
? loc.transactions.pending
: transactionTimeToReadable(item.getLatestTransactionTime());
const balance = !item.hideBalance && formatBalance(Number(item.getBalance()), item.getPreferredBalanceUnit(), true);
return (
<Animated.View
style={[
isLargeScreen || !horizontal ? [iStyles.rootLargeDevice, customStyle] : (customStyle ?? { ...iStyles.root, width: itemWidth }),
{ opacity, transform: [{ scale: scaleValue }] },
]}
>
<Pressable
accessibilityRole="button"
testID={item.getLabel()}
onPressIn={onPressedIn}
onPressOut={onPressedOut}
onLongPress={() => {
if (handleLongPress) handleLongPress();
}}
onPress={handlePress}
>
<View style={[iStyles.shadowContainer, { backgroundColor: colors.background, shadowColor: colors.shadowColor }]}>
<LinearGradient colors={WalletGradient.gradientsFor(item.type)} style={iStyles.grad}>
<Image source={image} style={iStyles.image} />
<Text style={iStyles.br} />
<Text numberOfLines={1} style={[iStyles.label, { color: colors.inverseForegroundColor }]}>
{renderHighlightedText && searchQuery ? renderHighlightedText(item.getLabel(), searchQuery) : item.getLabel()}
</Text>
<View style={iStyles.balanceContainer}>
{item.hideBalance ? (
<>
<BlueSpacing10 />
<BlurredBalanceView />
</>
) : (
<Text
numberOfLines={1}
adjustsFontSizeToFit
key={`${balance}`} // force component recreation on balance change. To fix right-to-left languages, like Farsi
style={[iStyles.balance, { color: colors.inverseForegroundColor }]}
>
{`${balance} `}
</Text>
)}
</View>
<Text style={iStyles.br} />
<Text numberOfLines={1} style={[iStyles.latestTx, { color: colors.inverseForegroundColor }]}>
{loc.wallets.list_latest_transaction}
</Text>
<Text numberOfLines={1} style={[iStyles.latestTxTime, { color: colors.inverseForegroundColor }]}>
{latestTransactionText}
</Text>
</LinearGradient>
</View>
</Pressable>
</Animated.View>
);
};
const iStyles = StyleSheet.create({
root: { paddingRight: 20 },
rootLargeDevice: { marginVertical: 20 },
@ -284,6 +167,119 @@ const iStyles = StyleSheet.create({
},
});
export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
({ item, onPress, handleLongPress, isSelectedWallet, customStyle, horizontal, allowOnPressAnimation = true }) => {
const scaleValue = useRef(new Animated.Value(1.0)).current;
const { colors } = useTheme();
const { walletTransactionUpdateStatus } = useStorage();
const { width } = useWindowDimensions();
const itemWidth = width * 0.82 > 375 ? 375 : width * 0.82;
const isLargeScreen = useIsLargeScreen();
const onPressedIn = useCallback(() => {
Animated.spring(scaleValue, {
toValue: 0.95,
useNativeDriver: true,
friction: 3,
tension: 100,
}).start();
}, [scaleValue]);
const onPressedOut = useCallback(() => {
Animated.spring(scaleValue, {
toValue: 1.0,
useNativeDriver: true,
friction: 3,
tension: 100,
}).start();
}, [scaleValue]);
const handlePress = useCallback(() => {
onPressedOut();
onPress(item);
}, [item, onPress, onPressedOut]);
const opacity = isSelectedWallet === false ? 0.5 : 1.0;
let image;
switch (item.type) {
case LightningLdkWallet.type:
case LightningCustodianWallet.type:
image = I18nManager.isRTL ? require('../img/lnd-shape-rtl.png') : require('../img/lnd-shape.png');
break;
case MultisigHDWallet.type:
image = I18nManager.isRTL ? require('../img/vault-shape-rtl.png') : require('../img/vault-shape.png');
break;
default:
image = I18nManager.isRTL ? require('../img/btc-shape-rtl.png') : require('../img/btc-shape.png');
}
const latestTransactionText =
walletTransactionUpdateStatus === WalletTransactionsStatus.ALL || walletTransactionUpdateStatus === item.getID()
? loc.transactions.updating
: item.getBalance() !== 0 && item.getLatestTransactionTime() === 0
? loc.wallets.pull_to_refresh
: item.getTransactions().find((tx: Transaction) => tx.confirmations === 0)
? loc.transactions.pending
: transactionTimeToReadable(item.getLatestTransactionTime());
const balance = !item.hideBalance && formatBalance(Number(item.getBalance()), item.getPreferredBalanceUnit(), true);
return (
<Animated.View
style={[
isLargeScreen || !horizontal ? [iStyles.rootLargeDevice, customStyle] : (customStyle ?? { ...iStyles.root, width: itemWidth }),
{ opacity, transform: [{ scale: scaleValue }] },
]}
>
<Pressable
accessibilityRole="button"
testID={item.getLabel()}
onPressIn={allowOnPressAnimation ? onPressedIn : undefined}
onPressOut={allowOnPressAnimation ? onPressedOut : undefined}
onLongPress={() => {
if (handleLongPress) handleLongPress();
}}
onPress={handlePress}
>
<View style={[iStyles.shadowContainer, { backgroundColor: colors.background, shadowColor: colors.shadowColor }]}>
<LinearGradient colors={WalletGradient.gradientsFor(item.type)} style={iStyles.grad}>
<Image source={image} style={iStyles.image} />
<Text style={iStyles.br} />
<Text numberOfLines={1} style={[iStyles.label, { color: colors.inverseForegroundColor }]}>
{item.getLabel()}
</Text>
<View style={iStyles.balanceContainer}>
{item.hideBalance ? (
<>
<BlueSpacing10 />
<BlurredBalanceView />
</>
) : (
<Text
numberOfLines={1}
adjustsFontSizeToFit
key={`${balance}`} // force component recreation on balance change. To fix right-to-left languages, like Farsi
style={[iStyles.balance, { color: colors.inverseForegroundColor }]}
>
{`${balance} `}
</Text>
)}
</View>
<Text style={iStyles.br} />
<Text numberOfLines={1} style={[iStyles.latestTx, { color: colors.inverseForegroundColor }]}>
{loc.wallets.list_latest_transaction}
</Text>
<Text numberOfLines={1} style={[iStyles.latestTxTime, { color: colors.inverseForegroundColor }]}>
{latestTransactionText}
</Text>
</LinearGradient>
</View>
</Pressable>
</Animated.View>
);
},
);
interface WalletsCarouselProps extends Partial<FlatListProps<any>> {
horizontal?: boolean;
selectedWallet?: string;
@ -292,8 +288,6 @@ interface WalletsCarouselProps extends Partial<FlatListProps<any>> {
handleLongPress?: () => void;
data: TWallet[];
scrollEnabled?: boolean;
renderHighlightedText?: (text: string, query: string) => JSX.Element;
searchQuery?: string;
}
type FlatListRefType = FlatList<any> & {
@ -322,17 +316,7 @@ const cStyles = StyleSheet.create({
const ListHeaderComponent: React.FC = () => <View style={cStyles.separatorStyle} />;
const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props, ref) => {
const {
horizontal,
data,
handleLongPress,
onPress,
selectedWallet,
scrollEnabled,
onNewWalletPress,
searchQuery,
renderHighlightedText,
} = props;
const { horizontal, data, handleLongPress, onPress, selectedWallet, scrollEnabled, onNewWalletPress } = props;
const renderItem = useCallback(
({ item, index }: ListRenderItemInfo<TWallet>) =>
item ? (
@ -342,11 +326,9 @@ const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props
handleLongPress={handleLongPress}
onPress={onPress}
horizontal={horizontal}
searchQuery={searchQuery}
renderHighlightedText={renderHighlightedText}
/>
) : null,
[horizontal, selectedWallet, handleLongPress, onPress, searchQuery, renderHighlightedText],
[horizontal, selectedWallet, handleLongPress, onPress],
);
const flatListRef = useRef<FlatList<any>>(null);
@ -420,8 +402,6 @@ const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props
handleLongPress={handleLongPress}
onPress={onPress}
key={index}
searchQuery={props.searchQuery}
renderHighlightedText={props.renderHighlightedText}
/>
) : null,
)}

View File

@ -1,6 +1,7 @@
import { Platform } from 'react-native';
import { check, request, PERMISSIONS, RESULTS } from 'react-native-permissions';
import { navigationRef } from '../NavigationService';
/**
* Helper function that navigates to ScanQR screen, and returns promise that will resolve with the result of a scan,
* and then navigates back. If QRCode scan was closed, promise resolves to null.
@ -37,9 +38,13 @@ function scanQrHelper(
params = { launchedBy: currentScreenName, showFileImportButton: Boolean(showFileImportButton) };
}
navigationRef.navigate('ScanQRCodeRoot', {
screen: 'ScanQRCode',
params,
navigationRef.navigate({
name: 'ScanQRCodeRoot',
params: {
screen: 'ScanQRCode',
params,
},
merge: true,
});
});
});

View File

@ -15,7 +15,11 @@ export const useExtendedNavigation = <T extends NavigationProp<ParamListBase>>()
const { wallets, saveToDisk } = useStorage();
const { isBiometricUseEnabled } = useBiometrics();
const enhancedNavigate: NavigationProp<ParamListBase>['navigate'] = (screenOrOptions: any, params?: any) => {
const enhancedNavigate: NavigationProp<ParamListBase>['navigate'] = (
screenOrOptions: any,
params?: any,
options?: { merge?: boolean },
) => {
let screenName: string;
if (typeof screenOrOptions === 'string') {
screenName = screenOrOptions;
@ -32,9 +36,11 @@ export const useExtendedNavigation = <T extends NavigationProp<ParamListBase>>()
const proceedWithNavigation = () => {
console.log('Proceeding with navigation to', screenName);
if (navigationRef.current?.isReady()) {
typeof screenOrOptions === 'string'
? originalNavigation.navigate(screenOrOptions, params)
: originalNavigation.navigate(screenName, params); // Fixed to use screenName and params
if (typeof screenOrOptions === 'string') {
originalNavigation.navigate({ name: screenOrOptions, params, merge: options?.merge });
} else {
originalNavigation.navigate({ ...screenOrOptions, params, merge: options?.merge });
}
}
};
@ -88,9 +94,14 @@ export const useExtendedNavigation = <T extends NavigationProp<ParamListBase>>()
})();
};
const navigateToWalletsList = () => {
enhancedNavigate('WalletsList');
}
return {
...originalNavigation,
navigate: enhancedNavigate,
navigateToWalletsList,
};
};

View File

@ -464,7 +464,7 @@ PODS:
- React-perflogger (= 0.72.14)
- ReactNativeCameraKit (13.0.0):
- React-Core
- RealmJS (12.11.1):
- RealmJS (12.12.1):
- React
- rn-ldk (0.8.4):
- React-Core
@ -517,6 +517,9 @@ PODS:
- RNWatch (1.1.0):
- React
- SocketRocket (0.6.1)
- TrueSheet (0.12.4):
- RCT-Folly (= 2021.07.22.00)
- React-Core
- Yoga (1.14.0)
DEPENDENCIES:
@ -600,6 +603,7 @@ DEPENDENCIES:
- RNSVG (from `../node_modules/react-native-svg`)
- RNVectorIcons (from `../node_modules/react-native-vector-icons`)
- RNWatch (from `../node_modules/react-native-watch-connectivity`)
- "TrueSheet (from `../node_modules/@lodev09/react-native-true-sheet`)"
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
SPEC REPOS:
@ -767,6 +771,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-vector-icons"
RNWatch:
:path: "../node_modules/react-native-watch-connectivity"
TrueSheet:
:path: "../node_modules/@lodev09/react-native-true-sheet"
Yoga:
:path: "../node_modules/react-native/ReactCommon/yoga"
@ -830,7 +836,7 @@ SPEC CHECKSUMS:
React-utils: 22a77b05da25ce49c744faa82e73856dcae1734e
ReactCommon: ff94462e007c568d8cdebc32e3c97af86ec93bb5
ReactNativeCameraKit: 9d46a5d7dd544ca64aa9c03c150d2348faf437eb
RealmJS: ecd23895a68689ce2c048d28df3d699cf76cedb7
RealmJS: f4f6d27b325b1c1b402eb0aa5ee79e9d5f603293
rn-ldk: 0d8749d98cc5ce67302a32831818c116b67f7643
RNCAsyncStorage: ec53e44dc3e75b44aa2a9f37618a49c3bc080a7a
RNCClipboard: 0a720adef5ec193aa0e3de24c3977222c7e52a37
@ -854,8 +860,9 @@ SPEC CHECKSUMS:
RNVectorIcons: 32462e7c7e58fe457474fc79c4d7de3f0ef08d70
RNWatch: fd30ca40a5b5ef58dcbc195638e68219bc455236
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
TrueSheet: 3ba036b4f20ea78a2953fcf7949e8a60dd63d3f6
Yoga: c32e0be1a17f8f1f0e633a3122f7666441f52c82
PODFILE CHECKSUM: f19eea438501edfe85fb2fa51d40ba1b57540758
COCOAPODS: 1.14.3
COCOAPODS: 1.15.2

21629
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -51,7 +51,6 @@
"clean:ios": "rm -fr node_modules && rm -fr ios/Pods && npm i && cd ios && pod update && cd ..; npm start -- --reset-cache",
"releasenotes2json": "./scripts/release-notes.sh > release-notes.txt; node -e 'console.log(JSON.stringify(require(\"fs\").readFileSync(\"release-notes.txt\", \"utf8\")));' > release-notes.json",
"branch2json": "./scripts/current-branch.sh > current-branch.json",
"prestart": "watchman watch-del $PWD && watchman watch-project $PWD",
"start": "node node_modules/react-native/local-cli/cli.js start",
"android": "react-native run-android",
"android:clean": "cd android; ./gradlew clean ; cd .. ; npm run android",
@ -108,6 +107,7 @@
"@bugsnag/react-native": "7.25.0",
"@bugsnag/source-maps": "2.3.3",
"@keystonehq/bc-ur-registry": "0.6.4",
"@lodev09/react-native-true-sheet": "github:BlueWallet/react-native-true-sheet#fbcb1af",
"@ngraveio/bc-ur": "1.1.12",
"@noble/secp256k1": "1.6.3",
"@react-native-async-storage/async-storage": "1.24.0",
@ -174,7 +174,6 @@
"react-native-keychain": "8.2.0",
"react-native-linear-gradient": "2.8.3",
"react-native-localize": "3.2.0",
"react-native-modal": "13.0.1",
"react-native-obscure": "https://github.com/BlueWallet/react-native-obscure.git#f4b83b4a261e39b1f5ed4a45ac5bcabc8a59eadb",
"react-native-permissions": "4.1.5",
"react-native-privacy-snapshot": "https://github.com/BlueWallet/react-native-privacy-snapshot#529e4627d93f67752a27e82a040ff7b64dca0783",
@ -194,7 +193,7 @@
"react-native-vector-icons": "10.1.0",
"react-native-watch-connectivity": "1.1.0",
"readable-stream": "3.6.2",
"realm": "12.11.1",
"realm": "12.12.1",
"rn-ldk": "github:BlueWallet/rn-ldk#v0.8.4",
"rn-nodeify": "10.3.0",
"scryptsy": "2.1.0",

View File

@ -6,7 +6,6 @@ import { BlueLoading, BlueSpacing10, BlueSpacing20, BlueTextCentered } from '../
import { LightningLdkWallet } from '../../class';
import { TWallet } from '../../class/wallets/types';
import presentAlert from '../../components/Alert';
import BottomModal from '../../components/BottomModal';
import Button from '../../components/Button';
import LNNodeBar from '../../components/LNNodeBar';
import navigationStyle from '../../components/navigationStyle';
@ -18,6 +17,7 @@ import selectWallet from '../../helpers/select-wallet';
import loc, { formatBalance } from '../../loc';
import { Chain } from '../../models/bitcoinUnits';
import { useStorage } from '../../hooks/context/useStorage';
import BottomModal, { BottomModalHandle } from '../../components/BottomModal';
const LdkNodeInfoChannelStatus = { ACTIVE: 'Active', INACTIVE: 'Inactive', PENDING: 'PENDING', STATUS: 'status' };
@ -46,6 +46,7 @@ const LdkInfo = () => {
const [pendingChannels, setPendingChannels] = useState<any[]>([]);
const [wBalance, setWalletBalance] = useState<{ confirmedBalance?: number }>({});
const [maturingBalance, setMaturingBalance] = useState(0);
const bottomModalRef = useRef<BottomModalHandle>(null);
const [maturingEta, setMaturingEta] = useState('');
const centerContent = channels.length === 0 && pendingChannels.length === 0 && inactiveChannels.length === 0;
const allChannelsAmount = useRef(0);
@ -210,6 +211,7 @@ const LdkInfo = () => {
const closeModal = () => {
Keyboard.dismiss();
setSelectedChannelIndex(undefined);
bottomModalRef.current?.dismiss();
};
const handleOnConnectPeerTapped = async (channelData: any) => {
@ -222,52 +224,50 @@ const LdkInfo = () => {
const status = selectedChannelIndex?.status;
const channelData = selectedChannelIndex?.channel.item;
return (
<BottomModal isVisible={selectedChannelIndex !== undefined} onClose={closeModal} avoidKeyboard>
<View style={[styles.modalContent, stylesHook.modalContent]}>
<Text style={stylesHook.detailsText}>{loc.lnd.node_alias}</Text>
<BlueSpacing10 />
{channelData && (
<Text style={stylesHook.detailsText}>
{LightningLdkWallet.pubkeyToAlias(channelData.remote_node_id) +
' (' +
channelData.remote_node_id.substr(0, 10) +
'...' +
channelData.remote_node_id.substr(-6) +
')'}
</Text>
)}
<BlueSpacing20 />
<LNNodeBar
disabled={
status === LdkNodeInfoChannelStatus.ACTIVE || status === LdkNodeInfoChannelStatus.INACTIVE ? !channelData?.is_usable : true
}
canSend={Number(channelData?.outbound_capacity_msat / 1000)}
canReceive={Number(channelData?.inbound_capacity_msat / 1000)}
itemPriceUnit={wallet.getPreferredBalanceUnit()}
/>
<BottomModal ref={bottomModalRef} contentContainerStyle={[styles.modalContent, stylesHook.modalContent]}>
<Text style={stylesHook.detailsText}>{loc.lnd.node_alias}</Text>
<BlueSpacing10 />
{channelData && (
<Text style={stylesHook.detailsText}>
{status === LdkNodeInfoChannelStatus.PENDING
? loc.transactions.pending
: channelData?.is_usable
? loc.lnd.active
: loc.lnd.inactive}
{LightningLdkWallet.pubkeyToAlias(channelData.remote_node_id) +
' (' +
channelData.remote_node_id.substr(0, 10) +
'...' +
channelData.remote_node_id.substr(-6) +
')'}
</Text>
)}
<BlueSpacing20 />
<LNNodeBar
disabled={
status === LdkNodeInfoChannelStatus.ACTIVE || status === LdkNodeInfoChannelStatus.INACTIVE ? !channelData?.is_usable : true
}
canSend={Number(channelData?.outbound_capacity_msat / 1000)}
canReceive={Number(channelData?.inbound_capacity_msat / 1000)}
itemPriceUnit={wallet.getPreferredBalanceUnit()}
/>
{status === LdkNodeInfoChannelStatus.INACTIVE && (
<>
<StyledButton
onPress={() => handleOnConnectPeerTapped(channelData)}
text={loc.lnd.reconnect_peer}
buttonStyle={StyledButtonType.grey}
/>
<BlueSpacing20 />
</>
)}
<Text style={stylesHook.detailsText}>
{status === LdkNodeInfoChannelStatus.PENDING
? loc.transactions.pending
: channelData?.is_usable
? loc.lnd.active
: loc.lnd.inactive}
</Text>
<StyledButton onPress={() => closeChannel(channelData)} text={loc.lnd.close_channel} buttonStyle={StyledButtonType.destroy} />
<BlueSpacing20 />
</View>
{status === LdkNodeInfoChannelStatus.INACTIVE && (
<>
<StyledButton
onPress={() => handleOnConnectPeerTapped(channelData)}
text={loc.lnd.reconnect_peer}
buttonStyle={StyledButtonType.grey}
/>
<BlueSpacing20 />
</>
)}
<StyledButton onPress={() => closeChannel(channelData)} text={loc.lnd.close_channel} buttonStyle={StyledButtonType.destroy} />
<BlueSpacing20 />
</BottomModal>
);
};

View File

@ -1,17 +1,6 @@
import { useFocusEffect, useRoute } from '@react-navigation/native';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import {
BackHandler,
InteractionManager,
Keyboard,
KeyboardAvoidingView,
Platform,
ScrollView,
StyleSheet,
Text,
TextInput,
View,
} from 'react-native';
import { BackHandler, InteractionManager, ScrollView, StyleSheet, Text, TextInput, View } from 'react-native';
import Share from 'react-native-share';
import * as BlueElectrum from '../../blue_modules/BlueElectrum';
@ -47,7 +36,6 @@ const ReceiveDetails = () => {
const [customUnit, setCustomUnit] = useState(BitcoinUnit.BTC);
const [bip21encoded, setBip21encoded] = useState('');
const [isCustom, setIsCustom] = useState(false);
const [isCustomModalVisible, setIsCustomModalVisible] = useState(false);
const [tempCustomLabel, setTempCustomLabel] = useState('');
const [tempCustomAmount, setTempCustomAmount] = useState('');
const [tempCustomUnit, setTempCustomUnit] = useState(BitcoinUnit.BTC);
@ -56,6 +44,7 @@ const ReceiveDetails = () => {
const [showAddress, setShowAddress] = useState(false);
const [currentTab, setCurrentTab] = useState(segmentControlValues[0]);
const { goBack, setParams } = useExtendedNavigation();
const bottomModalRef = useRef(null);
const { colors } = useTheme();
const [intervalMs, setIntervalMs] = useState(5000);
const [eta, setEta] = useState('');
@ -65,11 +54,6 @@ const ReceiveDetails = () => {
const fetchAddressInterval = useRef();
const receiveAddressButton = useRef();
const stylesHook = StyleSheet.create({
modalContent: {
backgroundColor: colors.modal,
borderTopColor: colors.foregroundColor,
borderWidth: colors.borderWidth,
},
customAmount: {
borderColor: colors.formBorder,
borderBottomColor: colors.formBorder,
@ -253,7 +237,7 @@ const ReceiveDetails = () => {
{isCustom && (
<>
{getDisplayAmount() && (
<BlueText testID="CustomAmountText" style={[styles.amount, stylesHook.amount]} numberOfLines={1}>
<BlueText testID="BitcoinAmountText" style={[styles.amount, stylesHook.amount]} numberOfLines={1}>
{getDisplayAmount()}
</BlueText>
)}
@ -325,21 +309,16 @@ const ReceiveDetails = () => {
}, [wallet, address, obtainWalletAddress, setAddressBIP21Encoded]),
);
const dismissCustomAmountModal = () => {
Keyboard.dismiss();
setIsCustomModalVisible(false);
};
const showCustomAmountModal = () => {
setTempCustomLabel(customLabel);
setTempCustomAmount(customAmount);
setTempCustomUnit(customUnit);
setIsCustomModalVisible(true);
bottomModalRef.current.present();
};
const createCustomAmountAddress = () => {
bottomModalRef.current.dismiss();
setIsCustom(true);
setIsCustomModalVisible(false);
let amount = tempCustomAmount;
switch (tempCustomUnit) {
case BitcoinUnit.BTC:
@ -373,52 +352,7 @@ const ReceiveDetails = () => {
setCustomUnit(wallet.getPreferredBalanceUnit());
setBip21encoded(DeeplinkSchemaMatch.bip21encode(address));
setShowAddress(true);
setIsCustomModalVisible(false);
};
const renderCustomAmountModal = () => {
return (
<BottomModal isVisible={isCustomModalVisible} onClose={dismissCustomAmountModal}>
<KeyboardAvoidingView enabled={!Platform.isPad} behavior={Platform.OS === 'ios' ? 'position' : null}>
<View style={[styles.modalContent, stylesHook.modalContent]}>
<AmountInput
unit={tempCustomUnit}
amount={tempCustomAmount || ''}
onChangeText={setTempCustomAmount}
onAmountUnitChange={setTempCustomUnit}
/>
<View style={[styles.customAmount, stylesHook.customAmount]}>
<TextInput
onChangeText={setTempCustomLabel}
placeholderTextColor="#81868e"
placeholder={loc.receive.details_label}
value={tempCustomLabel || ''}
numberOfLines={1}
style={[styles.customAmountText, stylesHook.customAmountText]}
testID="CustomAmountDescription"
/>
</View>
<BlueSpacing20 />
<View style={styles.modalButtonContainer}>
<Button
testID="CustomAmountResetButton"
style={[styles.modalButton, stylesHook.modalButton]}
title={loc.receive.reset}
onPress={resetCustomAmount}
/>
<View style={styles.modalButtonSpacing} />
<Button
testID="CustomAmountSaveButton"
style={[styles.modalButton, stylesHook.modalButton]}
title={loc.receive.details_create}
onPress={createCustomAmountAddress}
/>
</View>
<BlueSpacing20 />
</View>
</KeyboardAvoidingView>
</BottomModal>
);
bottomModalRef.current.dismiss();
};
const handleShareButtonPressed = () => {
@ -470,52 +404,93 @@ const ReceiveDetails = () => {
};
return (
<ScrollView contentContainerStyle={[styles.root, stylesHook.root]} keyboardShouldPersistTaps="always">
{wallet?.allowBIP47() && wallet.isBIP47Enabled() && (
<View style={styles.tabsContainer}>
<SegmentedControl
values={segmentControlValues}
selectedIndex={segmentControlValues.findIndex(tab => tab === currentTab)}
onChange={index => {
setCurrentTab(segmentControlValues[index]);
}}
<>
<ScrollView contentContainerStyle={[styles.root, stylesHook.root]} keyboardShouldPersistTaps="always">
{wallet?.allowBIP47() && wallet.isBIP47Enabled() && (
<View style={styles.tabsContainer}>
<SegmentedControl
values={segmentControlValues}
selectedIndex={segmentControlValues.findIndex(tab => tab === currentTab)}
onChange={index => {
setCurrentTab(segmentControlValues[index]);
}}
/>
</View>
)}
{showAddress && renderTabContent()}
{address !== undefined && showAddress && (
<HandOffComponent title={loc.send.details_address} type={HandOffActivityType.ReceiveOnchain} userInfo={{ address }} />
)}
{showConfirmedBalance ? renderConfirmedBalance() : null}
{showPendingBalance ? renderPendingBalance() : null}
{!showAddress && !showPendingBalance && !showConfirmedBalance ? <BlueLoading /> : null}
<View style={styles.share}>
<BlueCard>
{showAddress && currentTab === loc.wallets.details_address && (
<BlueButtonLink
style={styles.link}
testID="SetCustomAmountButton"
title={loc.receive.details_setAmount}
onPress={showCustomAmountModal}
/>
)}
<Button onPress={handleShareButtonPressed} title={loc.receive.details_share} />
</BlueCard>
</View>
</ScrollView>
<BottomModal
ref={bottomModalRef}
contentContainerStyle={styles.modalContainerJustify}
backgroundColor={colors.modal}
footer={
<View style={styles.modalButtonContainer}>
<Button
testID="CustomAmountResetButton"
style={[styles.modalButton, stylesHook.modalButton]}
title={loc.receive.reset}
onPress={resetCustomAmount}
/>
<View style={styles.modalButtonSpacing} />
<Button
testID="CustomAmountSaveButton"
style={[styles.modalButton, stylesHook.modalButton]}
title={loc.receive.details_create}
onPress={createCustomAmountAddress}
/>
</View>
}
>
<AmountInput
unit={tempCustomUnit}
amount={tempCustomAmount || ''}
onChangeText={setTempCustomAmount}
onAmountUnitChange={setTempCustomUnit}
/>
<View style={[styles.customAmount, stylesHook.customAmount]}>
<TextInput
onChangeText={setTempCustomLabel}
placeholderTextColor="#81868e"
placeholder={loc.receive.details_label}
value={tempCustomLabel || ''}
numberOfLines={1}
style={[styles.customAmountText, stylesHook.customAmountText]}
testID="CustomAmountDescription"
/>
</View>
)}
{showAddress && renderTabContent()}
{renderCustomAmountModal()}
{address !== undefined && showAddress && (
<HandOffComponent title={loc.send.details_address} type={HandOffActivityType.ReceiveOnchain} userInfo={{ address }} />
)}
{showConfirmedBalance ? renderConfirmedBalance() : null}
{showPendingBalance ? renderPendingBalance() : null}
{!showAddress && !showPendingBalance && !showConfirmedBalance ? <BlueLoading /> : null}
<View style={styles.share}>
<BlueCard>
{showAddress && currentTab === loc.wallets.details_address && (
<BlueButtonLink
style={styles.link}
testID="SetCustomAmountButton"
title={loc.receive.details_setAmount}
onPress={showCustomAmountModal}
/>
)}
<Button onPress={handleShareButtonPressed} title={loc.receive.details_share} />
</BlueCard>
</View>
</ScrollView>
<BlueSpacing20 />
<BlueSpacing20 />
</BottomModal>
</>
);
};
const styles = StyleSheet.create({
modalContent: {
modalContainerJustify: {
alignContent: 'center',
padding: 22,
justifyContent: 'center',
alignItems: 'center',
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
minHeight: 350,
height: 350,
justifyContent: 'space-between',
},
customAmount: {
flexDirection: 'row',
@ -570,6 +545,8 @@ const styles = StyleSheet.create({
modalButtonContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingBottom: 34,
},
modalButtonSpacing: {
width: 16,

View File

@ -36,7 +36,7 @@ import { AbstractHDElectrumWallet } from '../../class/wallets/abstract-hd-electr
import AddressInput from '../../components/AddressInput';
import presentAlert from '../../components/Alert';
import AmountInput from '../../components/AmountInput';
import BottomModal from '../../components/BottomModal';
import BottomModal, { BottomModalHandle } from '../../components/BottomModal';
import Button from '../../components/Button';
import CoinsSelected from '../../components/CoinsSelected';
import InputAccessoryAllFunds from '../../components/InputAccessoryAllFunds';
@ -91,10 +91,10 @@ const SendDetails = () => {
const [width, setWidth] = useState(Dimensions.get('window').width);
const [isLoading, setIsLoading] = useState(false);
const [wallet, setWallet] = useState<TWallet | null>(null);
const feeModalRef = useRef<BottomModalHandle>(null);
const optionsModalRef = useRef<BottomModalHandle>(null);
const [walletSelectionOrCoinsSelectedHidden, setWalletSelectionOrCoinsSelectedHidden] = useState(false);
const [isAmountToolbarVisibleForAndroid, setIsAmountToolbarVisibleForAndroid] = useState(false);
const [isFeeSelectionModalVisible, setIsFeeSelectionModalVisible] = useState(false);
const [optionsVisible, setOptionsVisible] = useState(false);
const [isTransactionReplaceable, setIsTransactionReplaceable] = useState<boolean>(false);
const [addresses, setAddresses] = useState<IPaymentDestinations[]>([]);
const [units, setUnits] = useState<BitcoinUnit[]>([]);
@ -112,7 +112,7 @@ const SendDetails = () => {
const [dumb, setDumb] = useState(false);
const { isEditable } = routeParams;
// if utxo is limited we use it to calculate available balance
const balance: number = utxo ? utxo.reduce((prev, curr) => prev + curr.value, 0) : wallet?.getBalance() ?? 0;
const balance: number = utxo ? utxo.reduce((prev, curr) => prev + curr.value, 0) : (wallet?.getBalance() ?? 0);
const allBalance = formatBalanceWithoutSuffix(balance, BitcoinUnit.BTC, true);
// if cutomFee is not set, we need to choose highest possible fee for wallet balance
@ -388,6 +388,10 @@ const SendDetails = () => {
useCallback(() => {
setIsLoading(false);
setDumb(v => !v);
return () => {
feeModalRef.current?.dismiss();
optionsModalRef.current?.dismiss();
};
}, []),
);
@ -609,11 +613,16 @@ const SendDetails = () => {
if (tx && routeParams.launchedBy && psbt) {
console.warn('navigating back to ', routeParams.launchedBy);
feeModalRef.current?.dismiss();
optionsModalRef.current?.dismiss();
// @ts-ignore idk how to fix FIXME?
navigation.navigate(routeParams.launchedBy, { psbt });
}
if (wallet?.type === WatchOnlyWallet.type) {
feeModalRef.current?.dismiss();
optionsModalRef.current?.dismiss();
// watch-only wallets with enabled HW wallet support have different flow. we have to show PSBT to user as QR code
// so he can scan it and sign it. then we have to scan it back from user (via camera and QR code), and ask
// user whether he wants to broadcast it
@ -628,6 +637,8 @@ const SendDetails = () => {
}
if (wallet?.type === MultisigHDWallet.type) {
feeModalRef.current?.dismiss();
optionsModalRef.current?.dismiss();
navigation.navigate('PsbtMultisig', {
memo: transactionMemo,
psbtBase64: psbt.toBase64(),
@ -652,6 +663,8 @@ const SendDetails = () => {
// (ez can be the case for single-address wallet when doing self-payment for consolidation)
recipients = outputs;
}
feeModalRef.current?.dismiss();
optionsModalRef.current?.dismiss();
navigation.navigate('Confirm', {
fee: new BigNumber(fee).dividedBy(100000000).toNumber(),
@ -685,8 +698,9 @@ const SendDetails = () => {
return presentAlert({ title: loc.errors.error, message: 'Importing transaction in non-watchonly wallet (this should never happen)' });
}
setOptionsVisible(false);
requestCameraAuthorization().then(() => {
feeModalRef.current?.dismiss();
optionsModalRef.current?.dismiss();
navigation.navigate('ScanQRCodeRoot', {
screen: 'ScanQRCode',
params: {
@ -707,18 +721,20 @@ const SendDetails = () => {
// this looks like NOT base64, so maybe its transaction's hex
// we dont support it in this flow
} else {
feeModalRef.current?.dismiss();
optionsModalRef.current?.dismiss();
// psbt base64?
// we construct PSBT object and pass to next screen
// so user can do smth with it:
const psbt = bitcoin.Psbt.fromBase64(ret.data);
navigation.navigate('PsbtWithHardwareWallet', {
memo: transactionMemo,
fromWallet: wallet,
psbt,
});
setIsLoading(false);
setOptionsVisible(false);
}
};
@ -731,6 +747,7 @@ const SendDetails = () => {
* @returns {Promise<void>}
*/
const importTransaction = async () => {
await optionsModalRef.current?.dismiss();
if (wallet?.type !== WatchOnlyWallet.type) {
return presentAlert({ title: loc.errors.error, message: 'Importing transaction in non-watchonly wallet (this should never happen)' });
}
@ -751,7 +768,7 @@ const SendDetails = () => {
const txhex = psbt.extractTransaction().toHex();
navigation.navigate('PsbtWithHardwareWallet', { memo: transactionMemo, fromWallet: wallet, txhex });
setIsLoading(false);
setOptionsVisible(false);
return;
}
@ -762,7 +779,7 @@ const SendDetails = () => {
const psbt = bitcoin.Psbt.fromBase64(file);
navigation.navigate('PsbtWithHardwareWallet', { memo: transactionMemo, fromWallet: wallet, psbt });
setIsLoading(false);
setOptionsVisible(false);
return;
}
@ -771,7 +788,7 @@ const SendDetails = () => {
const file = (await RNFS.readFile(res.uri, 'ascii')).replace('\n', '').replace('\r', '');
navigation.navigate('PsbtWithHardwareWallet', { memo: transactionMemo, fromWallet: wallet, txhex: file });
setIsLoading(false);
setOptionsVisible(false);
return;
}
@ -805,13 +822,13 @@ const SendDetails = () => {
};
const _importTransactionMultisig = async (base64arg: string | false) => {
await optionsModalRef.current?.dismiss();
try {
const base64 = base64arg || (await fs.openSignedTransaction());
if (!base64) return;
const psbt = bitcoin.Psbt.fromBase64(base64); // if it doesnt throw - all good, its valid
if ((wallet as MultisigHDWallet)?.howManySignaturesCanWeMake() > 0 && (await askCosignThisTransaction())) {
hideOptions();
setIsLoading(true);
await sleep(100);
(wallet as MultisigHDWallet).cosignPsbt(psbt);
@ -830,7 +847,6 @@ const SendDetails = () => {
presentAlert({ title: loc.send.problem_with_psbt, message: error.message });
}
setIsLoading(false);
setOptionsVisible(false);
};
const importTransactionMultisig = () => {
@ -851,8 +867,8 @@ const SendDetails = () => {
}
};
const importTransactionMultisigScanQr = () => {
setOptionsVisible(false);
const importTransactionMultisigScanQr = async () => {
await optionsModalRef.current?.dismiss();
requestCameraAuthorization().then(() => {
navigation.navigate('ScanQRCodeRoot', {
screen: 'ScanQRCode',
@ -865,9 +881,10 @@ const SendDetails = () => {
};
const handleAddRecipient = async () => {
console.log('handleAddRecipient');
console.debug('handleAddRecipient');
await optionsModalRef.current?.dismiss();
setAddresses(addrs => [...addrs, { address: '', key: String(Math.random()) } as IPaymentDestinations]);
setOptionsVisible(false);
await sleep(200); // wait for animation
scrollView.current?.scrollToEnd();
if (addresses.length === 0) return;
@ -875,39 +892,41 @@ const SendDetails = () => {
};
const handleRemoveRecipient = async () => {
await optionsModalRef.current?.dismiss();
const last = scrollIndex.current === addresses.length - 1;
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setAddresses(addrs => {
addrs.splice(scrollIndex.current, 1);
return [...addrs];
});
setOptionsVisible(false);
if (addresses.length === 0) return;
await sleep(200); // wait for animation
scrollView.current?.flashScrollIndicators();
if (last && Platform.OS === 'android') scrollView.current?.scrollToEnd(); // fix white screen on android
};
const handleCoinControl = () => {
const handleCoinControl = async () => {
await optionsModalRef.current?.dismiss();
if (!wallet) return;
setOptionsVisible(false);
navigation.navigate('CoinControl', {
walletID: wallet?.getID(),
onUTXOChoose: (u: CreateTransactionUtxo[]) => setUtxo(u),
});
};
const handleInsertContact = () => {
const handleInsertContact = async () => {
await optionsModalRef.current?.dismiss();
if (!wallet) return;
setOptionsVisible(false);
navigation.navigate('PaymentCodeList', { walletID: wallet.getID() });
};
const handlePsbtSign = async () => {
await optionsModalRef.current?.dismiss();
setIsLoading(true);
setOptionsVisible(false);
await new Promise(resolve => setTimeout(resolve, 100)); // sleep for animations
const scannedData = await scanQrHelper(name);
const scannedData = await scanQrHelper(name, true, undefined);
if (!scannedData) return setIsLoading(false);
let tx;
@ -945,11 +964,6 @@ const SendDetails = () => {
});
};
const hideOptions = () => {
Keyboard.dismiss();
setOptionsVisible(false);
};
// Header Right Button
const headerRightOnPress = (id: string) => {
@ -1070,8 +1084,7 @@ const SendDetails = () => {
disabled={isLoading}
style={styles.advancedOptions}
onPress={() => {
Keyboard.dismiss();
setOptionsVisible(true);
optionsModalRef.current?.present();
}}
testID="advancedOptionsMenuButton"
>
@ -1106,7 +1119,8 @@ const SendDetails = () => {
scrollIndex.current = index;
};
const onUseAllPressed = () => {
const onUseAllPressed = async () => {
await optionsModalRef.current?.dismiss();
triggerHapticFeedback(HapticFeedbackTypes.NotificationWarning);
const message = frozenBalance > 0 ? loc.send.details_adv_full_sure_frozen : loc.send.details_adv_full_sure;
Alert.alert(
@ -1126,12 +1140,16 @@ const SendDetails = () => {
u[scrollIndex.current] = BitcoinUnit.BTC;
return [...u];
});
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setOptionsVisible(false);
},
style: 'default',
},
{ text: loc._.cancel, onPress: () => {}, style: 'cancel' },
{
text: loc._.cancel,
onPress: () => {
optionsModalRef.current?.present();
},
style: 'cancel',
},
],
{ cancelable: false },
);
@ -1146,16 +1164,6 @@ const SendDetails = () => {
root: {
backgroundColor: colors.elevated,
},
modalContent: {
backgroundColor: colors.modal,
borderTopColor: colors.borderTopColor,
borderWidth: colors.borderWidth,
},
optionsContent: {
backgroundColor: colors.modal,
borderTopColor: colors.borderTopColor,
borderWidth: colors.borderWidth,
},
feeModalItemActive: {
backgroundColor: colors.feeActive,
},
@ -1237,42 +1245,18 @@ const SendDetails = () => {
];
return (
<BottomModal isVisible={isFeeSelectionModalVisible} onClose={() => setIsFeeSelectionModalVisible(false)}>
<KeyboardAvoidingView enabled={!isTablet} behavior={Platform.OS === 'ios' ? 'position' : undefined}>
<View style={[styles.modalContent, stylesHook.modalContent]}>
{options.map(({ label, time, fee, rate, active, disabled }, index) => (
<TouchableOpacity
accessibilityRole="button"
key={label}
disabled={disabled}
onPress={() => {
setFeePrecalc(fp => ({ ...fp, current: fee }));
setIsFeeSelectionModalVisible(false);
setCustomFee(rate.toString());
}}
style={[styles.feeModalItem, active && styles.feeModalItemActive, active && !disabled && stylesHook.feeModalItemActive]}
>
<View style={styles.feeModalRow}>
<Text style={[styles.feeModalLabel, disabled ? stylesHook.feeModalItemTextDisabled : stylesHook.feeModalLabel]}>
{label}
</Text>
<View style={[styles.feeModalTime, disabled ? stylesHook.feeModalItemDisabled : stylesHook.feeModalTime]}>
<Text style={stylesHook.feeModalTimeText}>~{time}</Text>
</View>
</View>
<View style={styles.feeModalRow}>
<Text style={disabled ? stylesHook.feeModalItemTextDisabled : stylesHook.feeModalValue}>{fee && formatFee(fee)}</Text>
<Text style={disabled ? stylesHook.feeModalItemTextDisabled : stylesHook.feeModalValue}>
{rate} {loc.units.sat_vbyte}
</Text>
</View>
</TouchableOpacity>
))}
<BottomModal
ref={feeModalRef}
backgroundColor={colors.modal}
contentContainerStyle={[styles.modalContent, styles.modalContentMinHeight]}
footerDefaultMargins
footer={
<View style={styles.feeModalFooter}>
<TouchableOpacity
testID="feeCustom"
accessibilityRole="button"
style={styles.feeModalCustom}
onPress={async () => {
await feeModalRef.current?.dismiss();
let error = loc.send.fee_satvbyte;
while (true) {
let fee: number | string;
@ -1291,7 +1275,6 @@ const SendDetails = () => {
if (Number(fee) < 1) fee = '1';
fee = Number(fee).toString(); // this will remove leading zeros if any
setCustomFee(fee);
setIsFeeSelectionModalVisible(false);
return;
}
}}
@ -1299,7 +1282,38 @@ const SendDetails = () => {
<Text style={[styles.feeModalCustomText, stylesHook.feeModalCustomText]}>{loc.send.fee_custom}</Text>
</TouchableOpacity>
</View>
</KeyboardAvoidingView>
}
>
<View style={styles.paddingTop80}>
{options.map(({ label, time, fee, rate, active, disabled }) => (
<TouchableOpacity
accessibilityRole="button"
key={label}
disabled={disabled}
onPress={() => {
setFeePrecalc(fp => ({ ...fp, current: fee }));
feeModalRef.current?.dismiss();
setCustomFee(rate.toString());
}}
style={[styles.feeModalItem, active && styles.feeModalItemActive, active && !disabled && stylesHook.feeModalItemActive]}
>
<View style={styles.feeModalRow}>
<Text style={[styles.feeModalLabel, disabled ? stylesHook.feeModalItemTextDisabled : stylesHook.feeModalLabel]}>
{label}
</Text>
<View style={[styles.feeModalTime, disabled ? stylesHook.feeModalItemDisabled : stylesHook.feeModalTime]}>
<Text style={stylesHook.feeModalTimeText}>~{time}</Text>
</View>
</View>
<View style={styles.feeModalRow}>
<Text style={disabled ? stylesHook.feeModalItemTextDisabled : stylesHook.feeModalValue}>{fee && formatFee(fee)}</Text>
<Text style={disabled ? stylesHook.feeModalItemTextDisabled : stylesHook.feeModalValue}>
{rate} {loc.units.sat_vbyte}
</Text>
</View>
</TouchableOpacity>
))}
</View>
</BottomModal>
);
};
@ -1308,56 +1322,52 @@ const SendDetails = () => {
const isSendMaxUsed = addresses.some(element => element.amount === BitcoinUnit.MAX);
return (
<BottomModal isVisible={optionsVisible} onClose={hideOptions}>
<KeyboardAvoidingView enabled={!isTablet} behavior={undefined}>
<View style={[styles.optionsContent, stylesHook.optionsContent]}>
{wallet?.allowBIP47() && wallet.isBIP47Enabled() && (
<ListItem testID="InsertContactButton" title={loc.send.details_insert_contact} onPress={handleInsertContact} />
)}
{isEditable && (
<ListItem
testID="sendMaxButton"
disabled={balance === 0 || isSendMaxUsed}
title={loc.send.details_adv_full}
onPress={onUseAllPressed}
/>
)}
{wallet?.type === HDSegwitBech32Wallet.type && isEditable && (
<ListItem
title={loc.send.details_adv_fee_bump}
Component={TouchableWithoutFeedback}
switch={{ value: isTransactionReplaceable, onValueChange: onReplaceableFeeSwitchValueChanged }}
/>
)}
{wallet?.type === WatchOnlyWallet.type && wallet.isHd() && (
<ListItem title={loc.send.details_adv_import} onPress={importTransaction} />
)}
{wallet?.type === WatchOnlyWallet.type && wallet.isHd() && (
<ListItem testID="ImportQrTransactionButton" title={loc.send.details_adv_import_qr} onPress={importQrTransaction} />
)}
{wallet?.type === MultisigHDWallet.type && isEditable && (
<ListItem title={loc.send.details_adv_import} onPress={importTransactionMultisig} />
)}
{wallet?.type === MultisigHDWallet.type && wallet.howManySignaturesCanWeMake() > 0 && isEditable && (
<ListItem title={loc.multisig.co_sign_transaction} onPress={importTransactionMultisigScanQr} />
)}
{isEditable && (
<>
<ListItem testID="AddRecipient" title={loc.send.details_add_rec_add} onPress={handleAddRecipient} />
<ListItem
testID="RemoveRecipient"
title={loc.send.details_add_rec_rem}
disabled={addresses.length < 2}
onPress={handleRemoveRecipient}
/>
</>
)}
<ListItem testID="CoinControl" title={loc.cc.header} onPress={handleCoinControl} />
{(wallet as MultisigHDWallet)?.allowCosignPsbt() && isEditable && (
<ListItem testID="PsbtSign" title={loc.send.psbt_sign} onPress={handlePsbtSign} />
)}
</View>
</KeyboardAvoidingView>
<BottomModal ref={optionsModalRef} backgroundColor={colors.modal} contentContainerStyle={styles.optionsContent}>
{wallet?.allowBIP47() && wallet.isBIP47Enabled() && (
<ListItem testID="InsertContactButton" title={loc.send.details_insert_contact} onPress={handleInsertContact} />
)}
{isEditable && (
<ListItem
testID="sendMaxButton"
disabled={balance === 0 || isSendMaxUsed}
title={loc.send.details_adv_full}
onPress={onUseAllPressed}
/>
)}
{wallet?.type === HDSegwitBech32Wallet.type && isEditable && (
<ListItem
title={loc.send.details_adv_fee_bump}
Component={TouchableWithoutFeedback}
switch={{ value: isTransactionReplaceable, onValueChange: onReplaceableFeeSwitchValueChanged }}
/>
)}
{wallet?.type === WatchOnlyWallet.type && wallet.isHd() && (
<ListItem title={loc.send.details_adv_import} onPress={importTransaction} />
)}
{wallet?.type === WatchOnlyWallet.type && wallet.isHd() && (
<ListItem testID="ImportQrTransactionButton" title={loc.send.details_adv_import_qr} onPress={importQrTransaction} />
)}
{wallet?.type === MultisigHDWallet.type && isEditable && (
<ListItem title={loc.send.details_adv_import} onPress={importTransactionMultisig} />
)}
{wallet?.type === MultisigHDWallet.type && wallet.howManySignaturesCanWeMake() > 0 && isEditable && (
<ListItem title={loc.multisig.co_sign_transaction} onPress={importTransactionMultisigScanQr} />
)}
{isEditable && (
<>
<ListItem testID="AddRecipient" title={loc.send.details_add_rec_add} onPress={handleAddRecipient} />
<ListItem
testID="RemoveRecipient"
title={loc.send.details_add_rec_rem}
disabled={addresses.length < 2}
onPress={handleRemoveRecipient}
/>
</>
)}
<ListItem testID="CoinControl" title={loc.cc.header} onPress={handleCoinControl} />
{(wallet as MultisigHDWallet)?.allowCosignPsbt() && isEditable && (
<ListItem testID="PsbtSign" title={loc.send.psbt_sign} onPress={handlePsbtSign} />
)}
</BottomModal>
);
};
@ -1400,7 +1410,11 @@ const SendDetails = () => {
<TouchableOpacity
accessibilityRole="button"
style={styles.selectTouch}
onPress={() => navigation.navigate('SelectWallet', { chainType: Chain.ONCHAIN })}
onPress={() => {
feeModalRef.current?.dismiss();
optionsModalRef.current?.dismiss();
navigation.navigate('SelectWallet', { chainType: Chain.ONCHAIN });
}}
>
<Text style={styles.selectText}>{loc.wallets.select_wallet.toLowerCase()}</Text>
<Icon name={I18nManager.isRTL ? 'angle-left' : 'angle-right'} size={18} type="font-awesome" color="#9aa0aa" />
@ -1410,7 +1424,9 @@ const SendDetails = () => {
<TouchableOpacity
accessibilityRole="button"
style={styles.selectTouch}
onPress={() => navigation.navigate('SelectWallet', { chainType: Chain.ONCHAIN })}
onPress={() => {
navigation.navigate('SelectWallet', { chainType: Chain.ONCHAIN });
}}
disabled={!isEditable || isLoading}
>
<Text style={[styles.selectLabel, stylesHook.selectLabel]}>{wallet?.getLabel()}</Text>
@ -1558,7 +1574,7 @@ const SendDetails = () => {
<TouchableOpacity
testID="chooseFee"
accessibilityRole="button"
onPress={() => setIsFeeSelectionModalVisible(true)}
onPress={() => feeModalRef.current?.present()}
disabled={isLoading}
style={styles.fee}
>
@ -1640,16 +1656,12 @@ const styles = StyleSheet.create({
right: 8,
},
modalContent: {
padding: 22,
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
minHeight: 200,
margin: 22,
},
modalContentMinHeight: Platform.OS === 'android' ? { minHeight: 400 } : {},
paddingTop80: { paddingTop: 80 },
optionsContent: {
padding: 22,
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
minHeight: 130,
},
feeModalItem: {
paddingHorizontal: 16,
@ -1673,11 +1685,6 @@ const styles = StyleSheet.create({
paddingHorizontal: 6,
paddingVertical: 3,
},
feeModalCustom: {
height: 60,
alignItems: 'center',
justifyContent: 'center',
},
feeModalCustomText: {
fontSize: 15,
fontWeight: '600',
@ -1715,6 +1722,9 @@ const styles = StyleSheet.create({
marginRight: 18,
marginVertical: 8,
},
feeModalFooter: {
paddingVertical: 50,
},
memo: {
flexDirection: 'row',
borderWidth: 1,

View File

@ -1,11 +1,10 @@
import { useNavigation, useRoute } from '@react-navigation/native';
import PropTypes from 'prop-types';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useRoute } from '@react-navigation/native';
import PropTypes from 'prop-types';
import {
ActivityIndicator,
FlatList,
Keyboard,
KeyboardAvoidingView,
LayoutAnimation,
PixelRatio,
Platform,
@ -29,6 +28,7 @@ import { useTheme } from '../../components/themes';
import loc, { formatBalance } from '../../loc';
import { BitcoinUnit } from '../../models/bitcoinUnits';
import { useStorage } from '../../hooks/context/useStorage';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
const FrozenBadge = () => {
const { colors } = useTheme();
@ -132,7 +132,7 @@ const OutputModal = ({ item: { address, txid, value, vout, confirmations = 0 },
const amount = formatBalance(value, balanceUnit, true);
const oStyles = StyleSheet.create({
container: { paddingHorizontal: 0, borderBottomColor: colors.lightBorder, backgroundColor: colors.elevated },
container: { paddingHorizontal: 0, borderBottomColor: colors.lightBorder, backgroundColor: 'transparent' },
avatar: { borderColor: 'white', borderWidth: 1, backgroundColor: color },
amount: { fontWeight: 'bold', color: colors.foregroundColor },
tranContainer: { paddingLeft: 20 },
@ -194,9 +194,12 @@ const mStyles = StyleSheet.create({
},
buttonContainer: {
height: 45,
marginBottom: 36,
marginHorizontal: 24,
},
});
const transparentBackground = { backgroundColor: 'transparent' };
const OutputModalContent = ({ output, wallet, onUseCoin, frozen, setFrozen }) => {
const { colors } = useTheme();
const { txMetadata, saveToDisk } = useStorage();
@ -234,11 +237,12 @@ const OutputModalContent = ({ output, wallet, onUseCoin, frozen, setFrozen }) =>
]}
onChangeText={onMemoChange}
/>
<ListItem title={loc.cc.freezeLabel} Component={TouchableWithoutFeedback} switch={switchValue} />
<BlueSpacing20 />
<View style={mStyles.buttonContainer}>
<Button testID="UseCoin" title={loc.cc.use_coin} onPress={() => onUseCoin([output])} />
</View>
<ListItem
title={loc.cc.freezeLabel}
containerStyle={transparentBackground}
Component={TouchableWithoutFeedback}
switch={switchValue}
/>
<BlueSpacing20 />
</>
);
@ -254,8 +258,9 @@ OutputModalContent.propTypes = {
const CoinControl = () => {
const { colors } = useTheme();
const navigation = useNavigation();
const navigation = useExtendedNavigation();
const { width } = useWindowDimensions();
const bottomModalRef = useRef(null);
const { walletID, onUTXOChoose } = useRoute().params;
const { wallets, saveToDisk, sleep } = useStorage();
const wallet = wallets.find(w => w.getID() === walletID);
@ -323,7 +328,8 @@ const CoinControl = () => {
const handleChoose = item => setOutput(item);
const handleUseCoin = u => {
const handleUseCoin = async u => {
await bottomModalRef.current?.dismiss();
setOutput(null);
navigation.pop();
onUTXOChoose(u);
@ -386,6 +392,12 @@ const CoinControl = () => {
return <OutputModalContent output={output} wallet={wallet} onUseCoin={handleUseCoin} frozen={oFrozen} setFrozen={setOFrozen} />;
};
useEffect(() => {
if (output) {
bottomModalRef.current?.present();
}
}, [output]);
if (loading) {
return (
<SafeArea style={[styles.center, { backgroundColor: colors.elevated }]}>
@ -403,17 +415,22 @@ const CoinControl = () => {
)}
<BottomModal
isVisible={Boolean(output)}
ref={bottomModalRef}
onClose={() => {
Keyboard.dismiss();
setOutput(false);
}}
backgroundColor={colors.elevated}
footer={
<View style={mStyles.buttonContainer}>
<Button testID="UseCoin" title={loc.cc.use_coin} onPress={() => handleUseCoin([output])} />
</View>
}
footerDefaultMargins
contentContainerStyle={[styles.modalContent, styles.modalMinHeight]}
>
<KeyboardAvoidingView enabled={!Platform.isPad} behavior={Platform.OS === 'ios' ? 'position' : null}>
<View style={[styles.modalContent, { backgroundColor: colors.elevated }]}>{output && renderOutputModalContent()}</View>
</KeyboardAvoidingView>
{output && renderOutputModalContent()}
</BottomModal>
<FlatList
ListHeaderComponent={tipCoins}
data={utxo}
@ -454,11 +471,8 @@ const styles = StyleSheet.create({
},
modalContent: {
padding: 22,
justifyContent: 'center',
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
borderColor: 'rgba(0, 0, 0, 0.1)',
},
modalMinHeight: Platform.OS === 'android' ? { minHeight: 500 } : {},
empty: {
flex: 1,
justifyContent: 'center',

View File

@ -60,7 +60,7 @@ const PsbtMultisigQRCode = () => {
};
const openScanner = async () => {
const scanned = await scanQrHelper(name, true);
const scanned = await scanQrHelper(name, true, undefined);
onBarScanned({ data: scanned });
};

View File

@ -28,7 +28,7 @@ import {
BlueText,
} from '../../BlueComponents';
import DeeplinkSchemaMatch from '../../class/deeplink-schema-match';
import presentAlert from '../../components/Alert';
import presentAlert, { AlertType } from '../../components/Alert';
import Button from '../../components/Button';
import ListItem from '../../components/ListItem';
import { BlueCurrentTheme } from '../../components/themes';
@ -205,7 +205,7 @@ export default class ElectrumSettings extends Component {
}
} catch (error) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({ message: error });
presentAlert({ message: error, type: AlertType.Toast });
}
this.setState({ isLoading: false });
});

View File

@ -18,7 +18,7 @@ import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/h
import Notifications from '../../blue_modules/notifications';
import { BlueCard, BlueSpacing, BlueSpacing20, BlueText } from '../../BlueComponents';
import { HDSegwitBech32Transaction, HDSegwitBech32Wallet } from '../../class';
import presentAlert from '../../components/Alert';
import presentAlert, { AlertType } from '../../components/Alert';
import Button from '../../components/Button';
import navigationStyle from '../../components/navigationStyle';
import SafeArea from '../../components/SafeArea';
@ -101,7 +101,7 @@ export default class CPFP extends Component {
} catch (error) {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
this.setState({ isLoading: false });
presentAlert({ message: error.message });
presentAlert({ message: error.message, type: AlertType.Toast });
}
});
};

View File

@ -143,7 +143,7 @@ const TransactionDetails = () => {
// okay, this txid _was_ with someone using payment codes, so we show the label edit dialog
// and load user-defined alias for the pc if any
setCounterpartyLabel(counterpartyMetadata ? counterpartyMetadata[foundPaymentCode]?.label ?? '' : '');
setCounterpartyLabel(counterpartyMetadata ? (counterpartyMetadata[foundPaymentCode]?.label ?? '') : '');
setIsCounterpartyLabelVisible(true);
setPaymentCode(foundPaymentCode);
}

View File

@ -31,7 +31,7 @@ import {
} from '../../BlueComponents';
import { HDSegwitBech32Wallet, MultisigCosigner, MultisigHDWallet } from '../../class';
import presentAlert from '../../components/Alert';
import BottomModal from '../../components/BottomModal';
import BottomModal, { BottomModalHandle } from '../../components/BottomModal';
import Button from '../../components/Button';
import MultipleStepsListItem, {
MultipleStepsListItemButtohType,
@ -68,9 +68,9 @@ const ViewEditMultisigCosigners: React.FC = () => {
const [isLoading, setIsLoading] = useState(true);
const [isSaveButtonDisabled, setIsSaveButtonDisabled] = useState(true);
const [currentlyEditingCosignerNum, setCurrentlyEditingCosignerNum] = useState<number | false>(false);
const [isProvideMnemonicsModalVisible, setIsProvideMnemonicsModalVisible] = useState(false);
const [isMnemonicsModalVisible, setIsMnemonicsModalVisible] = useState(false);
const [isShareModalVisible, setIsShareModalVisible] = useState(false);
const shareModalRef = useRef<BottomModalHandle>(null);
const provideMnemonicsModalRef = useRef<BottomModalHandle>(null);
const mnemonicsModalRef = useRef<BottomModalHandle>(null);
const [importText, setImportText] = useState('');
const [exportString, setExportString] = useState('{}'); // used in exportCosigner()
const [exportStringURv2, setExportStringURv2] = useState(''); // used in QR
@ -92,9 +92,6 @@ const ViewEditMultisigCosigners: React.FC = () => {
textDestination: {
color: colors.foregroundColor,
},
modalContent: {
backgroundColor: colors.elevated,
},
exportButton: {
backgroundColor: colors.buttonDisabledBackgroundColor,
},
@ -164,10 +161,11 @@ const ViewEditMultisigCosigners: React.FC = () => {
);
const saveFileButtonAfterOnPress = () => {
setIsShareModalVisible(false);
shareModalRef.current?.dismiss();
};
const onSave = async () => {
dismissAllModals();
if (!wallet) {
throw new Error('Wallet is undefined');
}
@ -224,65 +222,63 @@ const ViewEditMultisigCosigners: React.FC = () => {
}, [walletID]),
);
const hideMnemonicsModal = () => {
Keyboard.dismiss();
setIsMnemonicsModalVisible(false);
};
const renderMnemonicsModal = () => {
return (
<BottomModal isVisible={isMnemonicsModalVisible} onClose={hideMnemonicsModal} coverScreen={false}>
<View style={[styles.newKeyModalContent, stylesHook.modalContent]}>
<View style={styles.itemKeyUnprovidedWrapper}>
<View style={[styles.vaultKeyCircleSuccess, stylesHook.vaultKeyCircleSuccess]}>
<Icon size={24} name="check" type="ionicons" color={colors.msSuccessCheck} />
</View>
<View style={styles.vaultKeyTextWrapper}>
<Text style={[styles.vaultKeyText, stylesHook.vaultKeyText]}>
{loc.formatString(loc.multisig.vault_key, { number: vaultKeyData.keyIndex })}
</Text>
</View>
<BottomModal
ref={mnemonicsModalRef}
footerDefaultMargins
backgroundColor={colors.elevated}
contentContainerStyle={styles.newKeyModalContent}
footer={
<>
<Button
title={loc.multisig.share}
onPress={() => {
shareModalRef.current?.present();
}}
/>
<BlueSpacing20 />
</>
}
>
<View style={styles.itemKeyUnprovidedWrapper}>
<View style={[styles.vaultKeyCircleSuccess, stylesHook.vaultKeyCircleSuccess]}>
<Icon size={24} name="check" type="ionicons" color={colors.msSuccessCheck} />
</View>
<View style={styles.vaultKeyTextWrapper}>
<Text style={[styles.vaultKeyText, stylesHook.vaultKeyText]}>
{loc.formatString(loc.multisig.vault_key, { number: vaultKeyData.keyIndex })}
</Text>
</View>
<BlueSpacing20 />
{vaultKeyData.xpub.length > 1 && (
<>
<Text style={[styles.textDestination, stylesHook.textDestination]}>{loc._.wallet_key}</Text>
<BlueSpacing10 />
<SquareEnumeratedWords
contentAlign={SquareEnumeratedWordsContentAlign.left}
entries={[vaultKeyData.xpub, vaultKeyData.fp, vaultKeyData.path]}
appendNumber={false}
/>
</>
)}
{vaultKeyData.seed.length > 1 && (
<>
<BlueSpacing20 />
<Text style={[styles.textDestination, stylesHook.textDestination]}>{loc._.seed}</Text>
<BlueSpacing10 />
<SquareEnumeratedWords
contentAlign={SquareEnumeratedWordsContentAlign.left}
entries={vaultKeyData.seed.split(' ')}
appendNumber
/>
{vaultKeyData.passphrase.length > 1 && (
<Text style={[styles.textDestination, stylesHook.textDestination]}>{vaultKeyData.passphrase}</Text>
)}
</>
)}
<BlueSpacing20 />
<Button
title={loc.multisig.share}
onPress={() => {
setIsMnemonicsModalVisible(false);
setTimeout(() => {
setIsShareModalVisible(true);
}, 1000);
}}
/>
<BlueSpacing20 />
<Button title={loc.send.success_done} onPress={() => setIsMnemonicsModalVisible(false)} />
</View>
<BlueSpacing20 />
{vaultKeyData.xpub.length > 1 && (
<>
<Text style={[styles.textDestination, stylesHook.textDestination]}>{loc._.wallet_key}</Text>
<BlueSpacing10 />
<SquareEnumeratedWords
contentAlign={SquareEnumeratedWordsContentAlign.left}
entries={[vaultKeyData.xpub, vaultKeyData.fp, vaultKeyData.path]}
appendNumber={false}
/>
</>
)}
{vaultKeyData.seed.length > 1 && (
<>
<BlueSpacing20 />
<Text style={[styles.textDestination, stylesHook.textDestination]}>{loc._.seed}</Text>
<BlueSpacing10 />
<SquareEnumeratedWords
contentAlign={SquareEnumeratedWordsContentAlign.left}
entries={vaultKeyData.seed.split(' ')}
appendNumber
/>
{vaultKeyData.passphrase.length > 1 && (
<Text style={[styles.textDestination, stylesHook.textDestination]}>{vaultKeyData.passphrase}</Text>
)}
</>
)}
{renderShareModal()}
</BottomModal>
);
};
@ -346,7 +342,7 @@ const ViewEditMultisigCosigners: React.FC = () => {
setExportString(MultisigCosigner.exportToJson(fp, xpub, path));
setExportStringURv2(encodeUR(MultisigCosigner.exportToJson(fp, xpub, path))[0]);
setExportFilename('bw-cosigner-' + fp + '.json');
setIsMnemonicsModalVisible(true);
mnemonicsModalRef.current?.present();
},
}}
dashes={MultipleStepsListItemDashType.topAndBottom}
@ -360,7 +356,7 @@ const ViewEditMultisigCosigners: React.FC = () => {
disabled: vaultKeyData.isLoading,
onPress: () => {
setCurrentlyEditingCosignerNum(el.index + 1);
setIsProvideMnemonicsModalVisible(true);
provideMnemonicsModalRef.current?.present();
},
}}
dashes={el.index === length - 1 ? MultipleStepsListItemDashType.top : MultipleStepsListItemDashType.topAndBottom}
@ -389,7 +385,7 @@ const ViewEditMultisigCosigners: React.FC = () => {
passphrase: passphrase ?? '',
isLoading: false,
});
setIsMnemonicsModalVisible(true);
mnemonicsModalRef.current?.present();
const fp = wallet.getFingerprint(keyIndex);
const path = wallet.getCustomDerivationPathForCosigner(keyIndex);
if (!path) {
@ -407,7 +403,6 @@ const ViewEditMultisigCosigners: React.FC = () => {
)}
<MultipleStepsListItem
useActionSheet
actionSheetOptions={{
options: [loc._.cancel, loc.multisig.confirm],
title: loc._.seed,
@ -451,6 +446,11 @@ const ViewEditMultisigCosigners: React.FC = () => {
);
};
const dismissAllModals = () => {
provideMnemonicsModalRef.current?.dismiss();
shareModalRef.current?.dismiss();
mnemonicsModalRef.current?.dismiss();
};
const handleUseMnemonicPhrase = async () => {
let passphrase;
if (askPassphrase) {
@ -485,7 +485,7 @@ const ViewEditMultisigCosigners: React.FC = () => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setWallet(wallet);
setIsProvideMnemonicsModalVisible(false);
provideMnemonicsModalRef.current?.dismiss();
setIsSaveButtonDisabled(false);
setImportText('');
setAskPassphrase(false);
@ -509,75 +509,83 @@ const ViewEditMultisigCosigners: React.FC = () => {
};
const scanOrOpenFile = async () => {
setIsProvideMnemonicsModalVisible(false);
const scanned = await scanQrHelper(route.name, true);
await provideMnemonicsModalRef.current?.dismiss();
const scanned = await scanQrHelper(route.name, true, undefined);
setImportText(String(scanned));
setIsProvideMnemonicsModalVisible(true);
provideMnemonicsModalRef.current?.present();
};
const hideProvideMnemonicsModal = () => {
Keyboard.dismiss();
setIsProvideMnemonicsModalVisible(false);
provideMnemonicsModalRef.current?.dismiss();
setImportText('');
setAskPassphrase(false);
};
const hideShareModal = () => {
setIsShareModalVisible(false);
};
const hideShareModal = () => {};
const renderProvideMnemonicsModal = () => {
return (
<BottomModal avoidKeyboard isVisible={isProvideMnemonicsModalVisible} onClose={hideProvideMnemonicsModal} coverScreen={false}>
<KeyboardAvoidingView enabled={!isTablet} behavior={Platform.OS === 'ios' ? 'position' : 'padding'} keyboardVerticalOffset={120}>
<View style={[styles.modalContent, stylesHook.modalContent]}>
<BlueTextCentered>{loc.multisig.type_your_mnemonics}</BlueTextCentered>
<BlueSpacing20 />
<BlueFormMultiInput value={importText} onChangeText={setImportText} />
{isAdvancedModeEnabled && (
<>
<BlueSpacing10 />
<View style={styles.row}>
<BlueText>{loc.wallets.import_passphrase}</BlueText>
<Switch testID="AskPassphrase" value={askPassphrase} onValueChange={setAskPassphrase} />
</View>
</>
)}
<BlueSpacing20 />
<BottomModal
onClose={hideProvideMnemonicsModal}
ref={provideMnemonicsModalRef}
contentContainerStyle={styles.newKeyModalContent}
backgroundColor={colors.elevated}
footerDefaultMargins
footer={
<>
{isLoading ? (
<ActivityIndicator />
) : (
<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} />
</View>
</KeyboardAvoidingView>
<>
<BlueButtonLink ref={openScannerButtonRef} disabled={isLoading} onPress={scanOrOpenFile} title={loc.wallets.import_scan_qr} />
<BlueSpacing20 />
</>
</>
}
>
<BlueTextCentered>{loc.multisig.type_your_mnemonics}</BlueTextCentered>
<BlueSpacing20 />
<BlueFormMultiInput value={importText} onChangeText={setImportText} />
{isAdvancedModeEnabled && (
<>
<BlueSpacing10 />
<View style={styles.row}>
<BlueText>{loc.wallets.import_passphrase}</BlueText>
<Switch testID="AskPassphrase" value={askPassphrase} onValueChange={setAskPassphrase} />
</View>
</>
)}
</BottomModal>
);
};
const renderShareModal = () => {
return (
<BottomModal isVisible={isShareModalVisible} onClose={hideShareModal} doneButton coverScreen={false}>
<KeyboardAvoidingView enabled={!isTablet} behavior={Platform.OS === 'ios' ? 'position' : undefined}>
<View style={[styles.modalContent, stylesHook.modalContent, 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>
<QRCodeComponent value={exportStringURv2} size={260} isLogoRendered={false} />
<BlueSpacing20 />
<View style={styles.squareButtonWrapper}>
<SaveFileButton
style={[styles.exportButton, stylesHook.exportButton]}
fileContent={exportString}
fileName={exportFilename}
afterOnPress={saveFileButtonAfterOnPress}
>
<SquareButton title={loc.multisig.share} />
</SaveFileButton>
</View>
</View>
</KeyboardAvoidingView>
<BottomModal
ref={shareModalRef}
onClose={hideShareModal}
contentContainerStyle={[styles.modalContent, styles.alignItemsCenter, styles.shareModalHeight]}
backgroundColor={colors.elevated}
footerDefaultMargins
footer={
<SaveFileButton
style={[styles.exportButton, stylesHook.exportButton]}
fileContent={exportString}
fileName={exportFilename}
afterOnPress={saveFileButtonAfterOnPress}
>
<SquareButton title={loc.multisig.share} />
</SaveFileButton>
}
>
<Text style={[styles.headerText, stylesHook.textDestination]}>
{loc.multisig.this_is_cosigners_xpub} {Platform.OS === 'ios' ? loc.multisig.this_is_cosigners_xpub_airdrop : ''}
</Text>
<QRCodeComponent value={exportStringURv2} size={260} isLogoRendered={false} />
<BlueSpacing20 />
</BottomModal>
);
};
@ -643,8 +651,6 @@ const ViewEditMultisigCosigners: React.FC = () => {
{renderProvideMnemonicsModal()}
{renderShareModal()}
{renderMnemonicsModal()}
</View>
);
@ -661,20 +667,12 @@ const styles = StyleSheet.create({
vaultKeyTextWrapper: { justifyContent: 'center', alignItems: 'center', paddingLeft: 16 },
newKeyModalContent: {
paddingHorizontal: 22,
paddingVertical: 32,
justifyContent: 'center',
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
borderColor: 'rgba(0, 0, 0, 0.1)',
paddingTop: 32,
minHeight: 370,
},
modalContent: {
padding: 22,
justifyContent: 'center',
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
borderColor: 'rgba(0, 0, 0, 0.1)',
backgroundColor: 'white',
minHeight: 400,
},
vaultKeyCircleSuccess: {
width: 42,
@ -686,14 +684,14 @@ const styles = StyleSheet.create({
exportButton: {
height: 48,
borderRadius: 8,
flex: 1,
justifyContent: 'center',
paddingHorizontal: 16,
marginBottom: 32,
},
headerText: { fontSize: 15, color: '#13244D' },
mainBlock: { marginHorizontal: 16 },
alignItemsCenter: { alignItems: 'center' },
squareButtonWrapper: { height: 50, width: 250 },
alignItemsCenter: { alignItems: 'center', justifyContent: 'space-between' },
shareModalHeight: { minHeight: 450 },
tipKeys: {
fontSize: 15,
fontWeight: '600',

View File

@ -1,11 +1,11 @@
import React, { useState } from 'react';
import React, { useRef, useState } from 'react';
import { useRoute, RouteProp } from '@react-navigation/native';
import LottieView from 'lottie-react-native';
import { Keyboard, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { Icon } from '@rneui/themed';
import { BlueSpacing20 } from '../../BlueComponents';
import { MultisigHDWallet } from '../../class';
import BottomModal from '../../components/BottomModal';
import BottomModal, { BottomModalHandle } from '../../components/BottomModal';
import Button from '../../components/Button';
import ListItem from '../../components/ListItem';
import SafeArea from '../../components/SafeArea';
@ -23,9 +23,9 @@ const WalletsAddMultisig: React.FC = () => {
const { colors } = useTheme();
const { navigate } = useExtendedNavigation<NavigationProps>();
const { walletLabel } = useRoute<RouteProps>().params;
const bottomModalRef = useRef<BottomModalHandle>(null);
const [m, setM] = useState(2);
const [n, setN] = useState(3);
const [isModalVisible, setIsModalVisible] = useState(false);
const [format, setFormat] = useState(MultisigHDWallet.FORMAT_P2WSH);
const { isAdvancedModeEnabled } = useSettings();
@ -38,16 +38,15 @@ const WalletsAddMultisig: React.FC = () => {
textdesc: {
color: colors.alternativeTextColor,
},
modalContentShort: {
backgroundColor: colors.elevated,
},
textSubtitle: {
color: colors.alternativeTextColor,
},
selectedItem: {
paddingHorizontal: 8,
backgroundColor: colors.elevated,
},
deSelectedItem: {
paddingHorizontal: 8,
backgroundColor: 'transparent',
},
textHeader: {
@ -56,6 +55,7 @@ const WalletsAddMultisig: React.FC = () => {
});
const onLetsStartPress = () => {
bottomModalRef.current?.dismiss();
navigate('WalletsAddMultisigStep2', { m, n, format, walletLabel });
};
@ -90,88 +90,78 @@ const WalletsAddMultisig: React.FC = () => {
setN(n - 1);
};
const closeModal = () => {
Keyboard.dismiss();
setIsModalVisible(false);
};
const renderModal = () => {
return (
<BottomModal isVisible={isModalVisible} onClose={closeModal} doneButton propagateSwipe>
<View style={[styles.modalContentShort, stylesHook.modalContentShort]}>
<ScrollView>
<Text style={[styles.textHeader, stylesHook.textHeader]}>{loc.multisig.quorum_header}</Text>
<Text style={[styles.textSubtitle, stylesHook.textSubtitle]}>{loc.multisig.required_keys_out_of_total}</Text>
<View style={styles.rowCenter}>
<View style={styles.column}>
<TouchableOpacity accessibilityRole="button" onPress={increaseM} disabled={n === m || m === 7} style={styles.chevron}>
<Icon
name="chevron-up"
size={22}
type="font-awesome-5"
color={n === m || m === 7 ? colors.buttonDisabledTextColor : '#007AFF'}
/>
</TouchableOpacity>
<Text style={[styles.textM, stylesHook.textHeader]}>{m}</Text>
<TouchableOpacity accessibilityRole="button" onPress={decreaseM} disabled={m === 2} style={styles.chevron}>
<Icon name="chevron-down" size={22} type="font-awesome-5" color={m === 2 ? colors.buttonDisabledTextColor : '#007AFF'} />
</TouchableOpacity>
</View>
<BottomModal
sizes={['auto', 'large']}
ref={bottomModalRef}
contentContainerStyle={styles.modalContentShort}
backgroundColor={colors.elevated}
>
<Text style={[styles.textHeader, stylesHook.textHeader]}>{loc.multisig.quorum_header}</Text>
<Text style={[styles.textSubtitle, stylesHook.textSubtitle]}>{loc.multisig.required_keys_out_of_total}</Text>
<View style={styles.rowCenter}>
<View style={styles.column}>
<TouchableOpacity accessibilityRole="button" onPress={increaseM} disabled={n === m || m === 7} style={styles.chevron}>
<Icon
name="chevron-up"
size={22}
type="font-awesome-5"
color={n === m || m === 7 ? colors.buttonDisabledTextColor : '#007AFF'}
/>
</TouchableOpacity>
<Text style={[styles.textM, stylesHook.textHeader]}>{m}</Text>
<TouchableOpacity accessibilityRole="button" onPress={decreaseM} disabled={m === 2} style={styles.chevron}>
<Icon name="chevron-down" size={22} type="font-awesome-5" color={m === 2 ? colors.buttonDisabledTextColor : '#007AFF'} />
</TouchableOpacity>
</View>
<View style={styles.columnOf}>
<Text style={styles.textOf}>{loc.multisig.of}</Text>
</View>
<View style={styles.columnOf}>
<Text style={styles.textOf}>{loc.multisig.of}</Text>
</View>
<View style={styles.column}>
<TouchableOpacity accessibilityRole="button" disabled={n === 7} onPress={increaseN} style={styles.chevron}>
<Icon name="chevron-up" size={22} type="font-awesome-5" color={n === 7 ? colors.buttonDisabledTextColor : '#007AFF'} />
</TouchableOpacity>
<Text style={[styles.textM, stylesHook.textHeader]}>{n}</Text>
<TouchableOpacity
accessibilityRole="button"
onPress={decreaseN}
disabled={n === m}
style={styles.chevron}
testID="DecreaseN"
>
<Icon name="chevron-down" size={22} type="font-awesome-5" color={n === m ? colors.buttonDisabledTextColor : '#007AFF'} />
</TouchableOpacity>
</View>
</View>
<BlueSpacing20 />
<Text style={[styles.textHeader, stylesHook.textHeader]}>{loc.multisig.wallet_type}</Text>
<BlueSpacing20 />
<ListItem
bottomDivider={false}
onPress={setFormatP2wsh}
title={`${loc.multisig.native_segwit_title} (${MultisigHDWallet.FORMAT_P2WSH})`}
checkmark={isP2wsh()}
containerStyle={[styles.borderRadius6, styles.item, isP2wsh() ? stylesHook.selectedItem : stylesHook.deSelectedItem]}
/>
<ListItem
bottomDivider={false}
onPress={setFormatP2shP2wsh}
title={`${loc.multisig.wrapped_segwit_title} (${MultisigHDWallet.FORMAT_P2SH_P2WSH})`}
checkmark={isP2shP2wsh()}
containerStyle={[styles.borderRadius6, styles.item, isP2shP2wsh() ? stylesHook.selectedItem : stylesHook.deSelectedItem]}
/>
<ListItem
bottomDivider={false}
onPress={setFormatP2sh}
title={`${loc.multisig.legacy_title} (${MultisigHDWallet.FORMAT_P2SH})`}
checkmark={isP2sh()}
containerStyle={[styles.borderRadius6, styles.item, isP2sh() ? stylesHook.selectedItem : stylesHook.deSelectedItem]}
/>
</ScrollView>
<View style={styles.column}>
<TouchableOpacity accessibilityRole="button" disabled={n === 7} onPress={increaseN} style={styles.chevron}>
<Icon name="chevron-up" size={22} type="font-awesome-5" color={n === 7 ? colors.buttonDisabledTextColor : '#007AFF'} />
</TouchableOpacity>
<Text style={[styles.textM, stylesHook.textHeader]}>{n}</Text>
<TouchableOpacity accessibilityRole="button" onPress={decreaseN} disabled={n === m} style={styles.chevron} testID="DecreaseN">
<Icon name="chevron-down" size={22} type="font-awesome-5" color={n === m ? colors.buttonDisabledTextColor : '#007AFF'} />
</TouchableOpacity>
</View>
</View>
<BlueSpacing20 />
<Text style={[styles.textHeader, stylesHook.textHeader]}>{loc.multisig.wallet_type}</Text>
<BlueSpacing20 />
<ListItem
bottomDivider={false}
onPress={setFormatP2wsh}
title={`${loc.multisig.native_segwit_title} (${MultisigHDWallet.FORMAT_P2WSH})`}
checkmark={isP2wsh()}
containerStyle={[styles.borderRadius6, styles.item, isP2wsh() ? stylesHook.selectedItem : stylesHook.deSelectedItem]}
/>
<ListItem
bottomDivider={false}
onPress={setFormatP2shP2wsh}
title={`${loc.multisig.wrapped_segwit_title} (${MultisigHDWallet.FORMAT_P2SH_P2WSH})`}
checkmark={isP2shP2wsh()}
containerStyle={[styles.borderRadius6, styles.item, isP2shP2wsh() ? stylesHook.selectedItem : stylesHook.deSelectedItem]}
/>
<ListItem
bottomDivider={false}
onPress={setFormatP2sh}
title={`${loc.multisig.legacy_title} (${MultisigHDWallet.FORMAT_P2SH})`}
checkmark={isP2sh()}
containerStyle={[styles.borderRadius6, styles.item, isP2sh() ? stylesHook.selectedItem : stylesHook.deSelectedItem]}
/>
</BottomModal>
);
};
const showAdvancedOptionsModal = () => {
setIsModalVisible(true);
bottomModalRef.current?.present();
};
const getCurrentlySelectedFormat = (code: string) => {
@ -246,13 +236,7 @@ const styles = StyleSheet.create({
flex: 0.8,
},
modalContentShort: {
paddingHorizontal: 24,
paddingTop: 24,
justifyContent: 'center',
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
borderColor: 'rgba(0, 0, 0, 0.1)',
minHeight: 350,
padding: 24,
},
borderRadius6: {
borderRadius: 6,

View File

@ -1,12 +1,10 @@
import { useFocusEffect, useNavigation, useRoute } from '@react-navigation/native';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useFocusEffect, useRoute } from '@react-navigation/native';
import {
ActivityIndicator,
FlatList,
I18nManager,
InteractionManager,
Keyboard,
KeyboardAvoidingView,
LayoutAnimation,
Platform,
StyleSheet,
@ -16,7 +14,6 @@ import {
View,
} from 'react-native';
import { Icon } from '@rneui/themed';
import A from '../../blue_modules/analytics';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import { encodeUR } from '../../blue_modules/ur';
@ -35,11 +32,12 @@ import { SquareButton } from '../../components/SquareButton';
import { useTheme } from '../../components/themes';
import confirm from '../../helpers/confirm';
import prompt from '../../helpers/prompt';
import { scanQrHelper } from '../../helpers/scan-qr';
import usePrivacy from '../../hooks/usePrivacy';
import loc from '../../loc';
import { useStorage } from '../../hooks/context/useStorage';
import { useSettings } from '../../hooks/context/useSettings';
import { scanQrHelper } from '../../helpers/scan-qr';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
const staticCache = {};
@ -48,15 +46,15 @@ const WalletsAddMultisigStep2 = () => {
const { isAdvancedModeEnabled } = useSettings();
const { colors } = useTheme();
const navigation = useNavigation();
const { navigate, navigateToWalletsList } = useExtendedNavigation();
const { m, n, format, walletLabel } = useRoute().params;
const { name } = useRoute();
const [cosigners, setCosigners] = useState([]); // array of cosigners user provided. if format [cosigner, fp, path]
const [isLoading, setIsLoading] = useState(false);
const [isMnemonicsModalVisible, setIsMnemonicsModalVisible] = useState(false);
const [isProvideMnemonicsModalVisible, setIsProvideMnemonicsModalVisible] = useState(false);
const [isRenderCosignersXpubModalVisible, setIsRenderCosignersXpubModalVisible] = useState(false);
const mnemonicsModalRef = useRef(null);
const provideMnemonicsModalRef = useRef(null);
const renderCosignersXpubModalRef = useRef(null);
const [cosignerXpub, setCosignerXpub] = useState(''); // string used in exportCosigner()
const [cosignerXpubURv2, setCosignerXpubURv2] = useState(''); // string displayed in renderCosignersXpubModal()
const [cosignerXpubFilename, setCosignerXpubFilename] = useState('bw-cosigner.bwcosigner');
@ -82,7 +80,7 @@ const WalletsAddMultisigStep2 = () => {
(async function () {
if (await confirm(loc.multisig.shared_key_detected, loc.multisig.shared_key_detected_question)) {
setImportText(currentSharedCosigner);
setIsProvideMnemonicsModalVisible(true);
provideMnemonicsModalRef.current.present();
setSharedCosigner('');
}
})();
@ -90,8 +88,20 @@ const WalletsAddMultisigStep2 = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentSharedCosigner]);
const handleOnHelpPress = () => {
navigation.navigate('WalletsAddMultisigHelp');
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);
}
};
const stylesHook = StyleSheet.create({
@ -101,9 +111,6 @@ const WalletsAddMultisigStep2 = () => {
textDestination: {
color: colors.foregroundColor,
},
modalContent: {
backgroundColor: colors.modal,
},
exportButton: {
backgroundColor: colors.buttonDisabledBackgroundColor,
},
@ -157,6 +164,7 @@ const WalletsAddMultisigStep2 = () => {
w.setDerivationPath(MultisigHDWallet.PATH_LEGACY);
break;
default:
console.error('Unexpected format:', format);
throw new Error('This should never happen');
}
for (const cc of cosigners) {
@ -172,7 +180,7 @@ const WalletsAddMultisigStep2 = () => {
await saveToDisk();
A(A.ENUM.CREATED_WALLET);
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
navigation.getParent().goBack();
navigateToWalletsList();
};
const generateNewKey = () => {
@ -184,8 +192,7 @@ const WalletsAddMultisigStep2 = () => {
setCosigners(cosignersCopy);
setVaultKeyData({ keyIndex: cosignersCopy.length, seed: w.getSecret(), xpub: w.getXpub(), isLoading: false });
setIsLoading(true);
setIsMnemonicsModalVisible(true);
mnemonicsModalRef.current.present();
setTimeout(() => {
// filling cache
setXpubCacheForMnemonics(w.getSecret());
@ -209,6 +216,7 @@ const WalletsAddMultisigStep2 = () => {
path = MultisigHDWallet.PATH_LEGACY;
break;
default:
console.error('Unexpected format:', format);
throw new Error('This should never happen');
}
return path;
@ -219,7 +227,7 @@ const WalletsAddMultisigStep2 = () => {
setCosignerXpub(MultisigCosigner.exportToJson(cosigner[1], cosigner[0], cosigner[2]));
setCosignerXpubURv2(encodeUR(MultisigCosigner.exportToJson(cosigner[1], cosigner[0], cosigner[2]))[0]);
setCosignerXpubFilename('bw-cosigner-' + cosigner[1] + '.bwcosigner');
setIsRenderCosignersXpubModalVisible(true);
renderCosignersXpubModalRef.current.present();
} else {
const path = getPath();
@ -228,7 +236,7 @@ const WalletsAddMultisigStep2 = () => {
setCosignerXpub(MultisigCosigner.exportToJson(fp, xpub, path));
setCosignerXpubURv2(encodeUR(MultisigCosigner.exportToJson(fp, xpub, path))[0]);
setCosignerXpubFilename('bw-cosigner-' + fp + '.bwcosigner');
setIsRenderCosignersXpubModalVisible(true);
renderCosignersXpubModalRef.current.present();
}
};
@ -255,12 +263,12 @@ const WalletsAddMultisigStep2 = () => {
};
const iHaveMnemonics = () => {
setIsProvideMnemonicsModalVisible(true);
provideMnemonicsModalRef.current.present();
};
const tryUsingXpub = async (xpub, fp, path) => {
if (!MultisigHDWallet.isXpubForMultisig(xpub)) {
setIsProvideMnemonicsModalVisible(false);
provideMnemonicsModalRef.current.dismiss();
setIsLoading(false);
setImportText('');
setAskPassphrase(false);
@ -294,7 +302,7 @@ const WalletsAddMultisigStep2 = () => {
}
}
setIsProvideMnemonicsModalVisible(false);
provideMnemonicsModalRef.current.dismiss();
setIsLoading(false);
setImportText('');
setAskPassphrase(false);
@ -350,7 +358,7 @@ const WalletsAddMultisigStep2 = () => {
if (Platform.OS !== 'android') LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setCosigners(cosignersCopy);
setIsProvideMnemonicsModalVisible(false);
provideMnemonicsModalRef.current.dismiss();
setIsLoading(false);
setImportText('');
setAskPassphrase(false);
@ -379,8 +387,10 @@ const WalletsAddMultisigStep2 = () => {
if (ret.data.toUpperCase().startsWith('UR')) {
presentAlert({ message: 'BC-UR not decoded. This should never happen' });
} else if (isValidMnemonicSeed(ret.data)) {
setIsProvideMnemonicsModalVisible(true);
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 });
@ -390,7 +400,7 @@ const WalletsAddMultisigStep2 = () => {
}
let cosigner = new MultisigCosigner(ret.data);
if (!cosigner.isValid()) return presentAlert({ message: loc.multisig.invalid_cosigner });
setIsProvideMnemonicsModalVisible(false);
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
@ -416,6 +426,7 @@ const WalletsAddMultisigStep2 = () => {
}
break;
default:
console.error('Unexpected format:', format);
throw new Error('This should never happen');
}
}
@ -446,6 +457,7 @@ const WalletsAddMultisigStep2 = () => {
}
break;
default:
console.error('Unexpected format:', format);
throw new Error('This should never happen');
}
@ -458,12 +470,10 @@ const WalletsAddMultisigStep2 = () => {
}
};
const scanOrOpenFile = () => {
setIsProvideMnemonicsModalVisible(false);
InteractionManager.runAfterInteractions(async () => {
const scanned = await scanQrHelper(name, true);
onBarScanned({ data: scanned });
});
const scanOrOpenFile = async () => {
await provideMnemonicsModalRef.current.dismiss();
const scanned = await scanQrHelper(name, true, undefined);
onBarScanned({ data: scanned });
};
const dashType = ({ index, lastIndex, isChecked, isFocus }) => {
@ -562,75 +572,94 @@ const WalletsAddMultisigStep2 = () => {
const renderMnemonicsModal = () => {
return (
<BottomModal isVisible={isMnemonicsModalVisible} onClose={Keyboard.dismiss}>
<View style={[styles.newKeyModalContent, stylesHook.modalContent]}>
<View style={styles.itemKeyUnprovidedWrapper}>
<View style={[styles.vaultKeyCircleSuccess, stylesHook.vaultKeyCircleSuccess]}>
<Icon size={24} name="check" type="ionicons" color={colors.msSuccessCheck} />
</View>
<View style={styles.vaultKeyTextWrapper}>
<Text style={[styles.vaultKeyText, stylesHook.vaultKeyText]}>
{loc.formatString(loc.multisig.vault_key, { number: vaultKeyData.keyIndex })}
</Text>
</View>
<BottomModal
ref={mnemonicsModalRef}
isGrabberVisible={false}
dismissible={false}
showCloseButton={!isLoading}
footerDefaultMargins
backgroundColor={colors.modal}
contentContainerStyle={styles.newKeyModalContent}
footer={
<View style={styles.modalFooterBottomPadding}>
{isLoading ? (
<ActivityIndicator />
) : (
<Button title={loc.send.success_done} onPress={() => mnemonicsModalRef.current.dismiss()} />
)}
</View>
}
>
<View style={styles.itemKeyUnprovidedWrapper}>
<View style={[styles.vaultKeyCircleSuccess, stylesHook.vaultKeyCircleSuccess]}>
<Icon size={24} name="check" type="ionicons" color={colors.msSuccessCheck} />
</View>
<View style={styles.vaultKeyTextWrapper}>
<Text style={[styles.vaultKeyText, stylesHook.vaultKeyText]}>
{loc.formatString(loc.multisig.vault_key, { number: vaultKeyData.keyIndex })}
</Text>
</View>
<BlueSpacing20 />
<Text style={[styles.headerText, stylesHook.textDestination]}>{loc.multisig.wallet_key_created}</Text>
<BlueSpacing20 />
<Text style={[styles.textDestination, stylesHook.textDestination]}>{loc._.seed}</Text>
<BlueSpacing10 />
<View style={styles.secretContainer}>{renderSecret(vaultKeyData.seed.split(' '))}</View>
<BlueSpacing20 />
{isLoading ? <ActivityIndicator /> : <Button title={loc.send.success_done} onPress={() => setIsMnemonicsModalVisible(false)} />}
</View>
<BlueSpacing20 />
<Text style={[styles.headerText, stylesHook.textDestination]}>{loc.multisig.wallet_key_created}</Text>
<BlueSpacing20 />
<Text style={[styles.textDestination, stylesHook.textDestination]}>{loc._.seed}</Text>
<BlueSpacing10 />
<View style={styles.secretContainer}>{renderSecret(vaultKeyData.seed.split(' '))}</View>
<BlueSpacing20 />
</BottomModal>
);
};
const hideProvideMnemonicsModal = () => {
Keyboard.dismiss();
setIsProvideMnemonicsModalVisible(false);
setImportText('');
setAskPassphrase(false);
};
const renderProvideMnemonicsModal = () => {
return (
<BottomModal isVisible={isProvideMnemonicsModalVisible} onClose={hideProvideMnemonicsModal}>
<KeyboardAvoidingView enabled={!Platform.isPad} behavior={Platform.OS === 'ios' ? 'position' : null}>
<View style={[styles.modalContent, stylesHook.modalContent]}>
<BlueTextCentered>{loc.multisig.type_your_mnemonics}</BlueTextCentered>
<BlueSpacing20 />
<BlueFormMultiInput value={importText} onChangeText={setImportText} />
{isAdvancedModeEnabled && (
<>
<BlueSpacing10 />
<View style={styles.row}>
<BlueText>{loc.wallets.import_passphrase}</BlueText>
<Switch testID="AskPassphrase" value={askPassphrase} onValueChange={setAskPassphrase} />
</View>
</>
)}
<BlueSpacing20 />
<BottomModal
footerDefaultMargins
footer={
<View style={styles.modalFooterBottomPadding}>
{isLoading ? (
<ActivityIndicator />
) : (
<Button
testID="DoImportKeyButton"
disabled={importText.trim().length === 0}
title={loc.wallets.import_do_import}
onPress={useMnemonicPhrase}
/>
<>
<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}
/>
</>
)}
<BlueButtonLink
testID="ScanOrOpenFile"
ref={openScannerButton}
disabled={isLoading}
onPress={scanOrOpenFile}
title={loc.wallets.import_scan_qr}
/>
</View>
</KeyboardAvoidingView>
}
ref={provideMnemonicsModalRef}
backgroundColor={colors.modal}
isGrabberVisible={false}
contentContainerStyle={styles.modalContent}
onDismiss={() => {
Keyboard.dismiss();
setImportText('');
setAskPassphrase(false);
}}
>
<BlueTextCentered>{loc.multisig.type_your_mnemonics}</BlueTextCentered>
<BlueSpacing20 />
<BlueFormMultiInput value={importText} onChangeText={setImportText} />
{isAdvancedModeEnabled && (
<>
<BlueSpacing10 />
<View style={styles.row}>
<BlueText>{loc.wallets.import_passphrase}</BlueText>
<Switch testID="AskPassphrase" value={askPassphrase} onValueChange={setAskPassphrase} />
</View>
</>
)}
</BottomModal>
);
};
@ -645,37 +674,41 @@ const WalletsAddMultisigStep2 = () => {
const hideCosignersXpubModal = () => {
Keyboard.dismiss();
setIsRenderCosignersXpubModalVisible(false);
renderCosignersXpubModalRef.current.dismiss();
};
const renderCosignersXpubModal = () => {
return (
<BottomModal isVisible={isRenderCosignersXpubModalVisible} onClose={hideCosignersXpubModal}>
<KeyboardAvoidingView enabled={!Platform.isPad} behavior={Platform.OS === 'ios' ? 'position' : null}>
<View style={[styles.modalContent, stylesHook.modalContent, 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={cosignerXpubURv2} size={260} />
<BlueSpacing20 />
<View style={styles.squareButtonWrapper}>
{isLoading ? (
<ActivityIndicator />
) : (
<SaveFileButton
style={[styles.exportButton, stylesHook.exportButton]}
fileName={cosignerXpubFilename}
fileContent={cosignerXpub}
beforeOnPress={exportCosignerBeforeOnPress}
afterOnPress={exportCosignerAfterOnPress}
>
<SquareButton title={loc.multisig.share} />
</SaveFileButton>
)}
</View>
<BottomModal
onClose={hideCosignersXpubModal}
ref={renderCosignersXpubModalRef}
backgroundColor={colors.modal}
footerDefaultMargins
contentContainerStyle={[styles.modalContent, styles.alignItemsCenter]}
footer={
<View style={styles.modalFooterBottomPadding}>
{isLoading ? (
<ActivityIndicator />
) : (
<SaveFileButton
style={[styles.exportButton, stylesHook.exportButton]}
fileName={cosignerXpubFilename}
fileContent={cosignerXpub}
beforeOnPress={exportCosignerBeforeOnPress}
afterOnPress={exportCosignerAfterOnPress}
>
<SquareButton title={loc.multisig.share} />
</SaveFileButton>
)}
</View>
</KeyboardAvoidingView>
}
>
<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={cosignerXpubURv2} size={260} />
<BlueSpacing20 />
</BottomModal>
);
};
@ -740,20 +773,14 @@ const styles = StyleSheet.create({
paddingHorizontal: 22,
paddingVertical: 32,
justifyContent: 'center',
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
borderColor: 'rgba(0, 0, 0, 0.1)',
minHeight: 400,
minHeight: 450,
},
newKeyModalContent: {
paddingHorizontal: 22,
paddingBottom: 60,
paddingTop: 50,
justifyContent: 'center',
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
borderColor: 'rgba(0, 0, 0, 0.1)',
minHeight: 450,
},
modalFooterBottomPadding: { paddingBottom: 26 },
vaultKeyCircleSuccess: {
width: 42,
height: 42,
@ -782,13 +809,11 @@ const styles = StyleSheet.create({
exportButton: {
height: 48,
borderRadius: 8,
flex: 1,
justifyContent: 'center',
paddingHorizontal: 16,
},
headerText: { fontSize: 15, color: '#13244D' },
alignItemsCenter: { alignItems: 'center' },
squareButtonWrapper: { height: 50, width: 250 },
helpButtonWrapper: {
alignItems: 'flex-end',
flexDirection: I18nManager.isRTL ? 'row' : 'row-reverse',

View File

@ -26,7 +26,7 @@ import * as fs from '../../blue_modules/fs';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import { LightningCustodianWallet, LightningLdkWallet, MultisigHDWallet, WatchOnlyWallet } from '../../class';
import WalletGradient from '../../class/wallet-gradient';
import presentAlert from '../../components/Alert';
import presentAlert, { AlertType } from '../../components/Alert';
import { FButton, FContainer } from '../../components/FloatButtons';
import LNNodeBar from '../../components/LNNodeBar';
import navigationStyle from '../../components/navigationStyle';
@ -241,7 +241,7 @@ const WalletTransactions = ({ navigation }) => {
console.log(wallet.getLabel(), 'fetch tx took', (end - start) / 1000, 'sec');
} catch (err) {
noErr = false;
presentAlert({ message: err.message });
presentAlert({ message: err.message, type: AlertType.Toast });
setIsLoading(false);
setTimeElapsed(prev => prev + 1);
}
@ -303,7 +303,7 @@ const WalletTransactions = ({ navigation }) => {
await wallet.fetchBtcAddress();
toAddress = wallet.refill_addressess[0];
} catch (Err) {
return presentAlert({ message: Err.message });
return presentAlert({ message: Err.message, type: AlertType.Toast });
}
}
navigate('SendDetailsRoot', {

View File

@ -206,9 +206,11 @@ describe('BlueWallet UI Tests - no wallets', () => {
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')).tapReturnKey();
await element(by.id('CustomAmountSaveButton')).tap();
await sup('1 BTC');
await sup('test');
await expect(element(by.id('CustomAmountDescriptionText'))).toHaveText('test');
await expect(element(by.id('BitcoinAmountText'))).toHaveText('1 BTC');
await yo('BitcoinAddressQRCodeContainer');
await yo('CopyTextToClipboard');
await device.pressBack();
@ -575,6 +577,7 @@ describe('BlueWallet UI Tests - no wallets', () => {
await element(by.id('AddressInput')).replaceText('bc1q063ctu6jhe5k4v8ka99qac8rcm2tzjjnuktyrl');
await element(by.id('BitcoinAmountInput')).typeText('0.0005\n');
await element(by.id('BitcoinAmountInput')).tapReturnKey();
// setting fee rate:
const feeRate = 3;

View File

@ -501,7 +501,7 @@ describe('BlueWallet UI Tests - import BIP84 wallet', () => {
await element(by.text('test1')).atIndex(0).tap();
await element(by.id('OutputMemo')).replaceText('test2');
await element(by.type('android.widget.CompoundButton')).tap(); // freeze switch
await device.pressBack(); // closing modal
await element(by.id('ModalDoneButton')).tap();
await expect(element(by.text('test2')).atIndex(0)).toBeVisible();
await expect(element(by.text('Freeze')).atIndex(0)).toBeVisible();

View File

@ -1,4 +1,4 @@
import { hashIt, helperDeleteWallet, helperImportWallet, sleep, sup, yo } from './helperz';
import { hashIt, helperDeleteWallet, helperImportWallet, sleep, yo } from './helperz';
beforeAll(async () => {
// reinstalling the app just for any case to clean up app's storage
@ -42,9 +42,11 @@ describe('BlueWallet UI Tests - import Watch-only wallet (zpub)', () => {
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')).tapReturnKey();
await element(by.id('CustomAmountSaveButton')).tap();
await sup('1 BTC');
await sup('Test');
await expect(element(by.id('CustomAmountDescriptionText'))).toHaveText('Test');
await expect(element(by.id('BitcoinAmountText'))).toHaveText('1 BTC');
await expect(element(by.id('BitcoinAddressQRCodeContainer'))).toBeVisible();
await expect(element(by.text('bitcoin:bc1qc8wun6lf9vcajpddtgdpd2pdrp0kwp29j6upgv?amount=1&label=Test'))).toBeVisible();

View File

@ -78,6 +78,8 @@ jest.mock('react-native-default-preference', () => {
};
});
jest.mock('@lodev09/react-native-true-sheet');
jest.mock('react-native-fs', () => {
return {
mkdir: jest.fn(),