Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improved UX and error handling for wallet configuration #2594

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions components/TextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface TextInputProps {
numberOfLines?: number;
style?: ViewStyle;
textInputStyle?: StyleProp<TextStyle>;
textColor?: string;
placeholderTextColor?: string;
locked?: boolean;
keyboardType?: KeyboardTypeOptions;
Expand All @@ -36,6 +37,7 @@ interface TextInputProps {
ref?: React.Ref<TextInputRN>;
error?: boolean;
onFocus?: any;
onBlur?: any;
onSubmitEditing?: () => void;
}

Expand All @@ -48,6 +50,7 @@ const TextInput = React.forwardRef<TextInputRN, TextInputProps>(
numberOfLines,
style,
textInputStyle,
textColor,
placeholderTextColor,
locked,
keyboardType,
Expand All @@ -62,7 +65,9 @@ const TextInput = React.forwardRef<TextInputRN, TextInputProps>(
toggleUnits,
onPressIn,
right,
error
error,
onFocus,
onBlur
} = props;
const defaultStyle = numberOfLines
? {
Expand Down Expand Up @@ -153,9 +158,11 @@ const TextInput = React.forwardRef<TextInputRN, TextInputProps>(
style={{
...StyleSheet.flatten(textInputStyle),
...styles.input,
color: locked
? themeColor('secondaryText')
: themeColor('text')
color:
textColor ||
(locked
? themeColor('secondaryText')
: themeColor('text'))
}}
placeholderTextColor={
placeholderTextColor || themeColor('secondaryText')
Expand All @@ -170,6 +177,8 @@ const TextInput = React.forwardRef<TextInputRN, TextInputProps>(
onPressIn={onPressIn}
onSubmitEditing={props.onSubmitEditing}
ref={ref}
onFocus={onFocus}
onBlur={onBlur}
/>
{suffix ? (
toggleUnits ? (
Expand Down
1 change: 1 addition & 0 deletions locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1115,6 +1115,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",
Expand Down
11 changes: 11 additions & 0 deletions utils/ErrorUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,17 @@ describe('ErrorUtils', () => {
).toEqual(
'Unable to connect to node. Please verify the host and port are correct and the service is running.'
);
expect(
errorToUserFriendly(
Object.assign(new Error(), {
message:
'Error: {"code":2,"message":"verification failed: signature mismatch after caveat verification","details":[]}',
name: 'test'
})
)
).toEqual(
"Invalid macaroon. Please check that you've entered the correct macaroon for this node."
);
});

it('Returns normal error message for unhandled errorContext', () => {
Expand Down
2 changes: 2 additions & 0 deletions utils/ErrorUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ const userFriendlyErrors: any = {
'error.torBootstrap',
'Error: Failed to connect to': 'error.nodeConnectError',
'Error: Unable to resolve host': 'error.nodeConnectError',
'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',
Expand Down
197 changes: 197 additions & 0 deletions utils/ValidationUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import ValidationUtils from './ValidationUtils';

describe('isValidHostAndPort', () => {
it('accepts valid hostname without port', () => {
expect(ValidationUtils.isValidHostAndPort('example.com')).toBe(true);
});

it('accepts valid hostname with port', () => {
expect(ValidationUtils.isValidHostAndPort('example.com:8080')).toBe(
true
);
});

it('accepts valid hostname with protocol', () => {
expect(ValidationUtils.isValidHostAndPort('https://example.com')).toBe(
true
);
expect(ValidationUtils.isValidHostAndPort('http://example.com')).toBe(
true
);
});

it('rejects invalid port numbers', () => {
expect(ValidationUtils.isValidHostAndPort('example.com:0')).toBe(false);
expect(ValidationUtils.isValidHostAndPort('example.com:65536')).toBe(
false
);
});

it('rejects invalid hostnames', () => {
expect(ValidationUtils.isValidHostAndPort('example..com')).toBe(false);
expect(ValidationUtils.isValidHostAndPort('-example.com')).toBe(false);
});
});

describe('isValidHttpsHostAndPort', () => {
it('accepts valid hostname without protocol', () => {
expect(ValidationUtils.isValidHttpsHostAndPort('example.com')).toBe(
true
);
});

it('accepts valid hostname with https protocol', () => {
expect(
ValidationUtils.isValidHttpsHostAndPort('https://example.com')
).toBe(true);
});

it('accepts valid hostname with port', () => {
expect(
ValidationUtils.isValidHttpsHostAndPort('example.com:8080')
).toBe(true);
expect(
ValidationUtils.isValidHttpsHostAndPort('https://example.com:8080')
).toBe(true);
});

it('rejects hostname with http protocol', () => {
expect(
ValidationUtils.isValidHttpsHostAndPort('http://example.com')
).toBe(false);
expect(
ValidationUtils.isValidHttpsHostAndPort('http://example.com:8080')
).toBe(false);
});

it('rejects invalid port numbers', () => {
expect(ValidationUtils.isValidHttpsHostAndPort('example.com:0')).toBe(
false
);
expect(
ValidationUtils.isValidHttpsHostAndPort('example.com:65536')
).toBe(false);
});

it('rejects invalid hostnames', () => {
expect(ValidationUtils.isValidHttpsHostAndPort('example..com')).toBe(
false
);
expect(ValidationUtils.isValidHttpsHostAndPort('-example.com')).toBe(
false
);
});
});

describe('isValidHostname', () => {
it('accepts valid hostnames', () => {
expect(ValidationUtils.isValidHostname('example.com')).toBe(true);
expect(ValidationUtils.isValidHostname('sub.example.com')).toBe(true);
expect(ValidationUtils.isValidHostname('example-domain.com')).toBe(
true
);
expect(ValidationUtils.isValidHostname('localhost')).toBe(true);
expect(ValidationUtils.isValidHostname('10.0.2.2')).toBe(true);
});

it('accepts hostnames with protocol', () => {
expect(ValidationUtils.isValidHostname('http://example.com')).toBe(
true
);
expect(ValidationUtils.isValidHostname('https://example.com')).toBe(
true
);
});

it('rejects invalid hostnames', () => {
expect(ValidationUtils.isValidHostname('example..com')).toBe(false);
expect(ValidationUtils.isValidHostname('.example.com')).toBe(false);
expect(ValidationUtils.isValidHostname('example.com:')).toBe(false);
});
});

describe('isValidPort', () => {
it('accepts valid port numbers', () => {
expect(ValidationUtils.isValidPort('80')).toBe(true);
expect(ValidationUtils.isValidPort('8080')).toBe(true);
expect(ValidationUtils.isValidPort('65535')).toBe(true);
});

it('rejects invalid port numbers', () => {
expect(ValidationUtils.isValidPort('0')).toBe(false);
expect(ValidationUtils.isValidPort('65536')).toBe(false);
expect(ValidationUtils.isValidPort('-1')).toBe(false);
});
});

describe('hasValidRuneChars', () => {
it('accepts valid rune characters', () => {
expect(ValidationUtils.hasValidRuneChars('abcDEF123-_=')).toBe(true);
expect(ValidationUtils.hasValidRuneChars('ABC123')).toBe(true);
});

it('rejects invalid rune characters', () => {
expect(ValidationUtils.hasValidRuneChars('abc!')).toBe(false);
expect(ValidationUtils.hasValidRuneChars('abc@')).toBe(false);
expect(ValidationUtils.hasValidRuneChars('abc space')).toBe(false);
});
});

describe('hasValidMacaroonChars', () => {
it('accepts valid macaroon hex characters', () => {
expect(ValidationUtils.hasValidMacaroonChars('0123456789abcdef')).toBe(
true
);
expect(ValidationUtils.hasValidMacaroonChars('ABCDEF')).toBe(true);
});

it('rejects invalid macaroon hex characters', () => {
expect(ValidationUtils.hasValidMacaroonChars('0123g')).toBe(false);
expect(ValidationUtils.hasValidMacaroonChars('abcd!')).toBe(false);
expect(ValidationUtils.hasValidMacaroonChars('0123 4567')).toBe(false);
});
});

describe('hasValidPairingPhraseCharsAndWordcount', () => {
it('accepts valid 10-word pairing phrase', () => {
expect(
ValidationUtils.hasValidPairingPhraseCharsAndWordcount(
'cherry truth mask employ box silver mass bunker fiscal vote'
)
).toBe(true);
});

it('accepts phrase with extra spaces and normalizes', () => {
expect(
ValidationUtils.hasValidPairingPhraseCharsAndWordcount(
' cherry truth mask employ box silver mass bunker fiscal vote '
)
).toBe(true);
});

it('rejects phrases with wrong word count', () => {
expect(
ValidationUtils.hasValidPairingPhraseCharsAndWordcount(
'less than ten words'
)
).toBe(false);
expect(
ValidationUtils.hasValidPairingPhraseCharsAndWordcount(
'more than ten words truth mask employ box silver mass bunker'
)
).toBe(false);
});

it('rejects phrases with invalid characters', () => {
expect(
ValidationUtils.hasValidPairingPhraseCharsAndWordcount(
'cherry truth mask employ box silver mass bunker fiscal 123'
)
).toBe(false);
expect(
ValidationUtils.hasValidPairingPhraseCharsAndWordcount(
'cherry truth mask employ box silver mass bunker fiscal @'
)
).toBe(false);
});
});
68 changes: 68 additions & 0 deletions utils/ValidationUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
const isValidHostAndPort = (input: string): boolean => {
const urlPattern =
/^(https?:\/\/)?(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])(:\d+)?$/;
const portPattern = /:\d+$/;

if (!urlPattern.test(input)) return false;

const portMatch = input.match(portPattern);
if (portMatch) {
const port = parseInt(portMatch[0].substring(1));
if (port < 1 || port > 65535) return false;
}

return true;
};

const isValidHttpsHostAndPort = (input: string): boolean => {
const urlPattern =
/^(https:\/\/)?(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])(:\d+)?$/;
const portPattern = /:\d+$/;

if (!urlPattern.test(input)) return false;

const portMatch = input.match(portPattern);
if (portMatch) {
const port = parseInt(portMatch[0].substring(1));
if (port < 1 || port > 65535) return false;
}

return true;
};

const isValidHostname = (hostname: string): boolean => {
const urlPattern =
/^(https?:\/\/)?(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/;
return urlPattern.test(hostname);
};

const isValidPort = (port: string): boolean => {
const portNum = parseInt(port);
return portNum >= 1 && portNum <= 65535;
};

const hasValidRuneChars = (rune: string): boolean => {
return /^[A-Za-z0-9\-_=]+$/.test(rune);
};

const hasValidMacaroonChars = (macaroon: string): boolean => {
return /^[0-9a-fA-F]+$/.test(macaroon);
};

const hasValidPairingPhraseCharsAndWordcount = (phrase: string): boolean => {
const normalizedPhrase = phrase.trim().replace(/\s+/g, ' ');
if (!/^[a-zA-Z\s]+$/.test(normalizedPhrase)) return false;
return normalizedPhrase.split(' ').length === 10;
};

const ValidationUtils = {
isValidHostAndPort,
isValidHttpsHostAndPort,
isValidHostname,
isValidPort,
hasValidRuneChars,
hasValidMacaroonChars,
hasValidPairingPhraseCharsAndWordcount
};

export default ValidationUtils;
Loading
Loading