mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 01:47:00 +01:00
Adding periodic reminder modal for backing up recovery phrase (#11021)
* Adding recurring recovery phrase reminder modal * Refactoring per PR feedback
This commit is contained in:
parent
e76762548c
commit
9932c40651
@ -1457,6 +1457,30 @@
|
||||
"recipientAddressPlaceholder": {
|
||||
"message": "Search, public address (0x), or ENS"
|
||||
},
|
||||
"recoveryPhraseReminderBackupStart": {
|
||||
"message": "Start here"
|
||||
},
|
||||
"recoveryPhraseReminderConfirm": {
|
||||
"message": "Got it"
|
||||
},
|
||||
"recoveryPhraseReminderHasBackedUp": {
|
||||
"message": "Always keep your Secret Recovery Phrase in a secure and secret place"
|
||||
},
|
||||
"recoveryPhraseReminderHasNotBackedUp": {
|
||||
"message": "Need to backup your Secret Recovery Phrase again?"
|
||||
},
|
||||
"recoveryPhraseReminderItemOne": {
|
||||
"message": "Never share your Secret Recovery Phrase with anyone"
|
||||
},
|
||||
"recoveryPhraseReminderItemTwo": {
|
||||
"message": "The MetaMask team will never ask for your Secret Recovery Phrase"
|
||||
},
|
||||
"recoveryPhraseReminderSubText": {
|
||||
"message": "Your Secret Recovery Phrase controls all of your accounts."
|
||||
},
|
||||
"recoveryPhraseReminderTitle": {
|
||||
"message": "Protect your funds"
|
||||
},
|
||||
"reject": {
|
||||
"message": "Reject"
|
||||
},
|
||||
|
@ -24,6 +24,8 @@ export default class AppStateController extends EventEmitter {
|
||||
connectedStatusPopoverHasBeenShown: true,
|
||||
defaultHomeActiveTabName: null,
|
||||
browserEnvironment: {},
|
||||
recoveryPhraseReminderHasBeenShown: false,
|
||||
recoveryPhraseReminderLastShown: new Date().getTime(),
|
||||
...initState,
|
||||
});
|
||||
this.timer = null;
|
||||
@ -112,6 +114,27 @@ export default class AppStateController extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Record that the user has been shown the recovery phrase reminder
|
||||
* @returns {void}
|
||||
*/
|
||||
setRecoveryPhraseReminderHasBeenShown() {
|
||||
this.store.updateState({
|
||||
recoveryPhraseReminderHasBeenShown: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Record the timestamp of the last time the user has seen the recovery phrase reminder
|
||||
* @param {number} lastShown - timestamp when user was last shown the reminder
|
||||
* @returns {void}
|
||||
*/
|
||||
setRecoveryPhraseReminderLastShown(lastShown) {
|
||||
this.store.updateState({
|
||||
recoveryPhraseReminderLastShown: lastShown,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the last active time to the current time
|
||||
* @returns {void}
|
||||
|
@ -780,6 +780,14 @@ export default class MetamaskController extends EventEmitter {
|
||||
this.appStateController.setConnectedStatusPopoverHasBeenShown,
|
||||
this.appStateController,
|
||||
),
|
||||
setRecoveryPhraseReminderHasBeenShown: nodeify(
|
||||
this.appStateController.setRecoveryPhraseReminderHasBeenShown,
|
||||
this.appStateController,
|
||||
),
|
||||
setRecoveryPhraseReminderLastShown: nodeify(
|
||||
this.appStateController.setRecoveryPhraseReminderLastShown,
|
||||
this.appStateController,
|
||||
),
|
||||
|
||||
// EnsController
|
||||
tryReverseResolveAddress: nodeify(
|
||||
|
32
app/scripts/migrations/061.js
Normal file
32
app/scripts/migrations/061.js
Normal file
@ -0,0 +1,32 @@
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
const version = 61;
|
||||
|
||||
/**
|
||||
* Initialize attributes related to recovery seed phrase reminder
|
||||
*/
|
||||
export default {
|
||||
version,
|
||||
async migrate(originalVersionedData) {
|
||||
const versionedData = cloneDeep(originalVersionedData);
|
||||
versionedData.meta.version = version;
|
||||
const state = versionedData.data;
|
||||
const newState = transformState(state);
|
||||
versionedData.data = newState;
|
||||
return versionedData;
|
||||
},
|
||||
};
|
||||
|
||||
function transformState(state) {
|
||||
const currentTime = new Date().getTime();
|
||||
if (state.AppStateController) {
|
||||
state.AppStateController.recoveryPhraseReminderHasBeenShown = false;
|
||||
state.AppStateController.recoveryPhraseReminderLastShown = currentTime;
|
||||
} else {
|
||||
state.AppStateController = {
|
||||
recoveryPhraseReminderHasBeenShown: false,
|
||||
recoveryPhraseReminderLastShown: currentTime,
|
||||
};
|
||||
}
|
||||
return state;
|
||||
}
|
67
app/scripts/migrations/061.test.js
Normal file
67
app/scripts/migrations/061.test.js
Normal file
@ -0,0 +1,67 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import sinon from 'sinon';
|
||||
import migration61 from './061';
|
||||
|
||||
describe('migration #61', function () {
|
||||
let dateStub;
|
||||
|
||||
beforeEach(function () {
|
||||
dateStub = sinon.stub(Date.prototype, 'getTime').returns(1621580400000);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
dateStub.restore();
|
||||
});
|
||||
|
||||
it('should update the version metadata', async function () {
|
||||
const oldStorage = {
|
||||
meta: {
|
||||
version: 60,
|
||||
},
|
||||
data: {},
|
||||
};
|
||||
|
||||
const newStorage = await migration61.migrate(oldStorage);
|
||||
assert.deepEqual(newStorage.meta, {
|
||||
version: 61,
|
||||
});
|
||||
});
|
||||
|
||||
it('should set recoveryPhraseReminderHasBeenShown to false and recoveryPhraseReminderLastShown to the current time', async function () {
|
||||
const oldStorage = {
|
||||
meta: {},
|
||||
data: {
|
||||
AppStateController: {
|
||||
existingProperty: 'foo',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const newStorage = await migration61.migrate(oldStorage);
|
||||
assert.deepEqual(newStorage.data, {
|
||||
AppStateController: {
|
||||
recoveryPhraseReminderHasBeenShown: false,
|
||||
recoveryPhraseReminderLastShown: 1621580400000,
|
||||
existingProperty: 'foo',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should initialize AppStateController if it does not exist', async function () {
|
||||
const oldStorage = {
|
||||
meta: {},
|
||||
data: {
|
||||
existingProperty: 'foo',
|
||||
},
|
||||
};
|
||||
|
||||
const newStorage = await migration61.migrate(oldStorage);
|
||||
assert.deepEqual(newStorage.data, {
|
||||
existingProperty: 'foo',
|
||||
AppStateController: {
|
||||
recoveryPhraseReminderHasBeenShown: false,
|
||||
recoveryPhraseReminderLastShown: 1621580400000,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
@ -65,6 +65,7 @@ const migrations = [
|
||||
require('./058').default,
|
||||
require('./059').default,
|
||||
require('./060').default,
|
||||
require('./061').default,
|
||||
];
|
||||
|
||||
export default migrations;
|
||||
|
5
shared/constants/time.js
Normal file
5
shared/constants/time.js
Normal file
@ -0,0 +1,5 @@
|
||||
export const MILLISECOND = 1;
|
||||
export const SECOND = MILLISECOND * 1000;
|
||||
export const MINUTE = SECOND * 60;
|
||||
export const HOUR = MINUTE * 60;
|
||||
export const DAY = HOUR * 24;
|
@ -23,6 +23,7 @@
|
||||
@import 'permission-page-container/index';
|
||||
@import 'permissions-connect-footer/index';
|
||||
@import 'permissions-connect-header/index';
|
||||
@import 'recovery-phrase-reminder/index';
|
||||
@import 'selected-account/index';
|
||||
@import 'sidebars/index';
|
||||
@import 'signature-request/index';
|
||||
|
1
ui/components/app/recovery-phrase-reminder/index.js
Normal file
1
ui/components/app/recovery-phrase-reminder/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './recovery-phrase-reminder';
|
10
ui/components/app/recovery-phrase-reminder/index.scss
Normal file
10
ui/components/app/recovery-phrase-reminder/index.scss
Normal file
@ -0,0 +1,10 @@
|
||||
.recovery-phrase-reminder {
|
||||
&__list {
|
||||
list-style: disc;
|
||||
padding-left: 20px;
|
||||
|
||||
li {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||
// Components
|
||||
import Box from '../../ui/box';
|
||||
import Button from '../../ui/button';
|
||||
import Popover from '../../ui/popover';
|
||||
import Typography from '../../ui/typography';
|
||||
// Helpers
|
||||
import {
|
||||
COLORS,
|
||||
DISPLAY,
|
||||
TEXT_ALIGN,
|
||||
TYPOGRAPHY,
|
||||
BLOCK_SIZES,
|
||||
FONT_WEIGHT,
|
||||
JUSTIFY_CONTENT,
|
||||
} from '../../../helpers/constants/design-system';
|
||||
import { INITIALIZE_BACKUP_SEED_PHRASE_ROUTE } from '../../../helpers/constants/routes';
|
||||
|
||||
export default function RecoveryPhraseReminder({ onConfirm, hasBackedUp }) {
|
||||
const t = useI18nContext();
|
||||
const history = useHistory();
|
||||
|
||||
const handleBackUp = () => {
|
||||
history.push(INITIALIZE_BACKUP_SEED_PHRASE_ROUTE);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover centerTitle title={t('recoveryPhraseReminderTitle')}>
|
||||
<Box padding={[0, 4, 6, 4]} className="recovery-phrase-reminder">
|
||||
<Typography
|
||||
color={COLORS.BLACK}
|
||||
align={TEXT_ALIGN.CENTER}
|
||||
variant={TYPOGRAPHY.Paragraph}
|
||||
boxProps={{ marginTop: 0, marginBottom: 4 }}
|
||||
>
|
||||
{t('recoveryPhraseReminderSubText')}
|
||||
</Typography>
|
||||
<Box margin={[4, 0, 8, 0]}>
|
||||
<ul className="recovery-phrase-reminder__list">
|
||||
<li>
|
||||
<Typography
|
||||
tag="span"
|
||||
color={COLORS.BLACK}
|
||||
fontWeight={FONT_WEIGHT.BOLD}
|
||||
>
|
||||
{t('recoveryPhraseReminderItemOne')}
|
||||
</Typography>
|
||||
</li>
|
||||
<li>{t('recoveryPhraseReminderItemTwo')}</li>
|
||||
<li>
|
||||
{hasBackedUp ? (
|
||||
t('recoveryPhraseReminderHasBackedUp')
|
||||
) : (
|
||||
<>
|
||||
{t('recoveryPhraseReminderHasNotBackedUp')}
|
||||
<Box display={DISPLAY.INLINE_BLOCK} marginLeft={1}>
|
||||
<Button
|
||||
type="link"
|
||||
onClick={handleBackUp}
|
||||
style={{
|
||||
fontSize: 'inherit',
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
{t('recoveryPhraseReminderBackupStart')}
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
</Box>
|
||||
<Box justifyContent={JUSTIFY_CONTENT.CENTER}>
|
||||
<Box width={BLOCK_SIZES.TWO_FIFTHS}>
|
||||
<Button rounded type="primary" onClick={onConfirm}>
|
||||
{t('recoveryPhraseReminderConfirm')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
RecoveryPhraseReminder.propTypes = {
|
||||
hasBackedUp: PropTypes.bool.isRequired,
|
||||
onConfirm: PropTypes.func.isRequired,
|
||||
};
|
@ -47,6 +47,10 @@
|
||||
margin-right: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
&.center {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
|
@ -17,6 +17,7 @@ const Popover = ({
|
||||
showArrow,
|
||||
CustomBackground,
|
||||
popoverRef,
|
||||
centerTitle,
|
||||
}) => {
|
||||
const t = useI18nContext();
|
||||
return (
|
||||
@ -32,7 +33,12 @@ const Popover = ({
|
||||
>
|
||||
{showArrow ? <div className="popover-arrow" /> : null}
|
||||
<header className="popover-header">
|
||||
<div className="popover-header__title">
|
||||
<div
|
||||
className={classnames(
|
||||
'popover-header__title',
|
||||
centerTitle ? 'center' : '',
|
||||
)}
|
||||
>
|
||||
<h2 title={title}>
|
||||
{onBack ? (
|
||||
<button
|
||||
@ -43,12 +49,14 @@ const Popover = ({
|
||||
) : null}
|
||||
{title}
|
||||
</h2>
|
||||
<button
|
||||
className="fas fa-times popover-header__button"
|
||||
title={t('close')}
|
||||
data-testid="popover-close"
|
||||
onClick={onClose}
|
||||
/>
|
||||
{onClose ? (
|
||||
<button
|
||||
className="fas fa-times popover-header__button"
|
||||
title={t('close')}
|
||||
data-testid="popover-close"
|
||||
onClick={onClose}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{subtitle ? (
|
||||
<p className="popover-header__subtitle">{subtitle}</p>
|
||||
@ -76,7 +84,7 @@ Popover.propTypes = {
|
||||
footer: PropTypes.node,
|
||||
footerClassName: PropTypes.string,
|
||||
onBack: PropTypes.func,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func,
|
||||
CustomBackground: PropTypes.func,
|
||||
contentClassName: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
@ -84,6 +92,7 @@ Popover.propTypes = {
|
||||
popoverRef: PropTypes.shape({
|
||||
current: PropTypes.instanceOf(window.Element),
|
||||
}),
|
||||
centerTitle: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default class PopoverPortal extends PureComponent {
|
||||
|
@ -14,6 +14,7 @@ import ConnectedAccounts from '../connected-accounts';
|
||||
import { Tabs, Tab } from '../../components/ui/tabs';
|
||||
import { EthOverview } from '../../components/app/wallet-overview';
|
||||
import WhatsNewPopup from '../../components/app/whats-new-popup';
|
||||
import RecoveryPhraseReminder from '../../components/app/recovery-phrase-reminder';
|
||||
|
||||
import {
|
||||
ASSET_ROUTE,
|
||||
@ -76,6 +77,10 @@ export default class Home extends PureComponent {
|
||||
showWhatsNewPopup: PropTypes.bool.isRequired,
|
||||
hideWhatsNewPopup: PropTypes.func.isRequired,
|
||||
notificationsToShow: PropTypes.bool.isRequired,
|
||||
showRecoveryPhraseReminder: PropTypes.bool.isRequired,
|
||||
setRecoveryPhraseReminderHasBeenShown: PropTypes.func.isRequired,
|
||||
setRecoveryPhraseReminderLastShown: PropTypes.func.isRequired,
|
||||
seedPhraseBackedUp: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
@ -163,6 +168,15 @@ export default class Home extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
onRecoveryPhraseReminderClose = () => {
|
||||
const {
|
||||
setRecoveryPhraseReminderHasBeenShown,
|
||||
setRecoveryPhraseReminderLastShown,
|
||||
} = this.props;
|
||||
setRecoveryPhraseReminderHasBeenShown(true);
|
||||
setRecoveryPhraseReminderLastShown(new Date().getTime());
|
||||
};
|
||||
|
||||
renderNotifications() {
|
||||
const { t } = this.context;
|
||||
const {
|
||||
@ -325,6 +339,8 @@ export default class Home extends PureComponent {
|
||||
notificationsToShow,
|
||||
showWhatsNewPopup,
|
||||
hideWhatsNewPopup,
|
||||
seedPhraseBackedUp,
|
||||
showRecoveryPhraseReminder,
|
||||
} = this.props;
|
||||
|
||||
if (forgottenPassword) {
|
||||
@ -333,6 +349,8 @@ export default class Home extends PureComponent {
|
||||
return null;
|
||||
}
|
||||
|
||||
const showWhatsNew = notificationsToShow && showWhatsNewPopup;
|
||||
|
||||
return (
|
||||
<div className="main-container">
|
||||
<Route path={CONNECTED_ROUTE} component={ConnectedSites} exact />
|
||||
@ -342,8 +360,12 @@ export default class Home extends PureComponent {
|
||||
exact
|
||||
/>
|
||||
<div className="home__container">
|
||||
{notificationsToShow && showWhatsNewPopup ? (
|
||||
<WhatsNewPopup onClose={hideWhatsNewPopup} />
|
||||
{showWhatsNew ? <WhatsNewPopup onClose={hideWhatsNewPopup} /> : null}
|
||||
{!showWhatsNew && showRecoveryPhraseReminder ? (
|
||||
<RecoveryPhraseReminder
|
||||
hasBackedUp={seedPhraseBackedUp}
|
||||
onConfirm={this.onRecoveryPhraseReminderClose}
|
||||
/>
|
||||
) : null}
|
||||
{isPopup && !connectedStatusPopoverHasBeenShown
|
||||
? this.renderPopover()
|
||||
|
@ -14,6 +14,7 @@ import {
|
||||
getInfuraBlocked,
|
||||
getShowWhatsNewPopup,
|
||||
getSortedNotificationsToShow,
|
||||
getShowRecoveryPhraseReminder,
|
||||
} from '../../selectors';
|
||||
|
||||
import {
|
||||
@ -25,6 +26,8 @@ import {
|
||||
setDefaultHomeActiveTabName,
|
||||
setWeb3ShimUsageAlertDismissed,
|
||||
setAlertEnabledness,
|
||||
setRecoveryPhraseReminderHasBeenShown,
|
||||
setRecoveryPhraseReminderLastShown,
|
||||
} from '../../store/actions';
|
||||
import { setThreeBoxLastUpdated, hideWhatsNewPopup } from '../../ducks/app/app';
|
||||
import { getWeb3ShimUsageAlertEnabledness } from '../../ducks/metamask/metamask';
|
||||
@ -107,6 +110,8 @@ const mapStateToProps = (state) => {
|
||||
infuraBlocked: getInfuraBlocked(state),
|
||||
notificationsToShow: getSortedNotificationsToShow(state).length > 0,
|
||||
showWhatsNewPopup: getShowWhatsNewPopup(state),
|
||||
showRecoveryPhraseReminder: getShowRecoveryPhraseReminder(state),
|
||||
seedPhraseBackedUp,
|
||||
};
|
||||
};
|
||||
|
||||
@ -132,6 +137,10 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
disableWeb3ShimUsageAlert: () =>
|
||||
setAlertEnabledness(ALERT_TYPES.web3ShimUsage, false),
|
||||
hideWhatsNewPopup: () => dispatch(hideWhatsNewPopup()),
|
||||
setRecoveryPhraseReminderHasBeenShown: () =>
|
||||
dispatch(setRecoveryPhraseReminderHasBeenShown()),
|
||||
setRecoveryPhraseReminderLastShown: (lastShown) =>
|
||||
dispatch(setRecoveryPhraseReminderLastShown(lastShown)),
|
||||
});
|
||||
|
||||
export default compose(
|
||||
|
@ -23,6 +23,7 @@ import {
|
||||
import { TEMPLATED_CONFIRMATION_MESSAGE_TYPES } from '../pages/confirmation/templates';
|
||||
|
||||
import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils';
|
||||
import { DAY } from '../../shared/constants/time';
|
||||
import { getNativeCurrency } from './send';
|
||||
|
||||
/**
|
||||
@ -563,3 +564,15 @@ export function getSortedNotificationsToShow(state) {
|
||||
);
|
||||
return notificationsSortedByDate;
|
||||
}
|
||||
|
||||
export function getShowRecoveryPhraseReminder(state) {
|
||||
const {
|
||||
recoveryPhraseReminderLastShown,
|
||||
recoveryPhraseReminderHasBeenShown,
|
||||
} = state.metamask;
|
||||
|
||||
const currentTime = new Date().getTime();
|
||||
const frequency = recoveryPhraseReminderHasBeenShown ? DAY * 90 : DAY * 2;
|
||||
|
||||
return currentTime - recoveryPhraseReminderLastShown >= frequency;
|
||||
}
|
||||
|
@ -2570,6 +2570,26 @@ export function setConnectedStatusPopoverHasBeenShown() {
|
||||
};
|
||||
}
|
||||
|
||||
export function setRecoveryPhraseReminderHasBeenShown() {
|
||||
return () => {
|
||||
background.setRecoveryPhraseReminderHasBeenShown((err) => {
|
||||
if (err) {
|
||||
throw new Error(err.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function setRecoveryPhraseReminderLastShown(lastShown) {
|
||||
return () => {
|
||||
background.setRecoveryPhraseReminderLastShown(lastShown, (err) => {
|
||||
if (err) {
|
||||
throw new Error(err.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export async function setAlertEnabledness(alertId, enabledness) {
|
||||
await promisifiedBackground.setAlertEnabledness(alertId, enabledness);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user