Merge pull request #7165 from BlueWallet/isit

REF: IsItMyAddress to TSX
This commit is contained in:
GLaDOS 2024-10-15 22:24:36 +00:00 committed by GitHub
commit a0cb72a74f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 288 additions and 155 deletions

View File

@ -14,7 +14,7 @@ import LnurlAuth from '../screen/lnd/lnurlAuth';
import LnurlPay from '../screen/lnd/lnurlPay';
import LnurlPaySuccess from '../screen/lnd/lnurlPaySuccess';
import Broadcast from '../screen/send/Broadcast';
import IsItMyAddress from '../screen/send/isItMyAddress';
import IsItMyAddress from '../screen/settings/IsItMyAddress';
import Success from '../screen/send/success';
import CPFP from '../screen/transactions/CPFP';
import TransactionDetails from '../screen/transactions/TransactionDetails';
@ -199,6 +199,7 @@ const DetailViewStackScreensStack = () => {
<DetailViewStack.Screen
name="IsItMyAddress"
component={IsItMyAddress}
initialParams={{ address: undefined }}
options={navigationStyle({ title: loc.is_it_my_address.title })(theme)}
/>
<DetailViewStack.Screen

View File

@ -16,7 +16,7 @@ export type DetailViewStackParamList = {
LNDViewAdditionalInvoiceInformation: { invoiceId: string };
LNDViewAdditionalInvoicePreImage: { invoiceId: string };
Broadcast: { scannedData?: string };
IsItMyAddress: undefined;
IsItMyAddress: { address?: string };
GenerateWord: undefined;
LnurlPay: undefined;
LnurlPaySuccess: {
@ -77,7 +77,7 @@ export type DetailViewStackParamList = {
ReceiveDetailsRoot: {
screen: 'ReceiveDetails';
params: {
walletID: string;
walletID?: string;
address: string;
};
};

View File

@ -1,151 +0,0 @@
import React, { useRef, useState } from 'react';
import { useRoute } from '@react-navigation/native';
import { Keyboard, StyleSheet, TextInput, View } from 'react-native';
import { BlueButtonLink, BlueCard, BlueSpacing10, BlueSpacing20, BlueText } from '../../BlueComponents';
import Button from '../../components/Button';
import SafeArea from '../../components/SafeArea';
import { useTheme } from '../../components/themes';
import { scanQrHelper } from '../../helpers/scan-qr';
import loc from '../../loc';
import { useStorage } from '../../hooks/context/useStorage';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
const IsItMyAddress = () => {
/** @type {AbstractWallet[]} */
const { wallets } = useStorage();
const { navigate } = useExtendedNavigation();
const { name } = useRoute();
const { colors } = useTheme();
const scanButtonRef = useRef();
const [address, setAddress] = useState('');
const [result, setResult] = useState('');
const [resultCleanAddress, setResultCleanAddress] = useState();
const stylesHooks = StyleSheet.create({
input: {
borderColor: colors.formBorder,
borderBottomColor: colors.formBorder,
backgroundColor: colors.inputBackgroundColor,
},
});
const handleUpdateAddress = nextValue => setAddress(nextValue.trim());
const checkAddress = () => {
Keyboard.dismiss();
const cleanAddress = address.replace('bitcoin:', '').replace('BITCOIN:', '').replace('bitcoin=', '').split('?')[0];
const _result = [];
for (const w of wallets) {
if (w.weOwnAddress(cleanAddress)) {
setResultCleanAddress(cleanAddress);
_result.push(loc.formatString(loc.is_it_my_address.owns, { label: w.getLabel(), address: cleanAddress }));
}
}
if (_result.length === 0) {
setResult(_result.push(loc.is_it_my_address.no_wallet_owns_address));
setResultCleanAddress();
}
setResult(_result.join('\n\n'));
};
const onBarScanned = value => {
setAddress(value);
setResultCleanAddress(value);
};
const importScan = () => {
scanQrHelper(name, true, onBarScanned);
};
const clearAddressInput = () => {
setAddress('');
setResult();
setResultCleanAddress();
};
const viewQRCode = () => {
navigate('ReceiveDetailsRoot', {
screen: 'ReceiveDetails',
params: {
address: resultCleanAddress,
},
});
};
return (
<SafeArea style={styles.blueArea}>
<View style={styles.wrapper}>
<BlueCard style={styles.mainCard}>
<View style={[styles.input, stylesHooks.input]}>
<TextInput
style={styles.text}
maxHeight={100}
minHeight={100}
maxWidth="100%"
minWidth="100%"
multiline
editable
placeholder={loc.is_it_my_address.enter_address}
placeholderTextColor="#81868e"
value={address}
onChangeText={handleUpdateAddress}
testID="AddressInput"
/>
</View>
<BlueSpacing10 />
<BlueButtonLink ref={scanButtonRef} title={loc.wallets.import_scan_qr} onPress={importScan} />
<BlueSpacing10 />
<Button title={loc.send.input_clear} onPress={clearAddressInput} />
<BlueSpacing20 />
{resultCleanAddress && (
<>
<Button title={loc.is_it_my_address.view_qrcode} onPress={viewQRCode} />
<BlueSpacing20 />
</>
)}
<Button
disabled={address.trim().length === 0}
title={loc.is_it_my_address.check_address}
onPress={checkAddress}
testID="CheckAddress"
/>
<BlueSpacing20 />
<BlueText testID="Result">{result}</BlueText>
</BlueCard>
</View>
</SafeArea>
);
};
export default IsItMyAddress;
const styles = StyleSheet.create({
wrapper: {
marginTop: 16,
alignItems: 'center',
justifyContent: 'flex-start',
},
mainCard: {
padding: 0,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'flex-start',
},
input: {
flexDirection: 'row',
borderWidth: 1,
borderBottomWidth: 0.5,
alignItems: 'center',
borderRadius: 4,
},
text: {
padding: 8,
minHeight: 33,
color: '#81868e',
},
});

View File

@ -0,0 +1,282 @@
import React, { useRef, useState, useEffect } from 'react';
import { useRoute, useNavigation, RouteProp } from '@react-navigation/native';
import { Keyboard, StyleSheet, TextInput, View, ScrollView, TouchableOpacity, Text } from 'react-native';
import { BlueButtonLink, BlueCard, BlueSpacing10, BlueSpacing20, BlueSpacing40, BlueText } from '../../BlueComponents';
import Button from '../../components/Button';
import { useTheme } from '../../components/themes';
import { scanQrHelper } from '../../helpers/scan-qr';
import loc from '../../loc';
import { useStorage } from '../../hooks/context/useStorage';
import { TWallet } from '../../class/wallets/types';
import { WalletCarouselItem } from '../../components/WalletsCarousel';
import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import Icon from 'react-native-vector-icons/MaterialIcons';
import { Divider } from '@rneui/themed';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import presentAlert from '../../components/Alert';
type RouteProps = RouteProp<DetailViewStackParamList, 'IsItMyAddress'>;
type NavigationProp = NativeStackNavigationProp<DetailViewStackParamList, 'IsItMyAddress'>;
const IsItMyAddress: React.FC = () => {
const { wallets } = useStorage();
const navigation = useNavigation<NavigationProp>();
const route = useRoute<RouteProps>();
const { colors } = useTheme();
const scanButtonRef = useRef<any>();
const scrollViewRef = useRef<ScrollView>(null);
const firstWalletRef = useRef<View>(null);
const [address, setAddress] = useState<string>('');
const [matchingWallets, setMatchingWallets] = useState<TWallet[] | undefined>();
const [resultCleanAddress, setResultCleanAddress] = useState<string | undefined>();
const stylesHooks = StyleSheet.create({
input: {
borderColor: colors.formBorder,
borderBottomColor: colors.formBorder,
backgroundColor: colors.inputBackgroundColor,
},
});
useEffect(() => {
if (route.params?.address && route.params.address !== address) {
setAddress(route.params.address);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [route.params?.address]);
useEffect(() => {
const currentAddress = route.params?.address;
if (currentAddress !== address) {
navigation.setParams({ address });
}
}, [address, navigation, route.params?.address]);
const handleUpdateAddress = (nextValue: string) => setAddress(nextValue);
const clearAddressInput = () => {
setAddress('');
setResultCleanAddress(undefined);
setMatchingWallets(undefined);
};
const checkAddress = () => {
Keyboard.dismiss();
const cleanAddress = address.replace('bitcoin:', '').replace('BITCOIN:', '').replace('bitcoin=', '').split('?')[0];
const matching: TWallet[] = [];
for (const w of wallets) {
if (w.weOwnAddress(cleanAddress)) {
matching.push(w);
}
}
if (matching.length > 0) {
setMatchingWallets(matching);
setResultCleanAddress(cleanAddress);
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
} else {
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
presentAlert({
message: loc.is_it_my_address.no_wallet_owns_address,
buttons: [
{
text: loc.receive.reset,
onPress: () => {
clearAddressInput();
},
style: 'destructive',
},
{
text: loc._.ok,
onPress: () => {},
style: 'cancel',
},
],
options: { cancelable: true },
});
setMatchingWallets([]);
setResultCleanAddress(undefined);
}
};
const onBarScanned = (value: string) => {
const cleanAddress = value.replace(/^bitcoin(:|=)/i, '').split('?')[0];
setAddress(value);
setResultCleanAddress(cleanAddress);
};
const importScan = async () => {
const data = await scanQrHelper(route.name, true, undefined, true);
if (data) {
onBarScanned(data);
}
};
const viewQRCode = () => {
if (!resultCleanAddress) return;
navigation.navigate('ReceiveDetailsRoot', {
screen: 'ReceiveDetails',
params: {
address: resultCleanAddress,
},
});
};
const isCheckAddressDisabled = address.trim().length === 0;
useEffect(() => {
if (matchingWallets && matchingWallets.length > 0 && scrollViewRef.current && firstWalletRef.current) {
firstWalletRef.current.measureLayout(scrollViewRef.current.getInnerViewNode(), (x, y) => {
scrollViewRef.current?.scrollTo({ x: 0, y: y - 20, animated: true });
});
}
}, [matchingWallets]);
const renderFormattedText = (text: string, values: { [key: string]: string }) => {
const regex = /\{(\w+)\}/g;
const parts = [];
let lastIndex = 0;
let match;
let index = 0;
while ((match = regex.exec(text)) !== null) {
if (match.index > lastIndex) {
parts.push(<Text key={`text-${index++}`}>{text.substring(lastIndex, match.index)}</Text>);
}
const value = values[match[1]];
if (value) {
parts.push(
<Text key={`bold-${index++}`} style={styles.boldText} selectable>
{value}
</Text>,
);
}
lastIndex = regex.lastIndex;
}
if (lastIndex < text.length) {
parts.push(<Text key={`text-${index++}`}>{text.substring(lastIndex)}</Text>);
}
return parts;
};
return (
<ScrollView
ref={scrollViewRef}
contentContainerStyle={styles.wrapper}
automaticallyAdjustContentInsets
automaticallyAdjustKeyboardInsets
contentInsetAdjustmentBehavior="automatic"
>
<BlueCard style={styles.mainCard}>
<View style={[styles.input, stylesHooks.input]}>
<TextInput
style={styles.textInput}
multiline
editable
placeholder={loc.is_it_my_address.enter_address}
placeholderTextColor={colors.placeholderTextColor}
value={address}
onChangeText={handleUpdateAddress}
testID="AddressInput"
/>
{address.length > 0 && (
<TouchableOpacity onPress={clearAddressInput} style={styles.clearButton}>
<Icon name="close" size={20} color="#81868e" />
</TouchableOpacity>
)}
</View>
<BlueSpacing10 />
<BlueButtonLink ref={scanButtonRef} title={loc.wallets.import_scan_qr} onPress={importScan} />
<BlueSpacing20 />
{resultCleanAddress && (
<>
<Button title={loc.is_it_my_address.view_qrcode} onPress={viewQRCode} />
<BlueSpacing20 />
</>
)}
<Button disabled={isCheckAddressDisabled} title={loc.is_it_my_address.check_address} onPress={checkAddress} testID="CheckAddress" />
<BlueSpacing40 />
{matchingWallets !== undefined && matchingWallets.length > 0 && (
<>
<Divider />
<BlueSpacing40 />
</>
)}
{matchingWallets !== undefined &&
matchingWallets.length > 0 &&
matchingWallets.map((wallet, index) => (
<View key={wallet.getID()} ref={index === 0 ? firstWalletRef : undefined} style={styles.walletContainer}>
<BlueText selectable style={styles.resultText}>
{resultCleanAddress &&
renderFormattedText(loc.is_it_my_address.owns, {
label: wallet.getLabel(),
address: resultCleanAddress,
})}
</BlueText>
<BlueSpacing10 />
<WalletCarouselItem
item={wallet}
onPress={item => {
navigation.navigate('WalletTransactions', {
walletID: item.getID(),
walletType: item.type,
});
}}
/>
<BlueSpacing20 />
</View>
))}
</BlueCard>
</ScrollView>
);
};
export default IsItMyAddress;
const styles = StyleSheet.create({
wrapper: {
alignItems: 'center',
},
mainCard: {
padding: 0,
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'flex-start',
width: '100%',
},
input: {
flexDirection: 'row',
borderWidth: 1,
borderBottomWidth: 0.5,
alignItems: 'center',
borderRadius: 4,
width: '100%',
},
textInput: {
flex: 1,
padding: 8,
minHeight: 100,
color: '#81868e',
},
clearButton: {
padding: 8,
justifyContent: 'center',
alignItems: 'center',
},
boldText: {
fontWeight: 'bold',
},
resultText: {
marginVertical: 10,
textAlign: 'center',
},
walletContainer: {
width: '100%',
alignItems: 'center',
},
});

View File

@ -153,7 +153,8 @@ describe('BlueWallet UI Tests - no wallets', () => {
await element(by.id('IsItMyAddress')).tap();
await element(by.id('AddressInput')).replaceText('bc1q063ctu6jhe5k4v8ka99qac8rcm2tzjjnuktyrl');
await element(by.id('CheckAddress')).tap();
await expect(element(by.id('Result'))).toHaveText('None of the available wallets own the provided address.');
await expect(element(by.text('None of the available wallets own the provided address.'))).toBeVisible();
await element(by.text('OK')).tap();
await device.pressBack();
await device.pressBack();