Repository: polkadot-js/extension
Pull request #594 (2020-12-17 17:35:32)

Show error upon wrong password on Export screen
closes #593 
- add loader to "forget button" (it's too quick on my computer, I can't even see it)
- add error to export with wrong password
- add tests for the above
![error](https://user-images.githubusercontent.com/33178835/102522417-7eb51200-4096-11eb-8a6d-1767ed9e8e0a.gif)


jacogr: Forget is weird. I have had some slow ones (as per the issue I logged) and then faster. 

Patch:
diff --git a/packages/extension-ui/src/Popup/Export.tsx b/packages/extension-ui/src/Popup/Export.tsx
index a25407fa..1ecfb3e9 100644
--- a/packages/extension-ui/src/Popup/Export.tsx
+++ b/packages/extension-ui/src/Popup/Export.tsx
@@ -23,13 +23,20 @@ function Export ({ className, match: { params: { address } } }: Props): React.Re
   const onAction = useContext(ActionContext);
   const [isBusy, setIsBusy] = useState(false);
   const [pass, setPass] = useState('');
-  const [wrongPasswordHighlight, setWrongPasswordHighlight] = useState(false);
+  const [error, setError] = useState('');
 
   const _goHome = useCallback(
     () => onAction('/'),
     [onAction]
   );
 
+  const onPassChange = useCallback(
+    (password: string) => {
+      setPass(password);
+      setError('');
+    }
+    , []);
+
   const _onExportButtonClick = useCallback(
     (): void => {
       setIsBusy(true);
@@ -47,10 +54,8 @@ function Export ({ className, match: { params: { address } } }: Props): React.Re
         })
         .catch((error: Error) => {
           console.error(error);
-
+          setError(error.message);
           setIsBusy(false);
-          setWrongPasswordHighlight(true);
-          setTimeout(() => setWrongPasswordHighlight(false), 100);
         });
     },
     [address, onAction, pass]
@@ -71,17 +76,25 @@ function Export ({ className, match: { params: { address } } }: Props): React.Re
             <InputWithLabel
               data-export-password
               disabled={isBusy}
-              isError={pass.length < MIN_LENGTH || wrongPasswordHighlight}
+              isError={pass.length < MIN_LENGTH || !!error}
               label={t<string>('password for this account')}
-              onChange={setPass}
+              onChange={onPassChange}
               type='password'
             />
+            {error && (
+              <Warning
+                isBelowInput
+                isDanger
+              >
+                {error}
+              </Warning>
+            )}
             <Button
               className='export-button'
               data-export-button
               isBusy={isBusy}
               isDanger
-              isDisabled={pass.length === 0}
+              isDisabled={pass.length === 0 || !!error}
               onClick={_onExportButtonClick}
             >
               {t<string>('I want to export this account')}
@@ -109,6 +122,10 @@ export default withRouter(styled(Export)`
     margin: auto;
   }
 
+  .export-button {
+    margin-top: 6px;
+  }
+
   .movedWarning {
     margin-top: 8px;
   }
diff --git a/packages/extension-ui/src/Popup/Forget.tsx b/packages/extension-ui/src/Popup/Forget.tsx
index 832b62c7..42679bcc 100644
--- a/packages/extension-ui/src/Popup/Forget.tsx
+++ b/packages/extension-ui/src/Popup/Forget.tsx
@@ -3,7 +3,7 @@
 
 import type { ThemeProps } from '../types';
 
-import React, { useCallback, useContext } from 'react';
+import React, { useCallback, useContext, useState } from 'react';
 import { RouteComponentProps, withRouter } from 'react-router';
 import styled from 'styled-components';
 
@@ -19,6 +19,7 @@ interface Props extends RouteComponentProps<{ address: string }>, ThemeProps {
 function Forget ({ className, match: { params: { address } } }: Props): React.ReactElement<Props> {
   const { t } = useTranslation();
   const onAction = useContext(ActionContext);
+  const [isBusy, setIsBusy] = useState(false);
 
   const _goHome = useCallback(
     () => onAction('/'),
@@ -26,10 +27,18 @@ function Forget ({ className, match: { params: { address } } }: Props): React.Re
   );
 
   const _onClick = useCallback(
-    (): Promise<void> =>
+    (): void => {
+      setIsBusy(true);
       forgetAccount(address)
-        .then(() => onAction('/'))
-        .catch((error: Error) => console.error(error)),
+        .then(() => {
+          setIsBusy(false);
+          onAction('/');
+        })
+        .catch((error: Error) => {
+          setIsBusy(false);
+          console.error(error);
+        });
+    },
     [address, onAction]
   );
 
@@ -46,6 +55,7 @@ function Forget ({ className, match: { params: { address } } }: Props): React.Re
           </Warning>
           <div className='actionArea'>
             <Button
+              isBusy={isBusy}
               isDanger
               onClick={_onClick}
             >


Test patch:
diff --git a/packages/extension-ui/src/Popup/Export.test.tsx b/packages/extension-ui/src/Popup/Export.test.tsx
index b24ee1b3..857faf82 100644
--- a/packages/extension-ui/src/Popup/Export.test.tsx
+++ b/packages/extension-ui/src/Popup/Export.test.tsx
@@ -51,6 +51,39 @@ describe('Export component', () => {
     expect(wrapper.find(Button).prop('isDisabled')).toBe(true);
   });
 
+  it('shows an error if the password is wrong', async () => {
+    // eslint-disable-next-line @typescript-eslint/require-await
+    jest.spyOn(messaging, 'exportAccount').mockImplementation(async () => {
+      throw new Error('Unable to decode using the supplied passphrase');
+    });
+    enterPassword();
+    wrapper.find('[data-export-button] button').simulate('click');
+    await act(flushAllPromises);
+    wrapper.update();
+
+    // the first message is "You are exporting your account. Keep it safe and don't share it with anyone."
+    expect(wrapper.find('.warning-message').at(1).text()).toBe('Unable to decode using the supplied passphrase');
+    expect(wrapper.find(Button).prop('isDisabled')).toBe(true);
+    expect(wrapper.find('InputWithLabel').first().prop('isError')).toBe(true);
+  });
+
+  it('shows no error when typing again after a wrong password', async () => {
+    // eslint-disable-next-line @typescript-eslint/require-await
+    jest.spyOn(messaging, 'exportAccount').mockImplementation(async () => {
+      throw new Error('Unable to decode using the supplied passphrase');
+    });
+    enterPassword();
+    wrapper.find('[data-export-button] button').simulate('click');
+    await act(flushAllPromises);
+    wrapper.update();
+    enterPassword();
+
+    // the first message is "You are exporting your account. Keep it safe and don't share it with anyone."
+    expect(wrapper.find('.warning-message')).toHaveLength(1);
+    expect(wrapper.find(Button).prop('isDisabled')).toBe(false);
+    expect(wrapper.find('InputWithLabel').first().prop('isError')).toBe(false);
+  });
+
   it('button is enabled after password is typed', async () => {
     enterPassword();
     await act(flushAllPromises);