diff --git a/components/TextInput.tsx b/components/TextInput.tsx index ef9a2acb6..c3c3d43a1 100644 --- a/components/TextInput.tsx +++ b/components/TextInput.tsx @@ -36,6 +36,7 @@ interface TextInputProps { ref?: React.Ref; error?: boolean; onFocus?: any; + onBlur?: any; } const TextInput = React.forwardRef( @@ -61,7 +62,9 @@ const TextInput = React.forwardRef( toggleUnits, onPressIn, right, - error + error, + onFocus, + onBlur } = props; const defaultStyle = numberOfLines ? { @@ -168,6 +171,8 @@ const TextInput = React.forwardRef( secureTextEntry={secureTextEntry} onPressIn={onPressIn} ref={ref} + onFocus={onFocus} + onBlur={onBlur} /> {suffix ? ( toggleUnits ? ( diff --git a/locales/en.json b/locales/en.json index 2aaf531ff..1b5877714 100644 --- a/locales/en.json +++ b/locales/en.json @@ -126,6 +126,8 @@ "general.count": "Count", "general.experimental": "Experimental", "general.defaultNodeNickname": "My Lightning Node", + "general.pastedInvalidData": "The pasted text contains characters we can't use here. Please check and try again.", + "general.invalidHost": "Invalid host. Please enter a valid host, or host:port.", "restart.title": "Restart required", "restart.msg": "ZEUS has to be restarted before the new configuration is applied.", "restart.msg1": "Would you like to restart now?", @@ -251,6 +253,7 @@ "views.Settings.WalletConfiguration.walletActive": "Wallet Active", "views.Settings.AddEditNode.scanLndconnect": "Scan lndconnect config", "views.Settings.AddEditNode.scanLnc": "Scan LNC QR from Lightning Terminal", + "views.Settings.AddEditNode.wrongLncPairingPhraseLength": "A Lightning Node Connect pairing phrase must contain exactly 10 words. Please check and try again.", "views.Settings.AddEditNode.scanCLightningRest": "Scan c-lightning-REST QR", "views.Settings.AddEditNode.scanBtcpay": "Scan BTCPay config", "views.Settings.AddEditNode.scanLndhub": "Scan LNDHub QR", @@ -1095,6 +1098,7 @@ "error.failureReasonIncorrectPaymentDetails": "Payment failed: Payment details incorrect (unknown payment hash, invalid amount or invalid final CLTV delta).", "error.failureReasonIncorrectPaymentDetailsKeysend": "The receiving node might not accept keysend payments.", "error.failureReasonInsufficientBalance": "Insufficient local balance", + "error.invalidMacaroon": "Invalid macaroon. Please check that you've entered the correct macaroon for this node.", "pos.views.Wallet.PosPane.noOrders": "No orders open at the moment. To send to ZEUS, mark order as 'Other Payment Type' with a note that includes 'Zeus', 'BTC', or 'Bitcoin'", "pos.views.Wallet.PosPane.noOrdersStandalone": "No orders open at the moment", "pos.views.Wallet.PosPane.noOrdersPaid": "No orders have been paid yet", diff --git a/utils/ErrorUtils.test.ts b/utils/ErrorUtils.test.ts index f669454a7..8d8e2d165 100644 --- a/utils/ErrorUtils.test.ts +++ b/utils/ErrorUtils.test.ts @@ -96,6 +96,18 @@ describe('ErrorUtils', () => { ).toEqual( 'Error starting up Tor on your phone. Try restarting Zeus. If the problem persists consider using the Orbot app to connect to Tor, or using an alternative connection method like Lightning Node Connect or Tailscale.' ); + expect( + errorToUserFriendly( + Object.assign(new Error(), { + message: + 'Error: {"code":2,"message":"verification failed: signature mismatch after caveat verification","details":[]}', + name: 'test' + }), + false + ) + ).toEqual( + "Invalid macaroon. Please check that you've entered the correct macaroon for this node." + ); }); it('Returns normal error message for unhandled errorContext', () => { diff --git a/utils/ErrorUtils.ts b/utils/ErrorUtils.ts index a9982216f..a79341611 100644 --- a/utils/ErrorUtils.ts +++ b/utils/ErrorUtils.ts @@ -5,6 +5,8 @@ const userFriendlyErrors: any = { 'error.torBootstrap', 'Error: called `Result::unwrap()` on an `Err` value: BootStrapError("Timeout waiting for boostrap")': 'error.torBootstrap', + 'Error: {"code":2,"message":"verification failed: signature mismatch after caveat verification","details":[]}': + 'error.invalidMacaroon', FAILURE_REASON_TIMEOUT: 'error.failureReasonTimeout', FAILURE_REASON_NO_ROUTE: 'error.failureReasonNoRoute', FAILURE_REASON_ERROR: 'error.failureReasonError', diff --git a/views/Settings/WalletConfiguration.tsx b/views/Settings/WalletConfiguration.tsx index c01543d52..a4d334c77 100644 --- a/views/Settings/WalletConfiguration.tsx +++ b/views/Settings/WalletConfiguration.tsx @@ -17,6 +17,7 @@ import { Route } from '@react-navigation/native'; import { StackNavigationProp } from '@react-navigation/stack'; import { hash, STORAGE_KEY } from '../../backends/LNC/credentialStore'; +import ModalStore from '../../stores/ModalStore'; import AddressUtils, { CUSTODIAL_LNDHUBS } from '../../utils/AddressUtils'; import ConnectionFormatUtils from '../../utils/ConnectionFormatUtils'; @@ -62,6 +63,7 @@ import { interface WalletConfigurationProps { navigation: StackNavigationProp; SettingsStore: SettingsStore; + ModalStore: ModalStore; route: Route< 'WalletConfiguration', { @@ -126,7 +128,7 @@ const ScanBadge = ({ onPress }: { onPress: () => void }) => ( ); -@inject('SettingsStore') +@inject('SettingsStore', 'ModalStore') @observer export default class WalletConfiguration extends React.Component< WalletConfigurationProps, @@ -323,6 +325,12 @@ export default class WalletConfiguration extends React.Component< this.initFromProps(nextProps); } + componentWillUnmount() { + const { SettingsStore } = this.props; + SettingsStore.createAccountError = ''; + SettingsStore.createAccountSuccess = ''; + } + async initFromProps(props: WalletConfigurationProps) { const { route } = props; @@ -1229,6 +1237,7 @@ export default class WalletConfiguration extends React.Component< } locked={loading} autoCorrect={false} + autoCapitalize="none" /> {implementation === 'spark' && ( @@ -1254,6 +1263,8 @@ export default class WalletConfiguration extends React.Component< }); }} locked={loading} + autoCorrect={false} + autoCapitalize="none" /> )} @@ -1280,6 +1291,9 @@ export default class WalletConfiguration extends React.Component< }); }} locked={loading} + secureTextEntry={saved} + autoCorrect={false} + autoCapitalize="none" /> )} @@ -1299,15 +1313,57 @@ export default class WalletConfiguration extends React.Component< + onChangeText={(text: string) => { + const validHostChars = + /^[a-zA-Z0-9-.:]+$/; + + // Allow backspace/delete operations without validation + if ( + text.length < + (lndhubUrl?.length || 0) + ) { + this.setState({ + lndhubUrl: text, + saved: false + }); + return; + } + + // For single character additions + if ( + text.length === + (lndhubUrl?.length || 0) + 1 + ) { + const cleanedText = text.replace( + /[^a-zA-Z0-9-.:]/g, + '' + ); + this.setState({ + lndhubUrl: cleanedText, + saved: false + }); + return; + } + + // For pasted content + const trimmedText = text.trim(); + if (!validHostChars.test(trimmedText)) { + this.props.ModalStore.toggleInfoModal( + localeString( + 'general.pastedInvalidData' + ) + ); + return; + } + this.setState({ - lndhubUrl: text.trim(), + lndhubUrl: trimmedText, saved: false - }) - } + }); + }} locked={loading} - autoCorrect={false} /> <> @@ -1348,13 +1404,51 @@ export default class WalletConfiguration extends React.Component< + onChangeText={(text: string) => { + // Allow backspace/delete operations without validation + if ( + text.length < + (username?.length || 0) + ) { + this.setState({ + username: text, + saved: false + }); + return; + } + + // For single character additions + if ( + text.length === + (username?.length || 0) + 1 + ) { + const cleanedText = + text.trim(); + this.setState({ + username: cleanedText, + saved: false + }); + return; + } + + // For pasted content + const trimmedText = text.trim(); + if (/\s/.test(trimmedText)) { + this.props.ModalStore.toggleInfoModal( + localeString( + 'general.pastedInvalidData' + ) + ); + return; + } + this.setState({ - username: text.trim(), + username: trimmedText, saved: false - }) - } + }); + }} locked={loading} + autoCorrect={false} autoCapitalize="none" /> @@ -1374,12 +1468,13 @@ export default class WalletConfiguration extends React.Component< value={password} onChangeText={(text: string) => this.setState({ - password: text.trim(), + password: text, saved: false }) } locked={loading} secureTextEntry={saved} + autoCorrect={false} autoCapitalize="none" /> @@ -1402,7 +1497,9 @@ export default class WalletConfiguration extends React.Component< )} )} - {implementation === 'cln-rest' && ( + {(implementation === 'lnd' || + implementation === 'cln-rest' || + implementation === 'c-lightning-REST') && ( <> - this.setState({ - host: text.trim(), - saved: false - }) - } - locked={loading} - /> + onChangeText={(text: string) => { + // Allow backspace/delete operations without validation + if (text.length < (host?.length || 0)) { + this.setState({ + host: text, + saved: false + }); + return; + } - - {localeString( - 'views.Settings.AddEditNode.rune' - )} - - - this.setState({ - rune: text.trim(), - saved: false - }) - } - locked={loading} - /> + // For single character additions + if ( + text.length === + (host?.length || 0) + 1 + ) { + const cleanedText = text.replace( + /[^a-zA-Z0-9-.]/g, + '' + ); - - {localeString( - 'views.Settings.AddEditNode.restPort' - )} - - - this.setState({ - port: text.trim(), - saved: false - }) - } - locked={loading} - /> - - )} + this.setState({ + host: cleanedText, + saved: false + }); + return; + } - {(implementation === 'lnd' || - implementation === 'c-lightning-REST') && ( - <> - - {localeString( - 'views.Settings.AddEditNode.host' - )} - - - this.setState({ - host: text.trim(), - saved: false - }) - } - locked={loading} - /> + // For pasted content + const trimmedText = text.trim(); + if ( + !/^[a-zA-Z0-9-.]+$/.test( + trimmedText + ) + ) { + this.props.ModalStore.toggleInfoModal( + localeString( + 'general.pastedInvalidData' + ) + ); + return; + } - - {localeString( - 'views.Settings.AddEditNode.macaroon' - )} - - this.setState({ - macaroonHex: text.replace( - /\s+/g, - '' - ), + host: trimmedText, saved: false - }) - } + }); + }} locked={loading} /> + {implementation === 'cln-rest' ? ( + <> + + {localeString( + 'views.Settings.AddEditNode.rune' + )} + + { + // Allow backspace/delete operations without validation + if ( + text.length < + (rune?.length || 0) + ) { + this.setState({ + rune: text, + saved: false + }); + return; + } + + // For single character additions + if ( + text.length === + (rune?.length || 0) + 1 + ) { + const cleanedText = + text.replace( + /[^A-Za-z0-9\-_=]/g, + '' + ); + this.setState({ + rune: cleanedText, + saved: false + }); + return; + } + + // For pasted content + const trimmedText = text.trim(); + if ( + !/^[A-Za-z0-9\-_=]+$/.test( + trimmedText + ) + ) { + this.props.ModalStore.toggleInfoModal( + localeString( + 'general.pastedInvalidData' + ) + ); + return; + } + + this.setState({ + rune: trimmedText, + saved: false + }); + }} + locked={loading} + /> + + ) : ( + <> + + {localeString( + 'views.Settings.AddEditNode.macaroon' + )} + + { + // Allow backspace/delete operations without validation + if ( + text.length < + (macaroonHex?.length || 0) + ) { + this.setState({ + macaroonHex: text, + saved: false + }); + return; + } + + // For single character additions + if ( + text.length === + (macaroonHex?.length || 0) + + 1 + ) { + const cleanedText = + text.replace( + /[^0-9a-fA-F]/g, + '' + ); + this.setState({ + macaroonHex: + cleanedText, + saved: false + }); + return; + } + + // For pasted content + const trimmedText = text.trim(); + if ( + !/^[0-9a-fA-F]+$/.test( + trimmedText + ) + ) { + this.props.ModalStore.toggleInfoModal( + localeString( + 'general.pastedInvalidData' + ) + ); + return; + } + + this.setState({ + macaroonHex: trimmedText, + saved: false + }); + }} + locked={loading} + /> + + )} + + onChangeText={(text: string) => { + // Allow backspace/delete operations without validation + if ( + text.length < + (customMailboxServer?.length || + 0) + ) { + this.setState({ + customMailboxServer: + text, + saved: false + }); + return; + } + + // For single character additions + if ( + text.length === + (customMailboxServer?.length || + 0) + + 1 + ) { + if (text.includes('::')) + return; + + const cleanedText = + text.replace( + /[^a-zA-Z0-9-.:]/g, + '' + ); + this.setState({ + customMailboxServer: + cleanedText, + saved: false + }); + return; + } + + // For pasted content + const trimmedText = text.trim(); + if ( + !/^[a-zA-Z0-9-.:]+$/.test( + trimmedText + ) + ) { + this.props.ModalStore.toggleInfoModal( + localeString( + 'general.pastedInvalidData' + ) + ); + return; + } + this.setState({ customMailboxServer: - text.trim(), + trimmedText, saved: false - }) - } + }); + }} + onBlur={() => { + if ( + customMailboxServer && + !/^[a-zA-Z0-9-.]+(:\d+)?$/.test( + customMailboxServer + ) + ) { + this.props.ModalStore.toggleInfoModal( + localeString( + 'general.invalidHost' + ) + ); + } + }} locked={loading} /> @@ -1589,13 +1848,86 @@ export default class WalletConfiguration extends React.Component< placeholder={ 'cherry truth mask employ box silver mass bunker fiscal vote' } + autoCapitalize="none" value={pairingPhrase} - onChangeText={(text: string) => + onChangeText={(text: string) => { + // Allow backspace/delete operations without validation + if ( + text.length < + (pairingPhrase?.length || 0) + ) { + this.setState({ + pairingPhrase: text, + saved: false + }); + return; + } + + // For single character additions + if ( + text.length === + (pairingPhrase?.length || 0) + 1 + ) { + if (text === ' ') return; + if (text.includes(' ')) return; + + const cleanedText = text.replace( + /[^a-zA-Z\s]/g, + '' + ); + this.setState({ + pairingPhrase: cleanedText, + saved: false + }); + return; + } + + // For pasted content + const normalizedPhrase = text + .trim() + .replace(/\s+/g, ' '); + if ( + !/^[a-zA-Z\s]+$/.test( + normalizedPhrase + ) + ) { + this.props.ModalStore.toggleInfoModal( + localeString( + 'general.pastedInvalidData' + ) + ); + return; + } + this.setState({ - pairingPhrase: text, + pairingPhrase: normalizedPhrase, saved: false - }) - } + }); + }} + onBlur={() => { + const normalizedPhrase = pairingPhrase + ?.trim() + .replace(/\s+/g, ' '); + const wordCount = + normalizedPhrase?.split(' ') + .length || 0; + + if ( + normalizedPhrase && + wordCount !== 10 + ) { + this.props.ModalStore.toggleInfoModal( + localeString( + 'views.Settings.AddEditNode.wrongLncPairingPhraseLength' + ) + ); + } + + this.setState({ + pairingPhrase: normalizedPhrase, + saved: false + }); + }} locked={loading} /> {!!localKey && ( @@ -1718,7 +2050,7 @@ export default class WalletConfiguration extends React.Component< }); } }} - disabled={loading} + disabled={loading || !lndhubUrl} /> )} @@ -1842,11 +2174,37 @@ export default class WalletConfiguration extends React.Component< this.saveWalletConfiguration(); } }} - // disable save button if no creds passed + // disable save button if no host and creds passed disabled={ loading || (implementation === 'lndhub' && - !(username && password)) + !( + lndhubUrl && + username && + password + )) || + (implementation === + 'lightning-node-connect' && + (!pairingPhrase || + pairingPhrase + .trim() + .replace(/\s+/g, ' ') + .split(' ').length !== 10 || + (mailboxServer === + 'custom-defined' && + !/^[a-zA-Z0-9-.]+(:\d+)?$/.test( + customMailboxServer + )))) || + ((implementation === 'lnd' || + implementation === 'cln-rest' || + implementation === + 'c-lightning-REST') && + !( + host && + (implementation === 'cln-rest' + ? rune + : macaroonHex) + )) } />