diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 0071b38ae..36e0ceddc 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -22,6 +22,9 @@ "disconnectAccountConfirmationDescription": { "message": "Are you sure you want to disconnect? You may lose site functionality." }, + "dismiss": { + "message": "Dismiss" + }, "migrateSai": { "message": "A message from Maker: The new Multi-Collateral Dai token has been released. Your old tokens are now called Sai. Please upgrade your Sai tokens to the new Dai." }, @@ -867,6 +870,12 @@ "message": { "message": "Message" }, + "metaMaskConnectStatusParagraphOne": { + "message": "This is the new MetaMask Connect status indicator. From here you can easily see and manage sites you’ve connected to with your MetaMask wallet." + }, + "metaMaskConnectStatusParagraphTwo": { + "message": "Click the Connect status to see your connected sites and their permissions." + }, "metamaskDescription": { "message": "Connecting you to Ethereum and the Decentralized Web." }, @@ -1627,6 +1636,9 @@ "welcome": { "message": "Welcome to MetaMask" }, + "whatsThis": { + "message": "What's this?" + }, "writePhrase": { "message": "Write this phrase on a piece of paper and store in a secure location. If you want even more security, write it down on multiple pieces of paper and store each in 2 - 3 different locations." }, diff --git a/app/scripts/controllers/app-state.js b/app/scripts/controllers/app-state.js index 1aac34650..8c68faee8 100644 --- a/app/scripts/controllers/app-state.js +++ b/app/scripts/controllers/app-state.js @@ -22,6 +22,7 @@ class AppStateController extends EventEmitter { this.store = new ObservableStore(Object.assign({ timeoutMinutes: 0, mkrMigrationReminderTimestamp: null, + connectedStatusPopoverHasBeenShown: true, }, initState)) this.timer = null @@ -72,6 +73,15 @@ class AppStateController extends EventEmitter { }) } + /** + * Record that the user has seen the connected status info popover + */ + setConnectedStatusPopoverHasBeenShown () { + this.store.updateState({ + connectedStatusPopoverHasBeenShown: true, + }) + } + /** * Sets the last active time to the current time * @returns {void} diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 91cd0ec44..27ad485a9 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -511,6 +511,7 @@ export default class MetamaskController extends EventEmitter { // AppStateController setLastActiveTime: nodeify(this.appStateController.setLastActiveTime, this.appStateController), setMkrMigrationReminderTimestamp: nodeify(this.appStateController.setMkrMigrationReminderTimestamp, this.appStateController), + setConnectedStatusPopoverHasBeenShown: nodeify(this.appStateController.setConnectedStatusPopoverHasBeenShown, this.appStateController), // EnsController tryReverseResolveAddress: nodeify(this.ensController.reverseResolveAddress, this.ensController), diff --git a/app/scripts/migrations/042.js b/app/scripts/migrations/042.js new file mode 100644 index 000000000..2be7359a7 --- /dev/null +++ b/app/scripts/migrations/042.js @@ -0,0 +1,27 @@ +const version = 42 +import { cloneDeep } from 'lodash' + +/** + * PreferencesController.autoLogoutTimeLimit -> autoLockTimeLimit + */ +export default { + version, + migrate: async function (originalVersionedData) { + const versionedData = cloneDeep(originalVersionedData) + versionedData.meta.version = version + const state = versionedData.data + versionedData.data = transformState(state) + return versionedData + }, +} + +function transformState (state) { + if (state.AppStateController) { + state.AppStateController.connectedStatusPopoverHasBeenShown = false + } else { + state.AppStateController = { + connectedStatusPopoverHasBeenShown: false, + } + } + return state +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index 4a6c56cc7..4e176c9dd 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -52,6 +52,7 @@ const migrations = [ require('./039').default, require('./040').default, require('./041').default, + require('./042').default, ] export default migrations diff --git a/test/unit/migrations/042-test.js b/test/unit/migrations/042-test.js new file mode 100644 index 000000000..14a61d95e --- /dev/null +++ b/test/unit/migrations/042-test.js @@ -0,0 +1,70 @@ +import assert from 'assert' +import migration42 from '../../../app/scripts/migrations/042' + +describe('migration #42', function () { + + it('should update the version metadata', function (done) { + const oldStorage = { + 'meta': { + 'version': 41, + }, + 'data': {}, + } + + migration42.migrate(oldStorage) + .then((newStorage) => { + assert.deepEqual(newStorage.meta, { + 'version': 42, + }) + done() + }) + .catch(done) + }) + + it('should set connectedStatusPopoverHasBeenShown to false', function (done) { + const oldStorage = { + meta: {}, + data: { + AppStateController: { + connectedStatusPopoverHasBeenShown: true, + bar: 'baz', + }, + foo: 'bar', + }, + } + + migration42.migrate(oldStorage) + .then((newStorage) => { + assert.deepEqual(newStorage.data, { + AppStateController: { + connectedStatusPopoverHasBeenShown: false, + bar: 'baz', + }, + foo: 'bar', + }) + done() + }) + .catch(done) + }) + + it('should initialize AppStateController if it does not exist', function (done) { + const oldStorage = { + meta: {}, + data: { + foo: 'bar', + }, + } + + migration42.migrate(oldStorage) + .then((newStorage) => { + assert.deepEqual(newStorage.data, { + foo: 'bar', + AppStateController: { + connectedStatusPopoverHasBeenShown: false, + }, + }) + done() + }) + .catch(done) + }) +}) diff --git a/ui/app/components/ui/popover/index.scss b/ui/app/components/ui/popover/index.scss index 533ba938d..6b880a25c 100644 --- a/ui/app/components/ui/popover/index.scss +++ b/ui/app/components/ui/popover/index.scss @@ -25,6 +25,9 @@ display: flex; padding: 24px; flex-direction: column; + background: white; + position: relative; + border-radius: 10px; &__title { display: flex; @@ -107,4 +110,13 @@ margin: 0 auto; } } + + &-arrow { + width: 22px; + height: 22px; + background: white; + position: absolute; + transform: rotate(45deg); + box-shadow: 0px 4px 30px rgba(0, 0, 0, 0.25); + } } diff --git a/ui/app/components/ui/popover/popover.component.js b/ui/app/components/ui/popover/popover.component.js index a3d8aaf6c..b9d81cc07 100644 --- a/ui/app/components/ui/popover/popover.component.js +++ b/ui/app/components/ui/popover/popover.component.js @@ -4,12 +4,27 @@ import PropTypes from 'prop-types' import classnames from 'classnames' import { I18nContext } from '../../../contexts/i18n' -const Popover = ({ title, subtitle, children, footer, footerClassName, onBack, onClose }) => { +const Popover = ({ + title, + subtitle = '', + children, + footer, + footerClassName, + onBack, + onClose, + className, + showArrow, + CustomBackground, +}) => { const t = useContext(I18nContext) return (
-
-
+ { CustomBackground + ? + :
+ } +
+ { showArrow ?
: null}

@@ -32,7 +47,7 @@ const Popover = ({ title, subtitle, children, footer, footerClassName, onBack, o onClick={onClose} />

-

{subtitle}

+ { subtitle ?

{subtitle}

: null }
{ children @@ -59,12 +74,15 @@ const Popover = ({ title, subtitle, children, footer, footerClassName, onBack, o Popover.propTypes = { title: PropTypes.string.isRequired, - subtitle: PropTypes.string.isRequired, + subtitle: PropTypes.string, children: PropTypes.node, footer: PropTypes.node, footerClassName: PropTypes.string, onBack: PropTypes.func, onClose: PropTypes.func.isRequired, + CustomBackground: PropTypes.func, + className: PropTypes.string, + showArrow: PropTypes.bool, } export default class PopoverPortal extends PureComponent { diff --git a/ui/app/pages/home/home.component.js b/ui/app/pages/home/home.component.js index d986a4bf4..2b75389e1 100644 --- a/ui/app/pages/home/home.component.js +++ b/ui/app/pages/home/home.component.js @@ -11,6 +11,8 @@ import WalletView from '../../components/app/wallet-view' import TransactionList from '../../components/app/transaction-list' import TransactionViewBalance from '../../components/app/transaction-view-balance' import MenuBar from '../../components/app/menu-bar' +import Popover from '../../components/ui/popover' +import Button from '../../components/ui/button' import ConnectedSites from '../connected-sites' import { Tabs, Tab } from '../../components/ui/tabs' @@ -51,6 +53,8 @@ export default class Home extends PureComponent { hasDaiV1Token: PropTypes.bool, firstPermissionsRequestId: PropTypes.string, totalUnapprovedCount: PropTypes.number.isRequired, + setConnectedStatusPopoverHasBeenShown: PropTypes.func, + connectedStatusPopoverHasBeenShown: PropTypes.bool, } UNSAFE_componentWillMount () { @@ -172,11 +176,48 @@ export default class Home extends PureComponent { ) } + renderPopover = () => { + const { setConnectedStatusPopoverHasBeenShown } = this.props + const { t } = this.context + return ( + { + return ( +
+
+
+ ) + }} + footer={( + + )} + > +
+
{ t('metaMaskConnectStatusParagraphOne') }
+
{ t('metaMaskConnectStatusParagraphTwo') }
+
+ + ) + } render () { const { forgottenPassword, history, + connectedStatusPopoverHasBeenShown, + isPopup, } = this.props if (forgottenPassword) { @@ -191,6 +232,7 @@ export default class Home extends PureComponent {
+ { isPopup && !connectedStatusPopoverHasBeenShown ? this.renderPopover() : null } diff --git a/ui/app/pages/home/home.container.js b/ui/app/pages/home/home.container.js index a1de53118..38fb72e36 100644 --- a/ui/app/pages/home/home.container.js +++ b/ui/app/pages/home/home.container.js @@ -16,6 +16,7 @@ import { turnThreeBoxSyncingOn, getThreeBoxLastUpdated, setShowRestorePromptToFalse, + setConnectedStatusPopoverHasBeenShown, } from '../../store/actions' import { setThreeBoxLastUpdated } from '../../ducks/app/app' import { getEnvironmentType } from '../../../../app/scripts/lib/util' @@ -33,6 +34,7 @@ const mapStateToProps = (state) => { threeBoxSynced, showRestorePrompt, selectedAddress, + connectedStatusPopoverHasBeenShown, } = metamask const accountBalance = getCurrentEthBalance(state) const { forgottenPassword, threeBoxLastUpdated } = appState @@ -61,6 +63,7 @@ const mapStateToProps = (state) => { hasDaiV1Token: Boolean(getDaiV1Token(state)), firstPermissionsRequestId, totalUnapprovedCount, + connectedStatusPopoverHasBeenShown, } } @@ -79,6 +82,7 @@ const mapDispatchToProps = (dispatch) => ({ }, restoreFromThreeBox: (address) => dispatch(restoreFromThreeBox(address)), setShowRestorePromptToFalse: () => dispatch(setShowRestorePromptToFalse()), + setConnectedStatusPopoverHasBeenShown: () => dispatch(setConnectedStatusPopoverHasBeenShown()), }) export default compose( diff --git a/ui/app/pages/home/index.scss b/ui/app/pages/home/index.scss index f11e4a861..5ed13a155 100644 --- a/ui/app/pages/home/index.scss +++ b/ui/app/pages/home/index.scss @@ -36,4 +36,69 @@ &__tab { flex-grow: 1; } + + &__connect-status-text { + display: flex; + flex-direction: column; + @extend %content-text; + padding-left: 24px; + padding-right: 24px; + + div { + &:last-child { + margin-top: 26px; + } + } + } + + &__connected-status-popover { + width: 329px; + height: 295px; + margin-top: -12px; + + .popover-header { + padding-bottom: 4px; + } + + .popover-content { + overflow-y: auto; + } + + .popover-arrow { + top: -6px; + left: 24px; + } + + .popover-footer { + justify-content: flex-end; + + & :only-child { + margin: 0; + } + + button { + height: 39px; + width: 133px; + border-radius: 39px; + padding: 0; + } + } + } + + &__connected-status-popover-bg { + height: 34px; + width: 110px; + border-radius: 34px; + position: absolute; + top: 82px; + left: 12px; + opacity: 1; + box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.2); + background: none; + } + + &__connected-status-popover-bg-container { + height: 100%; + width: 100%; + } } diff --git a/ui/app/store/actions.js b/ui/app/store/actions.js index 0265f7195..5fd76c7f3 100644 --- a/ui/app/store/actions.js +++ b/ui/app/store/actions.js @@ -2208,6 +2208,16 @@ export function setMkrMigrationReminderTimestamp (timestamp) { } } +export function setConnectedStatusPopoverHasBeenShown () { + return () => { + background.setConnectedStatusPopoverHasBeenShown((err) => { + if (err) { + throw new Error(err.message) + } + }) + } +} + export function loadingMethoDataStarted () { return { type: actionConstants.LOADING_METHOD_DATA_STARTED,