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

Adding periodic reminder modal for backing up recovery phrase #11021

Merged
merged 2 commits into from Jun 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
24 changes: 24 additions & 0 deletions app/_locales/en/messages.json
Expand Up @@ -1425,6 +1425,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"
},
Expand Down
23 changes: 23 additions & 0 deletions app/scripts/controllers/app-state.js
Expand Up @@ -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;
Expand Down Expand Up @@ -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}
Expand Down
8 changes: 8 additions & 0 deletions app/scripts/metamask-controller.js
Expand Up @@ -779,6 +779,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(
Expand Down
32 changes: 32 additions & 0 deletions app/scripts/migrations/061.js
@@ -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 changes: 67 additions & 0 deletions app/scripts/migrations/061.test.js
@@ -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,
},
});
});
});
1 change: 1 addition & 0 deletions app/scripts/migrations/index.js
Expand Up @@ -65,6 +65,7 @@ const migrations = [
require('./058').default,
require('./059').default,
require('./060').default,
require('./061').default,
];

export default migrations;
5 changes: 5 additions & 0 deletions shared/constants/time.js
@@ -0,0 +1,5 @@
export const MILLISECOND = 1;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After merge I'll put up a change to use these across the codebase where applicable

export const SECOND = MILLISECOND * 1000;
export const MINUTE = SECOND * 60;
export const HOUR = MINUTE * 60;
export const DAY = HOUR * 24;
1 change: 1 addition & 0 deletions ui/components/app/app-components.scss
Expand Up @@ -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';
Expand Down
1 change: 1 addition & 0 deletions ui/components/app/recovery-phrase-reminder/index.js
@@ -0,0 +1 @@
export { default } from './recovery-phrase-reminder';
10 changes: 10 additions & 0 deletions ui/components/app/recovery-phrase-reminder/index.scss
@@ -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,
};
4 changes: 4 additions & 0 deletions ui/components/ui/popover/index.scss
Expand Up @@ -47,6 +47,10 @@
margin-right: 24px;
}
}

&.center {
justify-content: center;
}
}

&__subtitle {
Expand Down
25 changes: 17 additions & 8 deletions ui/components/ui/popover/popover.component.js
Expand Up @@ -17,6 +17,7 @@ const Popover = ({
showArrow,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per the designs, there is no 'X' button (closure happens upon clicking "Got it!" at the bottom of the popup), and the title is centered.

CustomBackground,
popoverRef,
centerTitle,
}) => {
const t = useI18nContext();
return (
Expand All @@ -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
Expand All @@ -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>
Expand Down Expand Up @@ -76,14 +84,15 @@ 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,
showArrow: PropTypes.bool,
popoverRef: PropTypes.shape({
current: PropTypes.instanceOf(window.Element),
}),
centerTitle: PropTypes.bool,
};

export default class PopoverPortal extends PureComponent {
Expand Down