diff --git a/Navigation.js b/Navigation.js index 36feb30e1..fb9c2c24b 100644 --- a/Navigation.js +++ b/Navigation.js @@ -34,6 +34,7 @@ import WalletExport from './screen/wallets/export'; import ExportMultisigCoordinationSetup from './screen/wallets/exportMultisigCoordinationSetup'; import ViewEditMultisigCosigners from './screen/wallets/viewEditMultisigCosigners'; import WalletXpub from './screen/wallets/xpub'; +import SignVerify from './screen/wallets/signVerify'; import BuyBitcoin from './screen/wallets/buyBitcoin'; import HodlHodl from './screen/wallets/hodlHodl'; import HodlHodlViewOffer from './screen/wallets/hodlHodlViewOffer'; @@ -385,6 +386,17 @@ const WalletXpubStackRoot = () => { ); }; +const SignVerifyStack = createStackNavigator(); +const SignVerifyStackRoot = () => { + const theme = useTheme(); + + return ( + + + + ); +}; + const WalletExportStack = createStackNavigator(); const WalletExportStackRoot = () => { const theme = useTheme(); @@ -485,6 +497,7 @@ const Navigation = () => { /> + diff --git a/class/wallets/lightning-custodian-wallet.js b/class/wallets/lightning-custodian-wallet.js index 6a7c026fe..1efffad3a 100644 --- a/class/wallets/lightning-custodian-wallet.js +++ b/class/wallets/lightning-custodian-wallet.js @@ -605,6 +605,10 @@ export class LightningCustodianWallet extends LegacyWallet { return true; } + allowSignVerifyMessage() { + return false; + } + /** * Example return: * { destination: '03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f', diff --git a/class/wallets/watch-only-wallet.js b/class/wallets/watch-only-wallet.js index 9cff25b13..1cff6710f 100644 --- a/class/wallets/watch-only-wallet.js +++ b/class/wallets/watch-only-wallet.js @@ -35,6 +35,10 @@ export class WatchOnlyWallet extends LegacyWallet { ); } + allowSignVerifyMessage() { + return false; + } + getAddress() { 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'); diff --git a/components/FloatButtons.js b/components/FloatButtons.js index 08810b1d8..8aaf0d03a 100644 --- a/components/FloatButtons.js +++ b/components/FloatButtons.js @@ -9,16 +9,20 @@ const ICON_MARGIN = 7; const cStyles = StyleSheet.create({ root: { - position: 'absolute', alignSelf: 'center', height: '6.3%', minHeight: 44, }, + rootAbsolute: { + position: 'absolute', + bottom: 30, + }, + rootInline: {}, rootPre: { + position: 'absolute', bottom: -1000, }, rootPost: { - bottom: 30, borderRadius: BORDER_RADIUS, flexDirection: 'row', overflow: 'hidden', @@ -42,7 +46,11 @@ export const FContainer = forwardRef((props, ref) => { }; return ( - + {newWidth ? React.Children.toArray(props.children) .filter(Boolean) @@ -61,6 +69,7 @@ export const FContainer = forwardRef((props, ref) => { FContainer.propTypes = { children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.element), PropTypes.element]), + inline: PropTypes.bool, }; const buttonFontSize = @@ -95,6 +104,9 @@ export const FButton = ({ text, icon, width, first, last, ...props }) => { text: { color: colors.buttonAlternativeTextColor, }, + textDisabled: { + color: colors.formBorder, + }, }); const style = {}; @@ -109,7 +121,7 @@ export const FButton = ({ text, icon, width, first, last, ...props }) => { return ( {icon} - + {text} @@ -122,4 +134,5 @@ FButton.propTypes = { width: PropTypes.number, first: PropTypes.bool, last: PropTypes.bool, + disabled: PropTypes.bool, }; diff --git a/loc/en.json b/loc/en.json index d1ced1732..525d77f1d 100644 --- a/loc/en.json +++ b/loc/en.json @@ -14,6 +14,7 @@ "no": "No", "save": "Save", "seed": "Seed", + "success": "Success", "wallet_key": "Wallet key", "invalid_animated_qr_code_fragment" : "Invalid animated QRCode fragment. Please try again.", "file_saved": "File ({filePath}) has been saved in your Downloads folder.", @@ -561,5 +562,16 @@ "MAX": "Max", "sat_byte": "sat/byte", "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" } } diff --git a/screen/wallets/details.js b/screen/wallets/details.js index 6b8373ba6..a9d06914e 100644 --- a/screen/wallets/details.js +++ b/screen/wallets/details.js @@ -231,6 +231,14 @@ const WalletDetails = () => { 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 = () => { return Platform.select({ @@ -552,6 +560,12 @@ const WalletDetails = () => { + {wallet.allowSignVerifyMessage() && ( + <> + + + + )} diff --git a/screen/wallets/signVerify.js b/screen/wallets/signVerify.js new file mode 100644 index 000000000..e86b8f885 --- /dev/null +++ b/screen/wallets/signVerify.js @@ -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 ( + + + + ); + + return ( + + + + {!isKeyboardVisible && ( + <> + + {loc.addresses.sign_help} + + + )} + + setAddress(t.replace('\n', ''))} + testID="Signature" + style={[styles.text, stylesHooks.text]} + autoCorrect={false} + autoCapitalize="none" + spellCheck={false} + editable={!loading} + /> + + + setSignature(t.replace('\n', ''))} + testID="Signature" + style={[styles.text, stylesHooks.text]} + autoCorrect={false} + autoCapitalize="none" + spellCheck={false} + editable={!loading} + /> + + + setMessageHasFocus(true)} + onBlur={() => setMessageHasFocus(false)} + /> + + + {!isKeyboardVisible && ( + <> + + + + + + + )} + + {Platform.select({ + ios: ( + setMessage('')} + onPasteTapped={text => { + setMessage(text); + Keyboard.dismiss(); + }} + /> + ), + android: isToolbarVisibleForAndroid && ( + { + setMessage(''); + Keyboard.dismiss(); + }} + onPasteTapped={text => { + setMessage(text); + Keyboard.dismiss(); + }} + /> + ), + })} + + + + ); +}; + +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', + }, +});