Merge branch 'master' into rtl

This commit is contained in:
marcosrdz 2021-03-23 18:29:43 -04:00
commit 883ac0d95b
7 changed files with 296 additions and 4 deletions

View File

@ -34,6 +34,7 @@ import WalletExport from './screen/wallets/export';
import ExportMultisigCoordinationSetup from './screen/wallets/exportMultisigCoordinationSetup'; import ExportMultisigCoordinationSetup from './screen/wallets/exportMultisigCoordinationSetup';
import ViewEditMultisigCosigners from './screen/wallets/viewEditMultisigCosigners'; import ViewEditMultisigCosigners from './screen/wallets/viewEditMultisigCosigners';
import WalletXpub from './screen/wallets/xpub'; import WalletXpub from './screen/wallets/xpub';
import SignVerify from './screen/wallets/signVerify';
import BuyBitcoin from './screen/wallets/buyBitcoin'; import BuyBitcoin from './screen/wallets/buyBitcoin';
import HodlHodl from './screen/wallets/hodlHodl'; import HodlHodl from './screen/wallets/hodlHodl';
import HodlHodlViewOffer from './screen/wallets/hodlHodlViewOffer'; import HodlHodlViewOffer from './screen/wallets/hodlHodlViewOffer';
@ -386,6 +387,17 @@ const WalletXpubStackRoot = () => {
); );
}; };
const SignVerifyStack = createStackNavigator();
const SignVerifyStackRoot = () => {
const theme = useTheme();
return (
<SignVerifyStack.Navigator name="SignVerifyRoot" screenOptions={defaultStackScreenOptions} initialRouteName="SignVerify">
<SignVerifyStack.Screen name="SignVerify" component={SignVerify} options={SignVerify.navigationOptions(theme)} />
</SignVerifyStack.Navigator>
);
};
const WalletExportStack = createStackNavigator(); const WalletExportStack = createStackNavigator();
const WalletExportStackRoot = () => { const WalletExportStackRoot = () => {
const theme = useTheme(); const theme = useTheme();
@ -486,6 +498,7 @@ const Navigation = () => {
/> />
<RootStack.Screen name="ViewEditMultisigCosignersRoot" component={ViewEditMultisigCosignersRoot} options={{ headerShown: false }} /> <RootStack.Screen name="ViewEditMultisigCosignersRoot" component={ViewEditMultisigCosignersRoot} options={{ headerShown: false }} />
<RootStack.Screen name="WalletXpubRoot" component={WalletXpubStackRoot} options={{ headerShown: false }} /> <RootStack.Screen name="WalletXpubRoot" component={WalletXpubStackRoot} options={{ headerShown: false }} />
<RootStack.Screen name="SignVerifyRoot" component={SignVerifyStackRoot} options={{ headerShown: false }} />
<RootStack.Screen name="BuyBitcoin" component={BuyBitcoin} options={BuyBitcoin.navigationOptions(theme)} /> <RootStack.Screen name="BuyBitcoin" component={BuyBitcoin} options={BuyBitcoin.navigationOptions(theme)} />
<RootStack.Screen name="Marketplace" component={Marketplace} options={Marketplace.navigationOptions(theme)} /> <RootStack.Screen name="Marketplace" component={Marketplace} options={Marketplace.navigationOptions(theme)} />
<RootStack.Screen name="SelectWallet" component={SelectWallet} options={{ headerLeft: null }} /> <RootStack.Screen name="SelectWallet" component={SelectWallet} options={{ headerLeft: null }} />

View File

@ -605,6 +605,10 @@ export class LightningCustodianWallet extends LegacyWallet {
return true; return true;
} }
allowSignVerifyMessage() {
return false;
}
/** /**
* Example return: * Example return:
* { destination: '03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f', * { destination: '03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f',

View File

@ -35,6 +35,10 @@ export class WatchOnlyWallet extends LegacyWallet {
); );
} }
allowSignVerifyMessage() {
return false;
}
getAddress() { getAddress() {
if (this.isAddressValid(this.secret)) return this.secret; // handling case when there is an XPUB there if (this.isAddressValid(this.secret)) return this.secret; // handling case when there is an XPUB there
if (this._hdWalletInstance) throw new Error('Should not be used in watch-only HD wallets'); if (this._hdWalletInstance) throw new Error('Should not be used in watch-only HD wallets');

View File

@ -9,16 +9,20 @@ const ICON_MARGIN = 7;
const cStyles = StyleSheet.create({ const cStyles = StyleSheet.create({
root: { root: {
position: 'absolute',
alignSelf: 'center', alignSelf: 'center',
height: '6.3%', height: '6.3%',
minHeight: 44, minHeight: 44,
}, },
rootAbsolute: {
position: 'absolute',
bottom: 30,
},
rootInline: {},
rootPre: { rootPre: {
position: 'absolute',
bottom: -1000, bottom: -1000,
}, },
rootPost: { rootPost: {
bottom: 30,
borderRadius: BORDER_RADIUS, borderRadius: BORDER_RADIUS,
flexDirection: 'row', flexDirection: 'row',
overflow: 'hidden', overflow: 'hidden',
@ -42,7 +46,11 @@ export const FContainer = forwardRef((props, ref) => {
}; };
return ( return (
<View ref={ref} onLayout={onLayout} style={[cStyles.root, newWidth ? cStyles.rootPost : cStyles.rootPre]}> <View
ref={ref}
onLayout={onLayout}
style={[cStyles.root, props.inline ? cStyles.rootInline : cStyles.rootAbsolute, newWidth ? cStyles.rootPost : cStyles.rootPre]}
>
{newWidth {newWidth
? React.Children.toArray(props.children) ? React.Children.toArray(props.children)
.filter(Boolean) .filter(Boolean)
@ -61,6 +69,7 @@ export const FContainer = forwardRef((props, ref) => {
FContainer.propTypes = { FContainer.propTypes = {
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.element), PropTypes.element]), children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.element), PropTypes.element]),
inline: PropTypes.bool,
}; };
const buttonFontSize = const buttonFontSize =
@ -95,6 +104,9 @@ export const FButton = ({ text, icon, width, first, last, ...props }) => {
text: { text: {
color: colors.buttonAlternativeTextColor, color: colors.buttonAlternativeTextColor,
}, },
textDisabled: {
color: colors.formBorder,
},
}); });
const style = {}; const style = {};
@ -109,7 +121,7 @@ export const FButton = ({ text, icon, width, first, last, ...props }) => {
return ( return (
<TouchableOpacity style={[bStyles.root, bStylesHook.root, style]} {...props}> <TouchableOpacity style={[bStyles.root, bStylesHook.root, style]} {...props}>
<View style={bStyles.icon}>{icon}</View> <View style={bStyles.icon}>{icon}</View>
<Text numberOfLines={1} style={[bStyles.text, bStylesHook.text]}> <Text numberOfLines={1} style={[bStyles.text, props.disabled ? bStylesHook.textDisabled : bStylesHook.text]}>
{text} {text}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
@ -122,4 +134,5 @@ FButton.propTypes = {
width: PropTypes.number, width: PropTypes.number,
first: PropTypes.bool, first: PropTypes.bool,
last: PropTypes.bool, last: PropTypes.bool,
disabled: PropTypes.bool,
}; };

View File

@ -14,6 +14,7 @@
"no": "No", "no": "No",
"save": "Save", "save": "Save",
"seed": "Seed", "seed": "Seed",
"success": "Success",
"wallet_key": "Wallet key", "wallet_key": "Wallet key",
"invalid_animated_qr_code_fragment" : "Invalid animated QRCode fragment. Please try again.", "invalid_animated_qr_code_fragment" : "Invalid animated QRCode fragment. Please try again.",
"file_saved": "File ({filePath}) has been saved in your Downloads folder.", "file_saved": "File ({filePath}) has been saved in your Downloads folder.",
@ -560,5 +561,16 @@
"MAX": "Max", "MAX": "Max",
"sat_byte": "sat/byte", "sat_byte": "sat/byte",
"sats": "sats" "sats": "sats"
},
"addresses": {
"sign_title": "Sign/Verify message",
"sign_help": "Here you can create or verify a cryptographic signature based on a Bitcoin address",
"sign_sign": "Sign",
"sign_verify": "Verify",
"sign_signature_correct": "Verification Succeeded!",
"sign_signature_incorrect": "Verification Failed!",
"sign_placeholder_address": "Address",
"sign_placeholder_message": "Message",
"sign_placeholder_signature": "Signature"
} }
} }

View File

@ -231,6 +231,14 @@ const WalletDetails = () => {
secret: wallet.getSecret(), secret: wallet.getSecret(),
}, },
}); });
const navigateToSignVerify = () =>
navigate('SignVerifyRoot', {
screen: 'SignVerify',
params: {
walletID: wallet.getID(),
address: wallet.getAllExternalAddresses()[0], // works for both single address and HD wallets
},
});
const renderMarketplaceButton = () => { const renderMarketplaceButton = () => {
return Platform.select({ return Platform.select({
@ -552,6 +560,12 @@ const WalletDetails = () => {
<BlueSpacing20 /> <BlueSpacing20 />
<SecondButton onPress={navigateToIsItMyAddress} testID="IsItMyAddress" title={loc.is_it_my_address.title} /> <SecondButton onPress={navigateToIsItMyAddress} testID="IsItMyAddress" title={loc.is_it_my_address.title} />
</> </>
{wallet.allowSignVerifyMessage() && (
<>
<BlueSpacing20 />
<SecondButton onPress={navigateToSignVerify} testID="SignVerify" title={loc.addresses.sign_title} />
</>
)}
<BlueSpacing20 /> <BlueSpacing20 />
<BlueSpacing20 /> <BlueSpacing20 />
<TouchableOpacity onPress={handleDeleteButtonTapped} testID="DeleteButton"> <TouchableOpacity onPress={handleDeleteButtonTapped} testID="DeleteButton">

View File

@ -0,0 +1,232 @@
import React, { useEffect, useState, useContext } from 'react';
import {
ActivityIndicator,
Alert,
Keyboard,
KeyboardAvoidingView,
Platform,
StyleSheet,
TextInput,
TouchableWithoutFeedback,
View,
} from 'react-native';
import { useRoute, useTheme } from '@react-navigation/native';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import { BlueDoneAndDismissKeyboardInputAccessory, BlueFormLabel, BlueSpacing10, BlueSpacing20, SafeBlueArea } from '../../BlueComponents';
import navigationStyle from '../../components/navigationStyle';
import { FContainer, FButton } from '../../components/FloatButtons';
import { BlueStorageContext } from '../../blue_modules/storage-context';
import loc from '../../loc';
const SignVerify = () => {
const { colors } = useTheme();
const { wallets, sleep } = useContext(BlueStorageContext);
const { params } = useRoute();
const [isKeyboardVisible, setIsKeyboardVisible] = useState(false);
const [address, setAddress] = useState(params.address);
const [message, setMessage] = useState('');
const [signature, setSignature] = useState('');
const [loading, setLoading] = useState(false);
const [messageHasFocus, setMessageHasFocus] = useState(false);
const wallet = wallets.find(w => w.getID() === params.walletID);
const isToolbarVisibleForAndroid = Platform.OS === 'android' && messageHasFocus && isKeyboardVisible;
useEffect(() => {
Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow', () => setIsKeyboardVisible(true));
Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide', () => setIsKeyboardVisible(false));
return () => {
Keyboard.removeListener(Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide');
Keyboard.removeListener(Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow');
};
}, []);
const stylesHooks = StyleSheet.create({
root: {
backgroundColor: colors.elevated,
},
text: {
borderColor: colors.formBorder,
borderBottomColor: colors.formBorder,
backgroundColor: colors.inputBackgroundColor,
color: colors.foregroundColor,
},
});
const handleSign = async () => {
setLoading(true);
await sleep(10); // wait for loading indicator to appear
try {
const newSignature = wallet.signMessage(message, address);
setSignature(newSignature);
} catch (e) {
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
Alert.alert(loc.errors.error, e.message);
}
setLoading(false);
};
const handleVerify = async () => {
setLoading(true);
await sleep(10); // wait for loading indicator to appear
try {
const res = wallet.verifyMessage(message, address, signature);
Alert.alert(
res ? loc._.success : loc.errors.error,
res ? loc.addresses.sign_signature_correct : loc.addresses.sign_signature_incorrect,
);
if (res) {
ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false });
}
} catch (e) {
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
Alert.alert(loc.errors.error, e.message);
}
setLoading(false);
};
if (loading)
return (
<View style={[styles.loading]}>
<ActivityIndicator />
</View>
);
return (
<SafeBlueArea>
<TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
<KeyboardAvoidingView style={[styles.root, stylesHooks.root]}>
{!isKeyboardVisible && (
<>
<BlueSpacing20 />
<BlueFormLabel>{loc.addresses.sign_help}</BlueFormLabel>
<BlueSpacing20 />
</>
)}
<TextInput
multiline
textAlignVertical="top"
blurOnSubmit
placeholder={loc.addresses.sign_placeholder_address}
placeholderTextColor="#81868e"
value={address}
onChangeText={t => setAddress(t.replace('\n', ''))}
testID="Signature"
style={[styles.text, stylesHooks.text]}
autoCorrect={false}
autoCapitalize="none"
spellCheck={false}
editable={!loading}
/>
<BlueSpacing10 />
<TextInput
multiline
textAlignVertical="top"
blurOnSubmit
placeholder={loc.addresses.sign_placeholder_signature}
placeholderTextColor="#81868e"
value={signature}
onChangeText={t => setSignature(t.replace('\n', ''))}
testID="Signature"
style={[styles.text, stylesHooks.text]}
autoCorrect={false}
autoCapitalize="none"
spellCheck={false}
editable={!loading}
/>
<BlueSpacing10 />
<TextInput
multiline
placeholder={loc.addresses.sign_placeholder_message}
placeholderTextColor="#81868e"
value={message}
onChangeText={setMessage}
testID="Message"
inputAccessoryViewID={BlueDoneAndDismissKeyboardInputAccessory.InputAccessoryViewID}
style={[styles.flex, styles.text, styles.textMessage, stylesHooks.text]}
autoCorrect={false}
autoCapitalize="none"
spellCheck={false}
editable={!loading}
onFocus={() => setMessageHasFocus(true)}
onBlur={() => setMessageHasFocus(false)}
/>
<BlueSpacing10 />
{!isKeyboardVisible && (
<>
<FContainer inline>
<FButton onPress={handleSign} text={loc.addresses.sign_sign} disabled={loading} />
<FButton onPress={handleVerify} text={loc.addresses.sign_verify} disabled={loading} />
</FContainer>
<BlueSpacing10 />
</>
)}
{Platform.select({
ios: (
<BlueDoneAndDismissKeyboardInputAccessory
onClearTapped={() => setMessage('')}
onPasteTapped={text => {
setMessage(text);
Keyboard.dismiss();
}}
/>
),
android: isToolbarVisibleForAndroid && (
<BlueDoneAndDismissKeyboardInputAccessory
onClearTapped={() => {
setMessage('');
Keyboard.dismiss();
}}
onPasteTapped={text => {
setMessage(text);
Keyboard.dismiss();
}}
/>
),
})}
</KeyboardAvoidingView>
</TouchableWithoutFeedback>
</SafeBlueArea>
);
};
SignVerify.navigationOptions = navigationStyle({}, opts => ({ ...opts, title: loc.addresses.sign_title }));
export default SignVerify;
const styles = StyleSheet.create({
root: {
flex: 1,
},
text: {
paddingHorizontal: 8,
paddingVertical: 8,
marginTop: 5,
marginHorizontal: 20,
borderWidth: 1,
borderBottomWidth: 0.5,
borderRadius: 4,
textAlignVertical: 'top',
},
textMessage: {
minHeight: 50,
},
flex: {
flex: 1,
},
loading: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
justifyContent: 'center',
alignItems: 'center',
},
});