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:
parent
015ba83c6e
commit
f763979bed
@ -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"
|
||||
},
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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', '*')
|
||||
}
|
||||
})
|
||||
|
@ -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
|
||||
|
@ -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 = {
|
||||
|
92
development/static-server.js
Normal file
92
development/static-server.js
Normal 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)
|
13
package.json
13
package.json
@ -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
@ -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>
|
||||
|
@ -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 () => {
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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'
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
1
ui/app/components/ui/snackbar/index.js
Normal file
1
ui/app/components/ui/snackbar/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './snackbar.component'
|
11
ui/app/components/ui/snackbar/index.scss
Normal file
11
ui/app/components/ui/snackbar/index.scss
Normal 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;
|
||||
}
|
18
ui/app/components/ui/snackbar/snackbar.component.js
Normal file
18
ui/app/components/ui/snackbar/snackbar.component.js
Normal 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
|
@ -1,5 +1,6 @@
|
||||
@import '../../../components/ui/button/buttons';
|
||||
@import '../../../components/ui/dialog/dialog';
|
||||
@import '../../../components/ui/snackbar/index';
|
||||
|
||||
@import './footer.scss';
|
||||
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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()),
|
||||
|
@ -50,4 +50,4 @@
|
||||
font-size: 80px;
|
||||
margin-top: 70px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
48
ui/app/pages/first-time-flow/onboarding-initiator-util.js
Normal file
48
ui/app/pages/first-time-flow/onboarding-initiator-util.js
Normal 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)
|
||||
}
|
||||
}
|
@ -60,7 +60,7 @@
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: 0xp;
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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
143
yarn.lock
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user