From 696df24f8aebf036ba08178c0d61e1ce4a582723 Mon Sep 17 00:00:00 2001 From: Brian <137178429+nonfungible-human@users.noreply.github.com> Date: Wed, 15 Nov 2023 13:43:59 -0700 Subject: [PATCH] main: make error messages more accurate --- .../components/auth/AuthPage.spec.tsx | 2 +- app/src/__tests__/store/authStore.spec.ts | 4 +- app/src/components/auth/AuthPage.tsx | 66 ++++++++++++++- app/src/i18n/locales/en-US.json | 6 +- app/src/store/stores/authStore.ts | 81 ++++++++++++++++++- 5 files changed, 148 insertions(+), 11 deletions(-) diff --git a/app/src/__tests__/components/auth/AuthPage.spec.tsx b/app/src/__tests__/components/auth/AuthPage.spec.tsx index d6d643352..4ea75926e 100644 --- a/app/src/__tests__/components/auth/AuthPage.spec.tsx +++ b/app/src/__tests__/components/auth/AuthPage.spec.tsx @@ -63,6 +63,6 @@ describe('AuthPage ', () => { const input = getByLabelText('Enter your password in the field above'); fireEvent.change(input, { target: { value: 'test-pw' } }); fireEvent.click(getByText('Submit')); - expect(await findByText('oops, that password is incorrect')).toBeInTheDocument(); + expect(await findByText('failed to connect')).toBeInTheDocument(); }); }); diff --git a/app/src/__tests__/store/authStore.spec.ts b/app/src/__tests__/store/authStore.spec.ts index 80abeb6b4..826228c22 100644 --- a/app/src/__tests__/store/authStore.spec.ts +++ b/app/src/__tests__/store/authStore.spec.ts @@ -37,9 +37,7 @@ describe('AuthStore', () => { if (desc.methodName === 'GetInfo') throw new Error('test-err'); return undefined as any; }); - await expect(store.login('test-pw')).rejects.toThrow( - 'oops, that password is incorrect', - ); + await expect(store.login('test-pw')).rejects.toThrow('failed to connect'); expect(store.credentials).toBe(''); }); diff --git a/app/src/components/auth/AuthPage.tsx b/app/src/components/auth/AuthPage.tsx index f1782b330..b5a9d8366 100644 --- a/app/src/components/auth/AuthPage.tsx +++ b/app/src/components/auth/AuthPage.tsx @@ -4,7 +4,14 @@ import styled from '@emotion/styled'; import { ReactComponent as LogoImage } from 'assets/images/logo.svg'; import { usePrefixedTranslation } from 'hooks'; import { useStore } from 'store'; -import { Background, Button, HeaderOne, Input } from 'components/base'; +import { + Background, + Button, + ChevronDown, + ChevronUp, + HeaderOne, + Input, +} from 'components/base'; const Styled = { Wrapper: styled.div` @@ -42,12 +49,23 @@ const Styled = { `, ErrMessage: styled.div` width: 100%; - margin: 0 0 80px; padding: 5px 0; background-color: ${props => props.theme.colors.pink}; color: ${props => props.theme.colors.offWhite}; text-align: center; `, + ErrDetail: styled.div` + width: 100%; + padding: 5px 0; + color: ${props => props.theme.colors.offWhite}; + text-align: center; + `, + ErrDetailToggle: styled(Button)` + width: 100%; + margin: 0 0 80px; + padding: 5px 0; + background-color: transparent; + `, Submit: styled(Button)` background-color: transparent; `, @@ -58,6 +76,9 @@ const AuthPage: React.FC = () => { const store = useStore(); const [pass, setPass] = useState(''); const [error, setError] = useState(''); + const [errorDetailLit, setErrorDetailLit] = useState(''); + const [errorDetailLnd, setErrorDetailLnd] = useState(''); + const [errorDetailVisible, setErrorDetailVisible] = useState(false); const handleChange = (e: React.ChangeEvent) => { setPass(e.target.value); @@ -70,6 +91,9 @@ const AuthPage: React.FC = () => { await store.authStore.login(pass); } catch (err) { setError(err.message); + const errors = store.authStore.errors; + setErrorDetailLit(errors.litDetail); + setErrorDetailLnd(errors.lndDetail); } }; @@ -77,7 +101,18 @@ const AuthPage: React.FC = () => { // a UI flicker while validating credentials stored in session storage if (!store.initialized) return null; - const { Wrapper, Logo, Title, Subtitle, Form, Label, ErrMessage, Submit } = Styled; + const { + Wrapper, + Logo, + Title, + Subtitle, + Form, + Label, + ErrMessage, + ErrDetail, + ErrDetailToggle, + Submit, + } = Styled; return ( @@ -94,7 +129,30 @@ const AuthPage: React.FC = () => { onChange={handleChange} /> {error ? ( - {error} + <> + {error} + {errorDetailVisible && errorDetailLit.length > 0 ? ( + LiT: {errorDetailLit} + ) : ( + '' + )} + {errorDetailVisible && errorDetailLnd.length > 0 ? ( + LND: {errorDetailLnd} + ) : ( + '' + )} + { + setErrorDetailVisible(!errorDetailVisible); + }} + > + {!errorDetailVisible ? : } + {!errorDetailVisible ? l('showDetail') : l('hideDetail')} + + ) : ( )} diff --git a/app/src/i18n/locales/en-US.json b/app/src/i18n/locales/en-US.json index 78ac05ee1..26971b4af 100644 --- a/app/src/i18n/locales/en-US.json +++ b/app/src/i18n/locales/en-US.json @@ -26,6 +26,8 @@ "cmps.auth.AuthPage.terminal": "Terminal", "cmps.auth.AuthPage.subtitle": "Efficiently manage Lightning node liquidity", "cmps.auth.AuthPage.passLabel": "Enter your password in the field above", + "cmps.auth.AuthPage.showDetail": "Show Detail", + "cmps.auth.AuthPage.hideDetail": "Hide Detail", "cmps.auth.AuthPage.submitBtn": "Submit", "cmps.common.Tile.maximizeTip": "Maximize", "cmps.common.PageHeader.exportTip": "Download CSV", @@ -399,6 +401,8 @@ "cmps.tour.SuccessStep.close": "Close", "stores.authStore.emptyPassErr": "oops, password is required", "stores.authStore.invalidPassErr": "oops, that password is incorrect", + "stores.authStore.noConnectionErr": "failed to connect", + "stores.authStore.walletLockedErr": "oops, wallet is locked", "stores.buildSwapView.noChannelsMsg": "You cannot perform a swap without any active channels", "stores.orderFormView.buy": "Bid", "stores.orderFormView.sell": "Ask", @@ -416,7 +420,7 @@ "stores.settingsStore.httpError": "url must start with 'http'", "stores.settingsStore.keyword": "url must contain {{keyword}}", "stores.appView.authErrorTitle": "Your session has expired", - "stores.appView.authErrorMsg": "Please enter you password to continue", + "stores.appView.authErrorMsg": "Please enter your password to continue", "views.fundNewAccountView.amountTooLow": "must be greater than {{accountMinimum}} sats", "views.fundNewAccountView.amountTooHigh": "must be less than wallet balance", "views.fundNewAccountView.lowExpireBlocks": "must be greater than {{blocks}} blocks", diff --git a/app/src/store/stores/authStore.ts b/app/src/store/stores/authStore.ts index d76d00223..8f42109d4 100644 --- a/app/src/store/stores/authStore.ts +++ b/app/src/store/stores/authStore.ts @@ -14,6 +14,8 @@ export default class AuthStore { /** the password encoded to base64 */ credentials = ''; + errors = { mainErr: '', litDetail: '', lndDetail: '' }; + constructor(store: Store) { makeAutoObservable(this, {}, { deep: false, autoBind: true }); @@ -31,6 +33,80 @@ export default class AuthStore { Object.values(this._store.api).forEach(api => api.setCredentials(credentials)); } + /** + * Convert exception to error message + */ + async getErrMsg(error: string) { + // determine the main error message + const invalidPassMsg = ['expected 1 macaroon, got 0']; + for (const m in invalidPassMsg) { + const errPos = error.lastIndexOf(invalidPassMsg[m]); + if (error.length - invalidPassMsg[m].length == errPos) { + this.errors.mainErr = l('invalidPassErr'); + break; + } + } + + let walletLocked = false; + if (this.errors.mainErr.length == 0) { + const walletLockedMsg = [ + 'wallet locked, unlock it to enable full RPC access', + 'proxy error with context auth: unknown macaroon to use', + ]; + for (const m in walletLockedMsg) { + const errPos = error.lastIndexOf(walletLockedMsg[m]); + if (error.length - walletLockedMsg[m].length == errPos) { + walletLocked = true; + this.errors.mainErr = l('walletLockedErr'); + break; + } + } + } + + if (this.errors.mainErr.length == 0) this.errors.mainErr = l('noConnectionErr'); + + // get the subserver status message + let litStatus = null; + let lndStatus = null; + try { + const serverStatus = await this._store.api.lit.listSubServerStatus(); + const subServersMap = serverStatus['subServersMap']; + for (let e = 0; e < subServersMap.length; e++) { + if (subServersMap[e][0] == 'lit') litStatus = subServersMap[e]; + if (subServersMap[e][0] == 'lnd') lndStatus = subServersMap[e]; + } + + // check for subserver error messages + if (litStatus !== null && litStatus[1].error.length > 0) { + this.errors.litDetail = litStatus[1].error; + } + if (lndStatus !== null && lndStatus[1].error.length > 0) { + this.errors.lndDetail = lndStatus[1].error; + } + + // check status + if ( + this.errors.litDetail.length == 0 && + (litStatus === null || !litStatus[1].running) + ) { + this.errors.litDetail = 'LiT is not running.'; + if (walletLocked) + this.errors.litDetail += ' Please ensure that the wallet is unlocked.'; + } + if ( + this.errors.lndDetail.length == 0 && + (lndStatus === null || !lndStatus[1].running) + ) { + this.errors.lndDetail = 'LND is not running. Please start lnd and try again.'; + } + } catch (e) { + this.errors.litDetail = + 'Unable to connect to LiT. Please restart litd and try again.'; + } + + return this.errors.mainErr; + } + /** * Validate the supplied password and save for later if successful */ @@ -49,8 +125,9 @@ export default class AuthStore { } catch (error) { // clear the credentials if incorrect this.setCredentials(''); - this._store.log.error('incorrect credentials'); - throw new Error(l('invalidPassErr')); + this._store.log.error('connection failure'); + this.errors = { mainErr: '', litDetail: '', lndDetail: '' }; + throw new Error(await this.getErrMsg(error.message)); } }