1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

Add support for one-click onboarding (#7017)

* Add support for one-click onboarding

MetaMask now allows sites to register as onboarding the user, so that
the user is redirected back to the initiating site after onboarding.
This is accomplished through the use of the `metamask-onboarding`
library and the MetaMask forwarder.

At the end of onboarding, a 'snackbar'-stype component will explain to the
user they are about to be moved back to the originating dapp, and it will
show the origin of that dapp. This is intended to help prevent phishing
attempts, as it highlights that a redirect is taking place to an untrusted
third party.

If the onboarding initiator tab is closed when onboarding is finished,
the user is redirected to the onboarding originator as a fallback.

Closes #6161

* Add onboarding button to contract test dapp

The `contract-test` dapp (run with `yarn dapp`, used in e2e tests) now
uses a `Connect` button instead of connecting automatically. This
button also serves as an onboarding button when a MetaMask installation
is not detected.

* Add new static server for test dapp

The `static-server` library we were using for the `contract-test` dapp
didn't allow referencing files outside the server root. This should
have been possible to work around using symlinks, but there was a bug
that resulted in symlinks crashing the server.

Instead it has been replaced with a simple static file server that
will serve paths starting with `node_modules` from the project root.
This will be useful in testing the onboarding library without vendoring
it.

* Add `@metamask/onboarding` and `@metamask/forwarder`

Both libraries used to test onboarding are now included as dev
dependencies, to help with testing. A few convenience scripts
were added to help with this (`yarn forwarder` and `yarn dapp-forwarder`)
This commit is contained in:
Mark Stacey 2019-11-22 13:03:51 -04:00 committed by GitHub
parent 015ba83c6e
commit f763979bed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 862 additions and 355 deletions

View File

@ -507,6 +507,10 @@
"endOfFlowMessage10": {
"message": "All Done"
},
"onboardingReturnNotice": {
"message": "\"$1\" will close this tab and direct back to $2",
"description": "Return the user to the site that initiated onboarding"
},
"ensRegistrationError": {
"message": "Error in ENS name registration"
},

View File

@ -314,6 +314,7 @@ function setupController (initState, initLangCode) {
//
extension.runtime.onConnect.addListener(connectRemote)
extension.runtime.onConnectExternal.addListener(connectExternal)
extension.runtime.onMessage.addListener(controller.onMessage.bind(controller))
const metamaskInternalProcessHash = {
[ENVIRONMENT_TYPE_POPUP]: true,

View File

@ -4,6 +4,7 @@ const pump = require('pump')
const log = require('loglevel')
const Dnode = require('dnode')
const querystring = require('querystring')
const { Writable } = require('readable-stream')
const LocalMessageDuplexStream = require('post-message-stream')
const ObjectMultiplex = require('obj-multiplex')
const extension = require('extensionizer')
@ -86,6 +87,44 @@ async function setupStreams () {
(err) => logStreamDisconnectWarning('MetaMask Background Multiplex', err)
)
const onboardingStream = pageMux.createStream('onboarding')
const addCurrentTab = new Writable({
objectMode: true,
write: (chunk, _, callback) => {
if (!chunk) {
return callback(new Error('Malformed onboarding message'))
}
const handleSendMessageResponse = (error, success) => {
if (!error && !success) {
error = extension.runtime.lastError
}
if (error) {
log.error(`Failed to send ${chunk.type} message`, error)
return callback(error)
}
callback(null)
}
try {
if (chunk.type === 'registerOnboarding') {
extension.runtime.sendMessage({ type: 'metamask:registerOnboarding', location: window.location.href }, handleSendMessageResponse)
} else {
throw new Error(`Unrecognized onboarding message type: '${chunk.type}'`)
}
} catch (error) {
log.error(error)
return callback(error)
}
},
})
pump(
onboardingStream,
addCurrentTab,
error => console.error('MetaMask onboarding channel traffic failed', error),
)
// forward communication across inpage-background for these channels only
forwardTrafficBetweenMuxers('provider', pageMux, extensionMux)
forwardTrafficBetweenMuxers('publicConfig', pageMux, extensionMux)

View File

@ -1,5 +1,6 @@
const ObservableStore = require('obs-store')
const extend = require('xtend')
const log = require('loglevel')
/**
* @typedef {Object} InitState
@ -9,11 +10,12 @@ const extend = require('xtend')
/**
* @typedef {Object} OnboardingOptions
* @property {InitState} initState The initial controller state
* @property {PreferencesController} preferencesController Controller for managing user perferences
*/
/**
* Controller responsible for maintaining
* a cache of account balances in local storage
* state related to onboarding
*/
class OnboardingController {
/**
@ -22,10 +24,28 @@ class OnboardingController {
* @param {OnboardingOptions} [opts] Controller configuration parameters
*/
constructor (opts = {}) {
const initState = extend({
seedPhraseBackedUp: true,
}, opts.initState)
const initialTransientState = {
onboardingTabs: {},
}
const initState = extend(
{
seedPhraseBackedUp: true,
},
opts.initState,
initialTransientState,
)
this.store = new ObservableStore(initState)
this.preferencesController = opts.preferencesController
this.completedOnboarding = this.preferencesController.store.getState().completedOnboarding
this.preferencesController.store.subscribe(({ completedOnboarding }) => {
if (completedOnboarding !== this.completedOnboarding) {
this.completedOnboarding = completedOnboarding
if (completedOnboarding) {
this.store.updateState(initialTransientState)
}
}
})
}
setSeedPhraseBackedUp (newSeedPhraseBackUpState) {
@ -38,6 +58,24 @@ class OnboardingController {
return this.store.getState().seedPhraseBackedUp
}
/**
* Registering a site as having initiated onboarding
*
* @param {string} location - The location of the site registering
* @param {string} tabId - The id of the tab registering
*/
async registerOnboarding (location, tabId) {
if (this.completedOnboarding) {
log.debug('Ignoring registerOnboarding; user already onboarded')
return
}
const onboardingTabs = Object.assign({}, this.store.getState().onboardingTabs)
if (!onboardingTabs[location] || onboardingTabs[location] !== tabId) {
log.debug(`Registering onboarding tab at location '${location}' with tabId '${tabId}'`)
onboardingTabs[location] = tabId
this.store.updateState({ onboardingTabs })
}
}
}
module.exports = OnboardingController

View File

@ -37,6 +37,9 @@ const log = require('loglevel')
const LocalMessageDuplexStream = require('post-message-stream')
const setupDappAutoReload = require('./lib/auto-reload.js')
const MetamaskInpageProvider = require('metamask-inpage-provider')
const ObjectMultiplex = require('obj-multiplex')
const pump = require('pump')
const promisify = require('pify')
const createStandardProvider = require('./createStandardProvider').default
let warned = false
@ -61,6 +64,14 @@ const inpageProvider = new MetamaskInpageProvider(metamaskStream)
// set a high max listener count to avoid unnecesary warnings
inpageProvider.setMaxListeners(100)
const pageMux = new ObjectMultiplex()
const onboardingStream = pageMux.createStream('onboarding')
pump(
pageMux,
metamaskStream,
error => log.error('MetaMask muxed in-page traffic failed', error)
)
let warnedOfAutoRefreshDeprecation = false
// augment the provider with its enable method
inpageProvider.enable = function ({ force } = {}) {
@ -134,6 +145,15 @@ inpageProvider._metamask = new Proxy({
const { isUnlocked } = await getPublicConfigWhenReady()
return Boolean(isUnlocked)
},
/**
* Registers a page as having initated onboarding. This facilitates MetaMask focusing the initiating tab after onboarding.
*
* @returns {Promise} - Promise resolving to undefined
*/
registerOnboarding: async () => {
await promisify(onboardingStream.write({ type: 'registerOnboarding' }))
},
}, {
get: function (obj, prop) {
!warned && console.warn('Heads up! ethereum._metamask exposes methods that have ' +
@ -178,9 +198,3 @@ setupDappAutoReload(web3, inpageProvider.publicConfigStore)
inpageProvider.publicConfigStore.subscribe(function (state) {
web3.eth.defaultAccount = state.selectedAddress
})
inpageProvider.publicConfigStore.subscribe(function (state) {
if (state.onboardingcomplete) {
window.postMessage('onboardingcomplete', '*')
}
})

View File

@ -4,10 +4,12 @@
* @license MIT
*/
const assert = require('assert').strict
const EventEmitter = require('events')
const pump = require('pump')
const Dnode = require('dnode')
const pify = require('pify')
const extension = require('extensionizer')
const ObservableStore = require('obs-store')
const ComposableObservableStore = require('./lib/ComposableObservableStore')
const createDnodeRemoteGetter = require('./lib/createDnodeRemoteGetter')
@ -177,6 +179,7 @@ module.exports = class MetamaskController extends EventEmitter {
this.onboardingController = new OnboardingController({
initState: initState.OnboardingController,
preferencesController: this.preferencesController,
})
// ensure accountTracker updates balances after network change
@ -390,7 +393,7 @@ module.exports = class MetamaskController extends EventEmitter {
publicConfigStore.putState(publicState)
}
function selectPublicState ({ isUnlocked, selectedAddress, network, completedOnboarding, provider }) {
function selectPublicState ({ isUnlocked, selectedAddress, network, provider }) {
const isEnabled = checkIsEnabled()
const isReady = isUnlocked && isEnabled
const result = {
@ -398,7 +401,6 @@ module.exports = class MetamaskController extends EventEmitter {
isEnabled,
selectedAddress: isReady ? selectedAddress : null,
networkVersion: network,
onboardingcomplete: completedOnboarding,
chainId: selectChainId({ network, provider }),
}
return result
@ -1521,6 +1523,43 @@ module.exports = class MetamaskController extends EventEmitter {
)
}
onMessage (message, sender, sendResponse) {
if (!message || !message.type) {
log.debug(`Ignoring invalid message: '${JSON.stringify(message)}'`)
return
}
let handleMessage
try {
if (message.type === 'metamask:registerOnboarding') {
assert(sender.tab, 'Missing tab from sender')
assert(sender.tab.id && sender.tab.id !== extension.tabs.TAB_ID_NONE, 'Missing tab ID from sender')
assert(message.location, 'Missing location from message')
handleMessage = this.onboardingController.registerOnboarding(message.location, sender.tab.id)
} else {
throw new Error(`Unrecognized message type: '${message.type}'`)
}
} catch (error) {
console.error(error)
sendResponse(error)
return true
}
if (handleMessage) {
handleMessage
.then(() => {
sendResponse(null, true)
})
.catch((error) => {
console.error(error)
sendResponse(error)
})
return true
}
}
/**
* A method for providing our public api over a stream.
* This includes a method for setting site metadata like title and image

View File

@ -3,7 +3,7 @@ const version = 31
const clone = require('clone')
/*
* The purpose of this migration is to properly set the completedOnboarding flag baesd on the state
* The purpose of this migration is to properly set the completedOnboarding flag based on the state
* of the KeyringController.
*/
module.exports = {

View File

@ -0,0 +1,92 @@
const fs = require('fs')
const http = require('http')
const path = require('path')
const chalk = require('chalk')
const pify = require('pify')
const serveHandler = require('serve-handler')
const fsStat = pify(fs.stat)
const DEFAULT_PORT = 9080
const onResponse = (request, response) => {
if (response.statusCode >= 400) {
console.log(chalk`{gray '-->'} {red ${response.statusCode}} ${request.url}`)
} else if (response.statusCode >= 200 && response.statusCode < 300) {
console.log(chalk`{gray '-->'} {green ${response.statusCode}} ${request.url}`)
} else {
console.log(chalk`{gray '-->'} {green.dim ${response.statusCode}} ${request.url}`)
}
}
const onRequest = (request, response) => {
console.log(chalk`{gray '<--'} {blue [${request.method}]} ${request.url}`)
response.on('finish', () => onResponse(request, response))
}
const startServer = ({ port, rootDirectory }) => {
const server = http.createServer((request, response) => {
if (request.url.startsWith('/node_modules/')) {
request.url = request.url.substr(14)
return serveHandler(request, response, {
directoryListing: false,
public: path.resolve('./node_modules'),
})
}
return serveHandler(request, response, {
directoryListing: false,
public: rootDirectory,
})
})
server.on('request', onRequest)
server.listen(port, () => {
console.log(`Running at http://localhost:${port}`)
})
}
const parsePort = (portString) => {
const port = Number(portString)
if (!Number.isInteger(port)) {
throw new Error(`Port '${portString}' is invalid; must be an integer`)
} else if (port < 0 || port > 65535) {
throw new Error(`Port '${portString}' is out of range; must be between 0 and 65535 inclusive`)
}
return port
}
const parseDirectoryArgument = async (pathString) => {
const resolvedPath = path.resolve(pathString)
const directoryStats = await fsStat(resolvedPath)
if (!directoryStats.isDirectory()) {
throw new Error(`Invalid path '${pathString}'; must be a directory`)
}
return resolvedPath
}
const main = async () => {
const args = process.argv.slice(2)
const options = {
port: process.env.port || DEFAULT_PORT,
rootDirectory: path.resolve('.'),
}
while (args.length) {
if (/^(--port|-p)$/i.test(args[0])) {
if (args[1] === undefined) {
throw new Error('Missing port argument')
}
options.port = parsePort(args[1])
args.splice(0, 2)
} else {
options.rootDirectory = await parseDirectoryArgument(args[0])
args.splice(0, 1)
}
}
startServer(options)
}
main()
.catch(console.error)

View File

@ -10,10 +10,12 @@
"start:test": "gulp dev:test",
"build:test": "gulp build:test",
"test": "yarn test:unit && yarn lint",
"dapp": "static-server test/e2e/contract-test --port 8080",
"dapp-chain": "GANACHE_ARGS='-b 2' concurrently -k -n ganache,dapp -p '[{time}][{name}]' 'yarn ganache:start' 'sleep 5 && static-server test/e2e/contract-test --port 8080'",
"dapp": "node development/static-server.js test/e2e/contract-test --port 8080",
"dapp-chain": "GANACHE_ARGS='-b 2' concurrently -k -n ganache,dapp -p '[{time}][{name}]' 'yarn ganache:start' 'sleep 5 && node development/static-server.js test/e2e/contract-test --port 8080'",
"forwarder": "node ./development/static-server.js ./node_modules/@metamask/forwarder/dist/ --port 9010",
"dapp-forwarder": "concurrently -k -n forwarder,dapp -p '[{time}][{name}]' 'yarn forwarder' 'yarn dapp'",
"watch:test:unit": "nodemon --exec \"yarn test:unit\" ./test ./app ./ui",
"sendwithprivatedapp": "static-server test/e2e/send-eth-with-private-key-test --port 8080",
"sendwithprivatedapp": "node development/static-server.js test/e2e/send-eth-with-private-key-test --port 8080",
"test:unit": "cross-env METAMASK_ENV=test mocha --exit --require test/setup.js --recursive \"test/unit/**/*.js\" \"ui/app/**/*.test.js\"",
"test:unit:global": "mocha test/unit-global/*",
"test:single": "cross-env METAMASK_ENV=test mocha --require test/helper.js",
@ -189,6 +191,8 @@
"@babel/preset-env": "^7.5.5",
"@babel/preset-react": "^7.0.0",
"@babel/register": "^7.5.5",
"@metamask/forwarder": "^1.0.0",
"@metamask/onboarding": "^0.1.2",
"@sentry/cli": "^1.30.3",
"@storybook/addon-actions": "^5.2.6",
"@storybook/addon-info": "^5.1.1",
@ -201,6 +205,7 @@
"browserify": "^16.2.3",
"browserify-transform-tools": "^1.7.0",
"chai": "^4.1.0",
"chalk": "^2.4.2",
"chromedriver": "^2.41.0",
"concurrently": "^4.1.1",
"coveralls": "^3.0.0",
@ -279,12 +284,12 @@
"rimraf": "^2.6.2",
"sass-loader": "^7.0.1",
"selenium-webdriver": "^3.5.0",
"serve-handler": "^6.1.2",
"sesify": "^4.2.1",
"sesify-viz": "^3.0.5",
"sinon": "^5.0.0",
"source-map": "^0.7.2",
"source-map-explorer": "^2.0.1",
"static-server": "^2.2.1",
"style-loader": "^0.21.0",
"stylelint": "^9.10.1",
"stylelint-config-standard": "^18.2.0",

File diff suppressed because one or more lines are too long

View File

@ -1,51 +1,64 @@
<html>
<head>
<meta charset="UTF-8">
<title>E2E Test Dapp</title>
<script src="node_modules/@metamask/onboarding/dist/metamask-onboarding.bundle.js"></script>
<script src="contract.js" defer></script>
</head>
<body>
<div style="display: flex; flex-flow: column;">
<div style="display: flex; font-size: 1.25rem;">Contract</div>
<div style="display: flex;">
<button id="deployButton">Deploy Contract</button>
<button id="depositButton">Deposit</button>
<button id="withdrawButton">Withdraw</button>
</div>
<div id="contractStatus" style="display: flex; font-size: 1rem;">
Not clicked
</div>
</div>
<div style="display: flex; flex-flow: column;">
<div style="display: flex; font-size: 1.25rem;">Send eth</div>
<div style="display: flex;">
<header>
<h1>E2E Test Dapp</h1>
</header>
<main>
<section>
<h2>Connect</h2>
<button id="connectButton">Connect</button>
</section>
<section>
<h2>Contract</h2>
<div>
<button id="deployButton">Deploy Contract</button>
<button id="depositButton">Deposit</button>
<button id="withdrawButton">Withdraw</button>
</div>
<div>
Contract Status: <span id="contractStatus">Not clicked</span>
</div>
</section>
<section>
<h2>Send Eth</h2>
<button id="sendButton">Send</button>
</div>
</div>
<div style="display: flex; flex-flow: column;">
<div style="display: flex; font-size: 1.25rem;">Send tokens</div>
<div id="tokenAddress"></div>
<div style="display: flex;">
<button id="createToken">Create Token</button>
<button id="transferTokens">Transfer Tokens</button>
<button id="approveTokens">Approve Tokens</button>
<button id="transferTokensWithoutGas">Transfer Tokens Without Gas</button>
<button id="approveTokensWithoutGas">Approve Tokens Without Gas</button>
</div>
</div>
<div style="display: flex; flex-flow: column;">
<div>Network: <div id="network"></div></div>
<div>ChainId: <div id="chainId"></div></div>
<div>Accounts: <div id="accounts"></div></div>
<div style="display: flex;">
</div>
<div style="display: flex; flex-flow: column;">
<div style="display: flex; font-size: 1.25rem;">Sign Typed Data</div>
<div style="display: flex;">
</section>
<section>
<h2>Send Tokens</h2>
<div>
Token: <span id="tokenAddress"></span>
</div>
<div>
<button id="createToken">Create Token</button>
<button id="transferTokens">Transfer Tokens</button>
<button id="approveTokens">Approve Tokens</button>
<button id="transferTokensWithoutGas">Transfer Tokens Without Gas</button>
<button id="approveTokensWithoutGas">Approve Tokens Without Gas</button>
</div>
</section>
<section>
<h2>Status</h2>
<div>
Network: <span id="network"></span>
</div>
<div>
ChainId: <span id="chainId"></span>
</div>
<div>
Accounts: <span id="accounts"></span>
</div>
</section>
<section>
<h2>Sign Typed Data</h2>
<button id="signTypedData">Sign</button>
<div>Sign Typed Data Result: <div id="signTypedDataResult"></div></div>
</div>
</div>
<script src="contract.js"></script>
<div>Sign Typed Data Result: <span id="signTypedDataResult"></span></div>
</section>
</main>
</body>
</html>
</html>

View File

@ -117,6 +117,11 @@ describe('MetaMask', function () {
await openNewPage(driver, 'http://127.0.0.1:8080/')
await delay(regularDelayMs)
const connectButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Connect')]`))
await connectButton.click()
await delay(regularDelayMs)
await waitUntilXWindowHandles(driver, 3)
const windowHandles = await driver.getAllWindowHandles()
@ -132,9 +137,9 @@ describe('MetaMask', function () {
await delay(regularDelayMs)
})
it('has not set the network within the dapp', async () => {
it('has the ganache network id within the dapp', async () => {
const networkDiv = await findElement(driver, By.css('#network'))
assert.equal(await networkDiv.getText(), '')
assert.equal(await networkDiv.getText(), '5777')
})
it('changes the network', async () => {

View File

@ -436,6 +436,11 @@ describe('MetaMask', function () {
await openNewPage(driver, 'http://127.0.0.1:8080/')
await delay(regularDelayMs)
const connectButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Connect')]`))
await connectButton.click()
await delay(regularDelayMs)
await waitUntilXWindowHandles(driver, 3)
windowHandles = await driver.getAllWindowHandles()

View File

@ -9,5 +9,5 @@ export PATH="$PATH:./node_modules/.bin"
concurrently --kill-others \
--names 'dapp,e2e' \
--prefix '[{time}][{name}]' \
'static-server test/web3 --port 8080' \
'node development/static-server.js test/web3 --port 8080' \
'sleep 5 && mocha test/e2e/web3.spec'

View File

@ -128,6 +128,11 @@ describe('MetaMask', function () {
await openNewPage(driver, 'http://127.0.0.1:8080/')
await delay(regularDelayMs)
const connectButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Connect')]`))
await connectButton.click()
await delay(regularDelayMs)
await waitUntilXWindowHandles(driver, 3)
windowHandles = await driver.getAllWindowHandles()

View File

@ -126,6 +126,11 @@ describe('Using MetaMask with an existing account', function () {
await openNewPage(driver, 'http://127.0.0.1:8080/')
await delay(regularDelayMs)
const connectButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Connect')]`))
await connectButton.click()
await delay(regularDelayMs)
await waitUntilXWindowHandles(driver, 3)
const windowHandles = await driver.getAllWindowHandles()

View File

@ -0,0 +1 @@
export { default } from './snackbar.component'

View File

@ -0,0 +1,11 @@
.snackbar {
padding: .75rem 1rem;
font-size: 0.75rem;
color: $Blue-600;
min-width: 360px;
width: fit-content;
background: $Blue-000;
border: 1px solid $Blue-200;
border-radius: 6px;
}

View File

@ -0,0 +1,18 @@
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
const Snackbar = ({ className = '', content }) => {
return (
<div className={classnames('snackbar', className)}>
{ content }
</div>
)
}
Snackbar.propTypes = {
className: PropTypes.string,
content: PropTypes.string.isRequired,
}
module.exports = Snackbar

View File

@ -1,5 +1,6 @@
@import '../../../components/ui/button/buttons';
@import '../../../components/ui/dialog/dialog';
@import '../../../components/ui/snackbar/index';
@import './footer.scss';

View File

@ -1,8 +1,10 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import Button from '../../../components/ui/button'
import Snackbar from '../../../components/ui/snackbar'
import MetaFoxLogo from '../../../components/ui/metafox-logo'
import { DEFAULT_ROUTE } from '../../../helpers/constants/routes'
import { returnToOnboardingInitiator } from '../onboarding-initiator-util'
export default class EndOfFlowScreen extends PureComponent {
static contextTypes = {
@ -14,11 +16,33 @@ export default class EndOfFlowScreen extends PureComponent {
history: PropTypes.object,
completeOnboarding: PropTypes.func,
completionMetaMetricsName: PropTypes.string,
onboardingInitiator: PropTypes.exact({
location: PropTypes.string,
tabId: PropTypes.number,
}),
}
onComplete = async () => {
const { history, completeOnboarding, completionMetaMetricsName, onboardingInitiator } = this.props
await completeOnboarding()
this.context.metricsEvent({
eventOpts: {
category: 'Onboarding',
action: 'Onboarding Complete',
name: completionMetaMetricsName,
},
})
if (onboardingInitiator) {
await returnToOnboardingInitiator(onboardingInitiator)
}
history.push(DEFAULT_ROUTE)
}
render () {
const { t } = this.context
const { history, completeOnboarding, completionMetaMetricsName } = this.props
const { onboardingInitiator } = this.props
return (
<div className="end-of-flow">
@ -62,20 +86,17 @@ export default class EndOfFlowScreen extends PureComponent {
<Button
type="primary"
className="first-time-flow__button"
onClick={async () => {
await completeOnboarding()
this.context.metricsEvent({
eventOpts: {
category: 'Onboarding',
action: 'Onboarding Complete',
name: completionMetaMetricsName,
},
})
history.push(DEFAULT_ROUTE)
}}
onClick={this.onComplete}
>
{ t('endOfFlowMessage10') }
</Button>
{
onboardingInitiator ?
<Snackbar
content={t('onboardingReturnNotice', [t('endOfFlowMessage10'), onboardingInitiator.location])}
/> :
null
}
</div>
)
}

View File

@ -1,21 +1,22 @@
import { connect } from 'react-redux'
import EndOfFlow from './end-of-flow.component'
import { setCompletedOnboarding } from '../../../store/actions'
import { getOnboardingInitiator } from '../first-time-flow.selectors'
const firstTimeFlowTypeNameMap = {
create: 'New Wallet Created',
'import': 'New Wallet Imported',
}
const mapStateToProps = ({ metamask }) => {
const { firstTimeFlowType } = metamask
const mapStateToProps = (state) => {
const { metamask: { firstTimeFlowType } } = state
return {
completionMetaMetricsName: firstTimeFlowTypeNameMap[firstTimeFlowType],
onboardingInitiator: getOnboardingInitiator(state),
}
}
const mapDispatchToProps = dispatch => {
return {
completeOnboarding: () => dispatch(setCompletedOnboarding()),

View File

@ -50,4 +50,4 @@
font-size: 80px;
margin-top: 70px;
}
}
}

View File

@ -4,12 +4,6 @@ import {
DEFAULT_ROUTE,
} from '../../helpers/constants/routes'
const selectors = {
getFirstTimeFlowTypeRoute,
}
module.exports = selectors
function getFirstTimeFlowTypeRoute (state) {
const { firstTimeFlowType } = state.metamask
@ -24,3 +18,25 @@ function getFirstTimeFlowTypeRoute (state) {
return nextRoute
}
const getOnboardingInitiator = (state) => {
const { onboardingTabs } = state.metamask
if (!onboardingTabs || Object.keys(onboardingTabs).length !== 1) {
return null
}
const location = Object.keys(onboardingTabs)[0]
const tabId = onboardingTabs[location]
return {
location,
tabId,
}
}
const selectors = {
getFirstTimeFlowTypeRoute,
getOnboardingInitiator,
}
module.exports = selectors

View File

@ -0,0 +1,48 @@
import extension from 'extensionizer'
import log from 'loglevel'
const returnToOnboardingInitiatorTab = async (onboardingInitiator) => {
const tab = await (new Promise((resolve) => {
extension.tabs.update(onboardingInitiator.tabId, { active: true }, (tab) => {
if (tab) {
resolve(tab)
} else {
// silence console message about unchecked error
if (extension.runtime.lastError) {
log.debug(extension.runtime.lastError)
}
resolve()
}
})
}))
if (!tab) {
// this case can happen if the tab was closed since being checked with `extension.tabs.get`
log.warn(`Setting current tab to onboarding initator has failed; falling back to redirect`)
window.location.assign(onboardingInitiator.location)
} else {
window.close()
}
}
export const returnToOnboardingInitiator = async (onboardingInitiator) => {
const tab = await (new Promise((resolve) => {
extension.tabs.get(onboardingInitiator.tabId, (tab) => {
if (tab) {
resolve(tab)
} else {
// silence console message about unchecked error
if (extension.runtime.lastError) {
log.debug(extension.runtime.lastError)
}
resolve()
}
})
}))
if (tab) {
await returnToOnboardingInitiatorTab(onboardingInitiator)
} else {
window.location.assign(onboardingInitiator.location)
}
}

View File

@ -60,7 +60,7 @@
}
button {
margin-top: 0xp;
margin-top: 0px;
}
&__buttons {

View File

@ -3,8 +3,10 @@ import PropTypes from 'prop-types'
import classnames from 'classnames'
import LockIcon from '../../../../components/ui/lock-icon'
import Button from '../../../../components/ui/button'
import Snackbar from '../../../../components/ui/snackbar'
import { INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE, DEFAULT_ROUTE } from '../../../../helpers/constants/routes'
import { exportAsFile } from '../../../../helpers/utils/util'
import { returnToOnboardingInitiator } from '../../onboarding-initiator-util'
export default class RevealSeedPhrase extends PureComponent {
static contextTypes = {
@ -17,6 +19,10 @@ export default class RevealSeedPhrase extends PureComponent {
seedPhrase: PropTypes.string,
setSeedPhraseBackedUp: PropTypes.func,
setCompletedOnboarding: PropTypes.func,
onboardingInitiator: PropTypes.exact({
location: PropTypes.string,
tabId: PropTypes.number,
}),
}
state = {
@ -27,8 +33,7 @@ export default class RevealSeedPhrase extends PureComponent {
exportAsFile('MetaMask Secret Backup Phrase', this.props.seedPhrase, 'text/plain')
}
handleNext = event => {
event.preventDefault()
handleNext = () => {
const { isShowingSeedPhrase } = this.state
const { history } = this.props
@ -47,9 +52,8 @@ export default class RevealSeedPhrase extends PureComponent {
history.push(INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE)
}
handleSkip = event => {
event.preventDefault()
const { history, setSeedPhraseBackedUp, setCompletedOnboarding } = this.props
handleSkip = async () => {
const { history, setSeedPhraseBackedUp, setCompletedOnboarding, onboardingInitiator } = this.props
this.context.metricsEvent({
eventOpts: {
@ -59,10 +63,12 @@ export default class RevealSeedPhrase extends PureComponent {
},
})
Promise.all([setCompletedOnboarding(), setSeedPhraseBackedUp(false)])
.then(() => {
history.push(DEFAULT_ROUTE)
})
await Promise.all([setCompletedOnboarding(), setSeedPhraseBackedUp(false)])
if (onboardingInitiator) {
await returnToOnboardingInitiator(onboardingInitiator)
}
history.push(DEFAULT_ROUTE)
}
renderSecretWordsContainer () {
@ -111,6 +117,7 @@ export default class RevealSeedPhrase extends PureComponent {
render () {
const { t } = this.context
const { isShowingSeedPhrase } = this.state
const { onboardingInitiator } = this.props
return (
<div className="reveal-seed-phrase">
@ -166,6 +173,13 @@ export default class RevealSeedPhrase extends PureComponent {
{ t('next') }
</Button>
</div>
{
onboardingInitiator ?
<Snackbar
content={t('onboardingReturnNotice', [t('remindMeLater'), onboardingInitiator.location])}
/> :
null
}
</div>
)
}

View File

@ -4,6 +4,13 @@ import {
setCompletedOnboarding,
setSeedPhraseBackedUp,
} from '../../../../store/actions'
import { getOnboardingInitiator } from '../../first-time-flow.selectors'
const mapStateToProps = (state) => {
return {
onboardingInitiator: getOnboardingInitiator(state),
}
}
const mapDispatchToProps = dispatch => {
return {
@ -12,4 +19,4 @@ const mapDispatchToProps = dispatch => {
}
}
export default connect(null, mapDispatchToProps)(RevealSeedPhrase)
export default connect(mapStateToProps, mapDispatchToProps)(RevealSeedPhrase)

143
yarn.lock
View File

@ -1985,6 +1985,18 @@
scroll "^2.0.3"
warning "^3.0.0"
"@metamask/forwarder@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@metamask/forwarder/-/forwarder-1.0.0.tgz#3e321022a36561cc6e7b7c84df25f600925f4d95"
integrity sha512-ufgPndhZz0oNhRrixiR6cXH/HwtFwurWvbrU8zAZsFnf1hB4L2VB2Wey/P1wStIx+BJJQjyROvCDyPDoz4ny1A==
"@metamask/onboarding@^0.1.2":
version "0.1.2"
resolved "https://registry.yarnpkg.com/@metamask/onboarding/-/onboarding-0.1.2.tgz#d5126cbb5e593d782645d6236c497e27bd38d3c4"
integrity sha512-+85Z5OxckGuYr5cCoMlpxASu9geJBMYvwkNWqa5qDDEYKZ8eNXHsADcVYFsvBhxFcf87dC7ty1kWljYVEfTIIA==
dependencies:
bowser "^2.5.4"
"@mrmlnc/readdir-enhanced@^2.2.1":
version "2.2.1"
resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
@ -3560,11 +3572,6 @@ ansi-red@^0.1.1:
dependencies:
ansi-wrap "0.1.0"
ansi-regex@^0.2.0, ansi-regex@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-0.2.1.tgz#0d8e946967a3d8143f93e24e298525fc1b2235f9"
integrity sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=
ansi-regex@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
@ -3580,11 +3587,6 @@ ansi-regex@^4.1.0:
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==
ansi-styles@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.1.0.tgz#eaecbf66cd706882760b2f4691582b8f55d7a7de"
integrity sha1-6uy/Zs1waIJ2Cy9GkVgrj1XXp94=
ansi-styles@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
@ -4025,10 +4027,11 @@ assert-plus@1.0.0, assert-plus@^1.0.0:
integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=
assert@^1.1.1, assert@^1.3.0, assert@^1.4.0, assert@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91"
integrity sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=
version "1.5.0"
resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb"
integrity sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==
dependencies:
object-assign "^4.1.1"
util "0.10.3"
assertion-error@^1.0.1:
@ -5640,6 +5643,11 @@ bowser@^1.7.3:
resolved "https://registry.yarnpkg.com/bowser/-/bowser-1.9.3.tgz#6643ae4d783f31683f6d23156976b74183862162"
integrity sha512-/gp96UlcFw5DbV2KQPCqTqi0Mb9gZRyDAHiDsGEH+4B/KOQjeoE5lM1PxlVX8DQDvfEfitmC1rW2Oy8fk/XBDg==
bowser@^2.5.4:
version "2.7.0"
resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.7.0.tgz#96eab1fa07fab08c1ec4c75977a7c8ddf8e0fe1f"
integrity sha512-aIlMvstvu8x+34KEiOHD3AsBgdrzg6sxALYiukOWhFvGMbQI6TRP/iY0LMhUrHs56aD6P1G0Z7h45PUJaa5m9w==
boxen@^1.2.1:
version "1.3.0"
resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b"
@ -6435,17 +6443,6 @@ chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.2, chalk@^2.4.1, chalk@^2.4.
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
chalk@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.5.1.tgz#663b3a648b68b55d04690d49167aa837858f2174"
integrity sha1-Zjs6ZItotV0EaQ1JFnqoN4WPIXQ=
dependencies:
ansi-styles "^1.1.0"
escape-string-regexp "^1.0.0"
has-ansi "^0.1.0"
strip-ansi "^0.3.0"
supports-color "^0.2.0"
chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
@ -7023,7 +7020,7 @@ comma-separated-tokens@^1.0.0:
resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.7.tgz#419cd7fb3258b1ed838dc0953167a25e152f5b59"
integrity sha512-Jrx3xsP4pPv4AwJUDWY9wOXGtwPXARej6Xd99h4TUGotmf8APuquKMpK+dnD3UgyxK7OEWaisjZz+3b5jtL6xQ==
commander@2, commander@2.11.0, commander@^2.3.0, commander@^2.5.0, commander@^2.6.0:
commander@2, commander@2.11.0, commander@^2.5.0, commander@^2.6.0:
version "2.11.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563"
integrity sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==
@ -7237,6 +7234,11 @@ contains-path@^0.1.0:
resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a"
integrity sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=
content-disposition@0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4"
integrity sha1-DPaLud318r55YcOoUXjLhdunjLQ=
content-disposition@0.5.3, content-disposition@^0.5.2:
version "0.5.3"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd"
@ -9598,7 +9600,7 @@ escape-html@^1.0.3, escape-html@~1.0.3:
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
escape-string-regexp@1.0.5, escape-string-regexp@^1.0.0, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.3, escape-string-regexp@^1.0.5:
escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.3, escape-string-regexp@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
@ -11308,6 +11310,13 @@ fast-safe-stringify@^2.0.6, fast-safe-stringify@^2.0.7:
resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743"
integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==
fast-url-parser@1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/fast-url-parser/-/fast-url-parser-1.1.3.tgz#f4af3ea9f34d8a271cf58ad2b3759f431f0b318d"
integrity sha1-9K8+qfNNiicc9YrSs3WfQx8LMY0=
dependencies:
punycode "^1.3.2"
fast-write-atomic@~0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/fast-write-atomic/-/fast-write-atomic-0.2.1.tgz#7ee8ef0ce3c1f531043c09ae8e5143361ab17ede"
@ -11442,11 +11451,6 @@ file-loader@^3.0.1:
loader-utils "^1.0.2"
schema-utils "^1.0.0"
file-size@0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/file-size/-/file-size-0.0.5.tgz#057d43c3a3ed735da3f90d6052ab380f1e6d5e3b"
integrity sha1-BX1Dw6Ptc12j+Q1gUqs4Dx5tXjs=
file-system-cache@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/file-system-cache/-/file-system-cache-1.0.5.tgz#84259b36a2bbb8d3d6eb1021d3132ffe64cfff4f"
@ -13288,13 +13292,6 @@ har-validator@~5.1.0:
ajv "^6.5.5"
har-schema "^2.0.0"
has-ansi@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-0.1.0.tgz#84f265aae8c0e6a88a12d7022894b7568894c62e"
integrity sha1-hPJlqujA5qiKEtcCKJS3VoiUxi4=
dependencies:
ansi-regex "^0.2.0"
has-ansi@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
@ -18311,11 +18308,23 @@ mime-db@^1.28.0:
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.41.0.tgz#9110408e1f6aa1b34aef51f2c9df3caddf46b6a0"
integrity sha512-B5gxBI+2K431XW8C2rcc/lhppbuji67nf9v39eH8pkWoZDxnAL0PxdpH32KYRScniF8qDHBDlI+ipgg5WrCUYw==
mime-db@~1.33.0:
version "1.33.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db"
integrity sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==
mime-db@~1.38.0:
version "1.38.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.38.0.tgz#1a2aab16da9eb167b49c6e4df2d9c68d63d8e2ad"
integrity sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==
mime-types@2.1.18:
version "2.1.18"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8"
integrity sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==
dependencies:
mime-db "~1.33.0"
mime-types@^2.1.12, mime-types@^2.1.21, mime-types@~2.1.16, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24:
version "2.1.24"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81"
@ -18335,7 +18344,7 @@ mime@1.6.0, mime@^1.6.0:
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
mime@^1.2.11, mime@^1.4.1:
mime@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
integrity sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==
@ -19766,13 +19775,6 @@ opn@5.4.0:
dependencies:
is-wsl "^1.1.0"
opn@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/opn/-/opn-5.2.0.tgz#71fdf934d6827d676cecbea1531f95d354641225"
integrity sha512-Jd/GpzPyHF4P2/aNOVmS3lfMSWV9J7cOhCG1s08XCEAsPkB7lp6ddiU0J7XzyQRDUh8BqJ7PchfINjR8jyofRQ==
dependencies:
is-wsl "^1.1.0"
optimist@0.6.x, optimist@^0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
@ -20443,7 +20445,7 @@ path-is-absolute@^1.0.0, path-is-absolute@^1.0.1:
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
path-is-inside@^1.0.1, path-is-inside@^1.0.2:
path-is-inside@1.0.2, path-is-inside@^1.0.1, path-is-inside@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=
@ -20480,6 +20482,11 @@ path-to-regexp@0.1.7:
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
path-to-regexp@2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-2.2.1.tgz#90b617025a16381a879bc82a38d4e8bdeb2bcf45"
integrity sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ==
path-to-regexp@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d"
@ -21928,6 +21935,11 @@ randomhex@0.1.5:
resolved "https://registry.yarnpkg.com/randomhex/-/randomhex-0.1.5.tgz#baceef982329091400f2a2912c6cd02f1094f585"
integrity sha1-us7vmCMpCRQA8qKRLGzQLxCU9YU=
range-parser@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
integrity sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=
range-parser@^1.2.0, range-parser@^1.2.1, range-parser@~1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
@ -24148,6 +24160,20 @@ serve-favicon@^2.5.0:
parseurl "~1.3.2"
safe-buffer "5.1.1"
serve-handler@^6.1.2:
version "6.1.2"
resolved "https://registry.yarnpkg.com/serve-handler/-/serve-handler-6.1.2.tgz#f05b0421a313fff2d257838cba00cbcc512cd2b6"
integrity sha512-RFh49wX7zJmmOVDcIjiDSJnMH+ItQEvyuYLYuDBVoA/xmQSCuj+uRmk1cmBB5QQlI3qOiWKp6p4DUGY+Z5AB2A==
dependencies:
bytes "3.0.0"
content-disposition "0.5.2"
fast-url-parser "1.1.3"
mime-types "2.1.18"
minimatch "3.0.4"
path-is-inside "1.0.2"
path-to-regexp "2.2.1"
range-parser "1.2.0"
serve-static@1.14.1:
version "1.14.1"
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9"
@ -25048,17 +25074,6 @@ static-module@^2.2.0:
static-eval "^2.0.0"
through2 "~2.0.3"
static-server@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/static-server/-/static-server-2.2.1.tgz#49e3cae2a001736b0ee9e95d21d3d843fc95efaa"
integrity sha512-j5eeW6higxYNmXMIT8iHjsdiViTpQDthg7o+SHsRtqdbxscdHqBHXwrXjHC8hL3F0Tsu34ApUpDkwzMBPBsrLw==
dependencies:
chalk "^0.5.1"
commander "^2.3.0"
file-size "0.0.5"
mime "^1.2.11"
opn "^5.2.0"
"statuses@>= 1.3.1 < 2":
version "1.3.1"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e"
@ -25363,13 +25378,6 @@ strip-ansi@5.2.0, strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0:
dependencies:
ansi-regex "^4.1.0"
strip-ansi@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.3.0.tgz#25f48ea22ca79187f3174a4db8759347bb126220"
integrity sha1-JfSOoiynkYfzF0pNuHWTR7sSYiA=
dependencies:
ansi-regex "^0.2.1"
strip-ansi@^3.0.0, strip-ansi@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
@ -25639,11 +25647,6 @@ supports-color@4.4.0:
dependencies:
has-flag "^2.0.0"
supports-color@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-0.2.0.tgz#d92de2694eb3f67323973d7ae3d8b55b4c22190a"
integrity sha1-2S3iaU6z9nMjlz1649i1W0wiGQo=
supports-color@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"