Skip to content

Commit df3055c

Browse files
authored
Merge pull request #678 from nonfungible-human/errormessages
main: make error messages more accurate
2 parents 82899ef + 51ea26b commit df3055c

File tree

5 files changed

+184
-14
lines changed

5 files changed

+184
-14
lines changed

app/src/__tests__/components/auth/AuthPage.spec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,6 @@ describe('AuthPage ', () => {
6363
const input = getByLabelText('Enter your password in the field above');
6464
fireEvent.change(input, { target: { value: 'test-pw' } });
6565
fireEvent.click(getByText('Submit'));
66-
expect(await findByText('oops, that password is incorrect')).toBeInTheDocument();
66+
expect(await findByText('failed to connect')).toBeInTheDocument();
6767
});
6868
});

app/src/__tests__/store/authStore.spec.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,7 @@ describe('AuthStore', () => {
3737
if (desc.methodName === 'GetInfo') throw new Error('test-err');
3838
return undefined as any;
3939
});
40-
await expect(store.login('test-pw')).rejects.toThrow(
41-
'oops, that password is incorrect',
42-
);
40+
await expect(store.login('test-pw')).rejects.toThrow('failed to connect');
4341
expect(store.credentials).toBe('');
4442
});
4543

app/src/components/auth/AuthPage.tsx

Lines changed: 95 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import styled from '@emotion/styled';
44
import { ReactComponent as LogoImage } from 'assets/images/logo.svg';
55
import { usePrefixedTranslation } from 'hooks';
66
import { useStore } from 'store';
7-
import { Background, Button, HeaderOne, Input } from 'components/base';
7+
import { Background, Button, ChevronDown, ChevronUp, HeaderOne } from 'components/base';
88

99
const Styled = {
1010
Wrapper: styled.div`
@@ -33,22 +33,57 @@ const Styled = {
3333
text-align: center;
3434
`,
3535
Form: styled.form`
36+
max-width: 550px;
3637
display: flex;
3738
flex-direction: column;
3839
align-items: center;
3940
`,
40-
Label: styled.label`
41-
margin: 10px 0 80px;
41+
Password: styled.input`
42+
font-family: ${props => props.theme.fonts.work.light};
43+
font-weight: 300;
44+
font-size: ${props => props.theme.sizes.xxl};
45+
color: ${props => props.theme.colors.offWhite};
46+
background-color: transparent;
47+
border-width: 0;
48+
border-bottom: 3px solid ${props => props.theme.colors.offWhite};
49+
padding: 5px;
50+
text-align: center;
51+
width: 100%;
52+
53+
&:active,
54+
&:focus {
55+
outline: none;
56+
background-color: ${props => props.theme.colors.overlay};
57+
border-bottom-color: ${props => props.theme.colors.white};
58+
}
59+
60+
&::placeholder {
61+
color: ${props => props.theme.colors.gray};
62+
}
4263
`,
64+
Label: styled.label``,
4365
ErrMessage: styled.div`
4466
width: 100%;
45-
margin: 0 0 80px;
67+
display: inline-block;
4668
padding: 5px 0;
4769
background-color: ${props => props.theme.colors.pink};
4870
color: ${props => props.theme.colors.offWhite};
4971
text-align: center;
5072
`,
73+
ErrDetail: styled.div`
74+
width: 100%;
75+
display: inline-block;
76+
padding: 5px 0;
77+
color: ${props => props.theme.colors.offWhite};
78+
text-align: center;
79+
`,
80+
ErrDetailToggle: styled(Button)`
81+
width: 100%;
82+
padding: 5px 0;
83+
background-color: transparent;
84+
`,
5185
Submit: styled(Button)`
86+
margin-top: 80px;
5287
background-color: transparent;
5388
`,
5489
};
@@ -58,10 +93,17 @@ const AuthPage: React.FC = () => {
5893
const store = useStore();
5994
const [pass, setPass] = useState('');
6095
const [error, setError] = useState('');
96+
const [errorDetailLit, setErrorDetailLit] = useState('');
97+
const [errorDetailLnd, setErrorDetailLnd] = useState('');
98+
const [errorDetailVisible, setErrorDetailVisible] = useState(false);
99+
const [showDetailButton, setShowDetailButton] = useState(true);
61100

62101
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
63102
setPass(e.target.value);
64103
setError('');
104+
setErrorDetailLit('');
105+
setErrorDetailLnd('');
106+
setShowDetailButton(false);
65107
};
66108

67109
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
@@ -70,14 +112,32 @@ const AuthPage: React.FC = () => {
70112
await store.authStore.login(pass);
71113
} catch (err) {
72114
setError(err.message);
115+
const errors = store.authStore.errors;
116+
setErrorDetailLit(errors.litDetail);
117+
setErrorDetailLnd(errors.lndDetail);
118+
119+
// don't display the detail toggle button if there is nothing to display
120+
setShowDetailButton(errors.litDetail.length > 0 || errors.litDetail.length > 0);
73121
}
74122
};
75123

76124
// don't display the login UI until the app is fully initialized this prevents
77125
// a UI flicker while validating credentials stored in session storage
78126
if (!store.initialized) return null;
79127

80-
const { Wrapper, Logo, Title, Subtitle, Form, Label, ErrMessage, Submit } = Styled;
128+
const {
129+
Wrapper,
130+
Logo,
131+
Title,
132+
Subtitle,
133+
Form,
134+
Password,
135+
Label,
136+
ErrMessage,
137+
ErrDetail,
138+
ErrDetailToggle,
139+
Submit,
140+
} = Styled;
81141
return (
82142
<Background gradient>
83143
<Wrapper>
@@ -86,15 +146,43 @@ const AuthPage: React.FC = () => {
86146
<Title>{l('terminal')}</Title>
87147
<Subtitle>{l('subtitle')}</Subtitle>
88148
<Form onSubmit={handleSubmit}>
89-
<Input
149+
<Password
90150
id="auth"
91151
type="password"
92152
autoFocus
93153
value={pass}
94154
onChange={handleChange}
95155
/>
96156
{error ? (
97-
<ErrMessage>{error}</ErrMessage>
157+
<>
158+
<ErrMessage>{error}</ErrMessage>
159+
{errorDetailVisible && errorDetailLit.length > 0 ? (
160+
<ErrDetail>{errorDetailLit}</ErrDetail>
161+
) : (
162+
''
163+
)}
164+
{errorDetailVisible && errorDetailLnd.length > 0 ? (
165+
<ErrDetail>{errorDetailLnd}</ErrDetail>
166+
) : (
167+
''
168+
)}
169+
{showDetailButton ? (
170+
<ErrDetailToggle
171+
ghost
172+
borderless
173+
compact
174+
type="button"
175+
onClick={() => {
176+
setErrorDetailVisible(!errorDetailVisible);
177+
}}
178+
>
179+
{!errorDetailVisible ? <ChevronDown /> : <ChevronUp />}
180+
{!errorDetailVisible ? l('showDetail') : l('hideDetail')}
181+
</ErrDetailToggle>
182+
) : (
183+
''
184+
)}
185+
</>
98186
) : (
99187
<Label htmlFor="auth">{l('passLabel')}</Label>
100188
)}

app/src/i18n/locales/en-US.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
"cmps.auth.AuthPage.terminal": "Terminal",
2727
"cmps.auth.AuthPage.subtitle": "Efficiently manage Lightning node liquidity",
2828
"cmps.auth.AuthPage.passLabel": "Enter your password in the field above",
29+
"cmps.auth.AuthPage.showDetail": "Show Detail",
30+
"cmps.auth.AuthPage.hideDetail": "Hide Detail",
2931
"cmps.auth.AuthPage.submitBtn": "Submit",
3032
"cmps.common.Tile.maximizeTip": "Maximize",
3133
"cmps.common.PageHeader.exportTip": "Download CSV",
@@ -399,6 +401,12 @@
399401
"cmps.tour.SuccessStep.close": "Close",
400402
"stores.authStore.emptyPassErr": "oops, password is required",
401403
"stores.authStore.invalidPassErr": "oops, that password is incorrect",
404+
"stores.authStore.noConnectionErr": "failed to connect",
405+
"stores.authStore.walletLockedErr": "oops, wallet is locked",
406+
"stores.authStore.litNotConnected": "Unable to connect to LiT. Please restart litd and try again.",
407+
"stores.authStore.litNotRunning": "LiT is not running.",
408+
"stores.authStore.lndNotRunning": "LND is not running. Please start lnd and try again.",
409+
"stores.authStore.suggestWalletUnlock": " Please ensure that the wallet is unlocked.",
402410
"stores.buildSwapView.noChannelsMsg": "You cannot perform a swap without any active channels",
403411
"stores.orderFormView.buy": "Bid",
404412
"stores.orderFormView.sell": "Ask",
@@ -416,7 +424,7 @@
416424
"stores.settingsStore.httpError": "url must start with 'http'",
417425
"stores.settingsStore.keyword": "url must contain {{keyword}}",
418426
"stores.appView.authErrorTitle": "Your session has expired",
419-
"stores.appView.authErrorMsg": "Please enter you password to continue",
427+
"stores.appView.authErrorMsg": "Please enter your password to continue",
420428
"views.fundNewAccountView.amountTooLow": "must be greater than {{accountMinimum}} sats",
421429
"views.fundNewAccountView.amountTooHigh": "must be less than wallet balance",
422430
"views.fundNewAccountView.lowExpireBlocks": "must be greater than {{blocks}} blocks",

app/src/store/stores/authStore.ts

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,18 @@ import { Store } from 'store';
55

66
const { l } = prefixTranslation('stores.authStore');
77

8+
class SubServerStatus {
9+
disabled: boolean;
10+
error: string;
11+
running: boolean;
12+
13+
constructor() {
14+
this.disabled = false;
15+
this.error = '';
16+
this.running = false;
17+
}
18+
}
19+
820
export default class AuthStore {
921
private _store: Store;
1022

@@ -14,6 +26,8 @@ export default class AuthStore {
1426
/** the password encoded to base64 */
1527
credentials = '';
1628

29+
errors = { mainErr: '', litDetail: '', lndDetail: '' };
30+
1731
constructor(store: Store) {
1832
makeAutoObservable(this, {}, { deep: false, autoBind: true });
1933

@@ -31,6 +45,67 @@ export default class AuthStore {
3145
Object.values(this._store.api).forEach(api => api.setCredentials(credentials));
3246
}
3347

48+
/**
49+
* Convert exception to error message
50+
*/
51+
async getErrMsg(error: string) {
52+
// determine the main error message
53+
const invalidPassMsg = ['expected 1 macaroon, got 0'];
54+
for (const m in invalidPassMsg) {
55+
const errPos = error.lastIndexOf(invalidPassMsg[m]);
56+
if (error.length - invalidPassMsg[m].length == errPos) {
57+
this.errors.mainErr = l('invalidPassErr');
58+
break;
59+
}
60+
}
61+
62+
let walletLocked = false;
63+
if (this.errors.mainErr.length == 0) {
64+
const walletLockedMsg = [
65+
'wallet locked, unlock it to enable full RPC access',
66+
'proxy error with context auth: unknown macaroon to use',
67+
];
68+
for (const m in walletLockedMsg) {
69+
const errPos = error.lastIndexOf(walletLockedMsg[m]);
70+
if (error.length - walletLockedMsg[m].length == errPos) {
71+
walletLocked = true;
72+
this.errors.mainErr = l('walletLockedErr');
73+
break;
74+
}
75+
}
76+
}
77+
78+
if (this.errors.mainErr.length == 0) this.errors.mainErr = l('noConnectionErr');
79+
80+
// get the subserver status message
81+
try {
82+
const serverStatus = await this._store.api.lit.listSubServerStatus();
83+
// convert the response's nested arrays to an object mapping `subServerName` -> `{ disabled, running, error }`
84+
const status = serverStatus.subServersMap.reduce(
85+
(acc, [serverName, serverStatus]) => ({ ...acc, [serverName]: serverStatus }),
86+
{} as Record<string, SubServerStatus>,
87+
);
88+
89+
// check status
90+
if (status.lit?.error) {
91+
this.errors.litDetail = status.lit.error;
92+
} else if (!status.lit?.running) {
93+
this.errors.litDetail = l('litNotRunning');
94+
if (walletLocked) this.errors.litDetail += l('suggestWalletUnlock');
95+
}
96+
97+
if (status.lnd?.error) {
98+
this.errors.lndDetail = status.lnd.error;
99+
} else if (!status.lnd?.running) {
100+
this.errors.lndDetail = l('lndNotRunning');
101+
}
102+
} catch (e) {
103+
this.errors.litDetail = l('litNotConnected');
104+
}
105+
106+
return this.errors.mainErr;
107+
}
108+
34109
/**
35110
* Validate the supplied password and save for later if successful
36111
*/
@@ -49,8 +124,9 @@ export default class AuthStore {
49124
} catch (error) {
50125
// clear the credentials if incorrect
51126
this.setCredentials('');
52-
this._store.log.error('incorrect credentials');
53-
throw new Error(l('invalidPassErr'));
127+
this._store.log.error('connection failure');
128+
this.errors = { mainErr: '', litDetail: '', lndDetail: '' };
129+
throw new Error(await this.getErrMsg(error.message));
54130
}
55131
}
56132

0 commit comments

Comments
 (0)