commit
741f623338
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 556 B |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 616 B |
@ -0,0 +1,86 @@ |
|||||||
|
import { EVENT_NAMES } from '../../../shared/constants/metametrics'; |
||||||
|
import { SECOND } from '../../../shared/constants/time'; |
||||||
|
|
||||||
|
const USER_PROMPTED_EVENT_NAME_MAP = { |
||||||
|
eth_signTypedData_v4: EVENT_NAMES.SIGNATURE_REQUESTED, |
||||||
|
eth_signTypedData_v3: EVENT_NAMES.SIGNATURE_REQUESTED, |
||||||
|
eth_signTypedData: EVENT_NAMES.SIGNATURE_REQUESTED, |
||||||
|
eth_personal_sign: EVENT_NAMES.SIGNATURE_REQUESTED, |
||||||
|
eth_sign: EVENT_NAMES.SIGNATURE_REQUESTED, |
||||||
|
eth_getEncryptionPublicKey: EVENT_NAMES.ENCRYPTION_PUBLIC_KEY_REQUESTED, |
||||||
|
eth_decrypt: EVENT_NAMES.DECRYPTION_REQUESTED, |
||||||
|
wallet_requestPermissions: EVENT_NAMES.PERMISSIONS_REQUESTED, |
||||||
|
eth_requestAccounts: EVENT_NAMES.PERMISSIONS_REQUESTED, |
||||||
|
}; |
||||||
|
|
||||||
|
const samplingTimeouts = {}; |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns a middleware that tracks inpage_provider usage using sampling for |
||||||
|
* each type of event except those that require user interaction, such as |
||||||
|
* signature requests |
||||||
|
* |
||||||
|
* @param {object} opts - options for the rpc method tracking middleware |
||||||
|
* @param {Function} opts.trackEvent - trackEvent method from MetaMetricsController |
||||||
|
* @param {Function} opts.getMetricsState - get the state of MetaMetricsController |
||||||
|
* @returns {Function} |
||||||
|
*/ |
||||||
|
export default function createRPCMethodTrackingMiddleware({ |
||||||
|
trackEvent, |
||||||
|
getMetricsState, |
||||||
|
}) { |
||||||
|
return function rpcMethodTrackingMiddleware( |
||||||
|
/** @type {any} */ req, |
||||||
|
/** @type {any} */ res, |
||||||
|
/** @type {Function} */ next, |
||||||
|
) { |
||||||
|
const startTime = Date.now(); |
||||||
|
const { origin } = req; |
||||||
|
|
||||||
|
next((callback) => { |
||||||
|
const endTime = Date.now(); |
||||||
|
if (!getMetricsState().participateInMetaMetrics) { |
||||||
|
return callback(); |
||||||
|
} |
||||||
|
if (USER_PROMPTED_EVENT_NAME_MAP[req.method]) { |
||||||
|
const userRejected = res.error?.code === 4001; |
||||||
|
trackEvent({ |
||||||
|
event: USER_PROMPTED_EVENT_NAME_MAP[req.method], |
||||||
|
category: 'inpage_provider', |
||||||
|
referrer: { |
||||||
|
url: origin, |
||||||
|
}, |
||||||
|
properties: { |
||||||
|
method: req.method, |
||||||
|
status: userRejected ? 'rejected' : 'approved', |
||||||
|
error_code: res.error?.code, |
||||||
|
error_message: res.error?.message, |
||||||
|
has_result: typeof res.result !== 'undefined', |
||||||
|
duration: endTime - startTime, |
||||||
|
}, |
||||||
|
}); |
||||||
|
} else if (typeof samplingTimeouts[req.method] === 'undefined') { |
||||||
|
trackEvent({ |
||||||
|
event: 'Provider Method Called', |
||||||
|
category: 'inpage_provider', |
||||||
|
referrer: { |
||||||
|
url: origin, |
||||||
|
}, |
||||||
|
properties: { |
||||||
|
method: req.method, |
||||||
|
error_code: res.error?.code, |
||||||
|
error_message: res.error?.message, |
||||||
|
has_result: typeof res.result !== 'undefined', |
||||||
|
duration: endTime - startTime, |
||||||
|
}, |
||||||
|
}); |
||||||
|
// Only record one call to this method every ten seconds to avoid
|
||||||
|
// overloading network requests.
|
||||||
|
samplingTimeouts[req.method] = setTimeout(() => { |
||||||
|
delete samplingTimeouts[req.method]; |
||||||
|
}, SECOND * 10); |
||||||
|
} |
||||||
|
return callback(); |
||||||
|
}); |
||||||
|
}; |
||||||
|
} |
@ -1,73 +0,0 @@ |
|||||||
{ |
|
||||||
"gasPricesBasic": { |
|
||||||
"average": 85, |
|
||||||
"fast": 200, |
|
||||||
"safeLow": 80 |
|
||||||
}, |
|
||||||
"metametrics": { |
|
||||||
"mockMetaMetricsResponse": true |
|
||||||
}, |
|
||||||
"swaps": { |
|
||||||
"featureFlags": { |
|
||||||
"bsc": { |
|
||||||
"mobile_active": false, |
|
||||||
"extension_active": true, |
|
||||||
"fallback_to_v1": true |
|
||||||
}, |
|
||||||
"ethereum": { |
|
||||||
"mobile_active": false, |
|
||||||
"extension_active": true, |
|
||||||
"fallback_to_v1": true |
|
||||||
}, |
|
||||||
"polygon": { |
|
||||||
"mobile_active": false, |
|
||||||
"extension_active": true, |
|
||||||
"fallback_to_v1": false |
|
||||||
} |
|
||||||
} |
|
||||||
}, |
|
||||||
"tokenList": { |
|
||||||
"0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0": { |
|
||||||
"address": "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0", |
|
||||||
"symbol": "MATIC", |
|
||||||
"decimals": 18, |
|
||||||
"name": "Polygon", |
|
||||||
"iconUrl": "https://raw.githubusercontent.com/MetaMask/eth-contract-metadata/master/images/matic-network-logo.svg", |
|
||||||
"aggregators": [ |
|
||||||
"airswapLight", |
|
||||||
"bancor", |
|
||||||
"coinGecko", |
|
||||||
"kleros", |
|
||||||
"oneInch", |
|
||||||
"paraswap", |
|
||||||
"pmm", |
|
||||||
"totle", |
|
||||||
"zapper", |
|
||||||
"zerion", |
|
||||||
"zeroEx" |
|
||||||
], |
|
||||||
"occurrences": 11 |
|
||||||
}, |
|
||||||
"0x0d8775f648430679a709e98d2b0cb6250d2887ef": { |
|
||||||
"address": "0x0d8775f648430679a709e98d2b0cb6250d2887ef", |
|
||||||
"symbol": "BAT", |
|
||||||
"decimals": 18, |
|
||||||
"name": "Basic Attention Tok", |
|
||||||
"iconUrl": "https://s3.amazonaws.com/airswap-token-images/BAT.png", |
|
||||||
"aggregators": [ |
|
||||||
"airswapLight", |
|
||||||
"bancor", |
|
||||||
"coinGecko", |
|
||||||
"kleros", |
|
||||||
"oneInch", |
|
||||||
"paraswap", |
|
||||||
"pmm", |
|
||||||
"totle", |
|
||||||
"zapper", |
|
||||||
"zerion", |
|
||||||
"zeroEx" |
|
||||||
], |
|
||||||
"occurrences": 11 |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,21 @@ |
|||||||
|
{ |
||||||
|
"address": "0961ca10d49b9b8e371aa0bcf77fe5730b18f2e4", |
||||||
|
"crypto": { |
||||||
|
"ciphertext": "eb10547515f1bf3bb6eefed3cb39303c64a30560f378b789592b1ac632bbc14c", |
||||||
|
"cipherparams": { |
||||||
|
"iv": "25ebfc7358ce77462ef7b970e91309d6" |
||||||
|
}, |
||||||
|
"cipher": "aes-128-ctr", |
||||||
|
"kdf": "scrypt", |
||||||
|
"kdfparams": { |
||||||
|
"dklen": 32, |
||||||
|
"salt": "42417a49fb6cbb10167d7bc4ed8ae71d02cb4ee6309a42417e0051e7472d45c5", |
||||||
|
"n": 8192, |
||||||
|
"r": 8, |
||||||
|
"p": 1 |
||||||
|
}, |
||||||
|
"mac": "0491462fbca0c7a71d249de141736304d699749c2faf5ab575d6a502e26099d7" |
||||||
|
}, |
||||||
|
"id": "03754e23-770b-43d3-a35e-bae1196e6f86", |
||||||
|
"version": 3 |
||||||
|
} |
@ -0,0 +1,114 @@ |
|||||||
|
const { strict: assert } = require('assert'); |
||||||
|
const { convertToHexValue, withFixtures } = require('../helpers'); |
||||||
|
|
||||||
|
describe('Chain Interactions', function () { |
||||||
|
it('should add the XDAI chain and not switch the network', async function () { |
||||||
|
const ganacheOptions = { |
||||||
|
accounts: [ |
||||||
|
{ |
||||||
|
secretKey: |
||||||
|
'0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC', |
||||||
|
balance: convertToHexValue(25000000000000000000), |
||||||
|
}, |
||||||
|
], |
||||||
|
}; |
||||||
|
await withFixtures( |
||||||
|
{ |
||||||
|
dapp: true, |
||||||
|
fixtures: 'connected-state', |
||||||
|
ganacheOptions, |
||||||
|
title: this.test.title, |
||||||
|
}, |
||||||
|
async ({ driver }) => { |
||||||
|
await driver.navigate(); |
||||||
|
await driver.fill('#password', 'correct horse battery staple'); |
||||||
|
await driver.press('#password', driver.Key.ENTER); |
||||||
|
|
||||||
|
// trigger add chain confirmation
|
||||||
|
await driver.openNewPage('http://127.0.0.1:8080/'); |
||||||
|
await driver.clickElement('#addEthereumChain'); |
||||||
|
await driver.waitUntilXWindowHandles(3); |
||||||
|
const windowHandles = await driver.getAllWindowHandles(); |
||||||
|
const extension = windowHandles[0]; |
||||||
|
await driver.switchToWindowWithTitle( |
||||||
|
'MetaMask Notification', |
||||||
|
windowHandles, |
||||||
|
); |
||||||
|
|
||||||
|
// verify chain details
|
||||||
|
const [networkName, networkUrl, chainId] = await driver.findElements( |
||||||
|
'.definition-list dd', |
||||||
|
); |
||||||
|
assert.equal(await networkName.getText(), 'xDAI Chain'); |
||||||
|
assert.equal(await networkUrl.getText(), 'https://dai.poa.network'); |
||||||
|
assert.equal(await chainId.getText(), '100'); |
||||||
|
|
||||||
|
// approve add chain, cancel switch chain
|
||||||
|
await driver.clickElement({ text: 'Approve', tag: 'button' }); |
||||||
|
await driver.clickElement({ text: 'Cancel', tag: 'button' }); |
||||||
|
|
||||||
|
// switch to extension
|
||||||
|
await driver.waitUntilXWindowHandles(2); |
||||||
|
await driver.switchToWindow(extension); |
||||||
|
|
||||||
|
// verify networks
|
||||||
|
const networkDisplay = await driver.findElement('.network-display'); |
||||||
|
await networkDisplay.click(); |
||||||
|
assert.equal(await networkDisplay.getText(), 'Localhost 8545'); |
||||||
|
const xDaiChain = await driver.findElements({ |
||||||
|
text: 'xDAI Chain', |
||||||
|
tag: 'span', |
||||||
|
}); |
||||||
|
assert.ok(xDaiChain.length, 1); |
||||||
|
}, |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should add the XDAI chain and switch the network', async function () { |
||||||
|
const ganacheOptions = { |
||||||
|
accounts: [ |
||||||
|
{ |
||||||
|
secretKey: |
||||||
|
'0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC', |
||||||
|
balance: convertToHexValue(25000000000000000000), |
||||||
|
}, |
||||||
|
], |
||||||
|
}; |
||||||
|
await withFixtures( |
||||||
|
{ |
||||||
|
dapp: true, |
||||||
|
fixtures: 'connected-state', |
||||||
|
ganacheOptions, |
||||||
|
title: this.test.title, |
||||||
|
}, |
||||||
|
async ({ driver }) => { |
||||||
|
await driver.navigate(); |
||||||
|
await driver.fill('#password', 'correct horse battery staple'); |
||||||
|
await driver.press('#password', driver.Key.ENTER); |
||||||
|
|
||||||
|
// trigger add chain confirmation
|
||||||
|
await driver.openNewPage('http://127.0.0.1:8080/'); |
||||||
|
await driver.clickElement('#addEthereumChain'); |
||||||
|
await driver.waitUntilXWindowHandles(3); |
||||||
|
const windowHandles = await driver.getAllWindowHandles(); |
||||||
|
const extension = windowHandles[0]; |
||||||
|
await driver.switchToWindowWithTitle( |
||||||
|
'MetaMask Notification', |
||||||
|
windowHandles, |
||||||
|
); |
||||||
|
|
||||||
|
// approve and switch chain
|
||||||
|
await driver.clickElement({ text: 'Approve', tag: 'button' }); |
||||||
|
await driver.clickElement({ text: 'Switch network', tag: 'button' }); |
||||||
|
|
||||||
|
// switch to extension
|
||||||
|
await driver.waitUntilXWindowHandles(2); |
||||||
|
await driver.switchToWindow(extension); |
||||||
|
|
||||||
|
// verify current network
|
||||||
|
const networkDisplay = await driver.findElement('.network-display'); |
||||||
|
assert.equal(await networkDisplay.getText(), 'xDAI Chain'); |
||||||
|
}, |
||||||
|
); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,273 @@ |
|||||||
|
const { strict: assert } = require('assert'); |
||||||
|
const { convertToHexValue, withFixtures } = require('../helpers'); |
||||||
|
|
||||||
|
describe('Settings Search', function () { |
||||||
|
const ganacheOptions = { |
||||||
|
accounts: [ |
||||||
|
{ |
||||||
|
secretKey: |
||||||
|
'0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC', |
||||||
|
balance: convertToHexValue(25000000000000000000), |
||||||
|
}, |
||||||
|
], |
||||||
|
}; |
||||||
|
const settingsSearch = { |
||||||
|
general: 'Primary Currency', |
||||||
|
advanced: 'State Logs', |
||||||
|
contacts: 'Contacts', |
||||||
|
security: 'Reveal Secret', |
||||||
|
alerts: 'Browsing a website', |
||||||
|
networks: 'Ethereum Mainnet', |
||||||
|
experimental: 'Token Detection', |
||||||
|
about: 'Terms of Use', |
||||||
|
}; |
||||||
|
|
||||||
|
it('should find element inside the General tab', async function () { |
||||||
|
await withFixtures( |
||||||
|
{ |
||||||
|
dapp: true, |
||||||
|
fixtures: 'imported-account', |
||||||
|
ganacheOptions, |
||||||
|
title: this.test.title, |
||||||
|
}, |
||||||
|
async ({ driver }) => { |
||||||
|
await driver.navigate(); |
||||||
|
await driver.fill('#password', 'correct horse battery staple'); |
||||||
|
await driver.press('#password', driver.Key.ENTER); |
||||||
|
|
||||||
|
await driver.clickElement('.account-menu__icon'); |
||||||
|
await driver.clickElement({ text: 'Settings', tag: 'div' }); |
||||||
|
await driver.fill('#search-settings', settingsSearch.general); |
||||||
|
|
||||||
|
const page = 'General'; |
||||||
|
await driver.clickElement({ text: page, tag: 'span' }); |
||||||
|
assert.equal( |
||||||
|
await driver.isElementPresent({ text: page, tag: 'div' }), |
||||||
|
true, |
||||||
|
`${settingsSearch.general} item does not redirect to ${page} view`, |
||||||
|
); |
||||||
|
}, |
||||||
|
); |
||||||
|
}); |
||||||
|
it('should find element inside the Advanced tab', async function () { |
||||||
|
await withFixtures( |
||||||
|
{ |
||||||
|
dapp: true, |
||||||
|
fixtures: 'imported-account', |
||||||
|
ganacheOptions, |
||||||
|
title: this.test.title, |
||||||
|
}, |
||||||
|
async ({ driver }) => { |
||||||
|
await driver.navigate(); |
||||||
|
await driver.fill('#password', 'correct horse battery staple'); |
||||||
|
await driver.press('#password', driver.Key.ENTER); |
||||||
|
|
||||||
|
await driver.clickElement('.account-menu__icon'); |
||||||
|
await driver.clickElement({ text: 'Settings', tag: 'div' }); |
||||||
|
await driver.fill('#search-settings', settingsSearch.advanced); |
||||||
|
|
||||||
|
// Check if element redirects to the correct page
|
||||||
|
const page = 'Advanced'; |
||||||
|
await driver.clickElement({ text: page, tag: 'span' }); |
||||||
|
assert.equal( |
||||||
|
await driver.isElementPresent({ text: page, tag: 'div' }), |
||||||
|
true, |
||||||
|
`${settingsSearch.advanced} item does not redirect to ${page} view`, |
||||||
|
); |
||||||
|
}, |
||||||
|
); |
||||||
|
}); |
||||||
|
it('should find element inside the Contacts tab', async function () { |
||||||
|
await withFixtures( |
||||||
|
{ |
||||||
|
dapp: true, |
||||||
|
fixtures: 'imported-account', |
||||||
|
ganacheOptions, |
||||||
|
title: this.test.title, |
||||||
|
}, |
||||||
|
async ({ driver }) => { |
||||||
|
await driver.navigate(); |
||||||
|
await driver.fill('#password', 'correct horse battery staple'); |
||||||
|
await driver.press('#password', driver.Key.ENTER); |
||||||
|
|
||||||
|
await driver.clickElement('.account-menu__icon'); |
||||||
|
await driver.clickElement({ text: 'Settings', tag: 'div' }); |
||||||
|
await driver.fill('#search-settings', settingsSearch.contacts); |
||||||
|
|
||||||
|
// Check if element redirects to the correct page
|
||||||
|
const page = 'Contacts'; |
||||||
|
await driver.clickElement({ text: page, tag: 'span' }); |
||||||
|
assert.equal( |
||||||
|
await driver.isElementPresent({ text: page, tag: 'div' }), |
||||||
|
true, |
||||||
|
`${settingsSearch.contacts} item does not redirect to ${page} view`, |
||||||
|
); |
||||||
|
}, |
||||||
|
); |
||||||
|
}); |
||||||
|
it('should find element inside the Security tab', async function () { |
||||||
|
await withFixtures( |
||||||
|
{ |
||||||
|
dapp: true, |
||||||
|
fixtures: 'imported-account', |
||||||
|
ganacheOptions, |
||||||
|
title: this.test.title, |
||||||
|
}, |
||||||
|
async ({ driver }) => { |
||||||
|
await driver.navigate(); |
||||||
|
await driver.fill('#password', 'correct horse battery staple'); |
||||||
|
await driver.press('#password', driver.Key.ENTER); |
||||||
|
|
||||||
|
await driver.clickElement('.account-menu__icon'); |
||||||
|
await driver.clickElement({ text: 'Settings', tag: 'div' }); |
||||||
|
await driver.fill('#search-settings', settingsSearch.security); |
||||||
|
|
||||||
|
// Check if element redirects to the correct page
|
||||||
|
const page = 'Security'; |
||||||
|
await driver.clickElement({ text: page, tag: 'span' }); |
||||||
|
assert.equal( |
||||||
|
await driver.isElementPresent({ text: page, tag: 'div' }), |
||||||
|
true, |
||||||
|
`${settingsSearch.security} item does not redirect to ${page} view`, |
||||||
|
); |
||||||
|
}, |
||||||
|
); |
||||||
|
}); |
||||||
|
it('should find element inside the Alerts tab', async function () { |
||||||
|
await withFixtures( |
||||||
|
{ |
||||||
|
dapp: true, |
||||||
|
fixtures: 'imported-account', |
||||||
|
ganacheOptions, |
||||||
|
title: this.test.title, |
||||||
|
}, |
||||||
|
async ({ driver }) => { |
||||||
|
await driver.navigate(); |
||||||
|
await driver.fill('#password', 'correct horse battery staple'); |
||||||
|
await driver.press('#password', driver.Key.ENTER); |
||||||
|
|
||||||
|
await driver.clickElement('.account-menu__icon'); |
||||||
|
await driver.clickElement({ text: 'Settings', tag: 'div' }); |
||||||
|
await driver.fill('#search-settings', settingsSearch.alerts); |
||||||
|
|
||||||
|
// Check if element redirects to the correct page
|
||||||
|
const page = 'Alerts'; |
||||||
|
await driver.clickElement({ text: page, tag: 'span' }); |
||||||
|
assert.equal( |
||||||
|
await driver.isElementPresent({ text: page, tag: 'div' }), |
||||||
|
true, |
||||||
|
`${settingsSearch.alerts} item does not redirect to ${page} view`, |
||||||
|
); |
||||||
|
}, |
||||||
|
); |
||||||
|
}); |
||||||
|
it('should find element inside the Networks tab', async function () { |
||||||
|
await withFixtures( |
||||||
|
{ |
||||||
|
dapp: true, |
||||||
|
fixtures: 'imported-account', |
||||||
|
ganacheOptions, |
||||||
|
title: this.test.title, |
||||||
|
}, |
||||||
|
async ({ driver }) => { |
||||||
|
await driver.navigate(); |
||||||
|
await driver.fill('#password', 'correct horse battery staple'); |
||||||
|
await driver.press('#password', driver.Key.ENTER); |
||||||
|
|
||||||
|
await driver.clickElement('.account-menu__icon'); |
||||||
|
await driver.clickElement({ text: 'Settings', tag: 'div' }); |
||||||
|
await driver.fill('#search-settings', settingsSearch.networks); |
||||||
|
|
||||||
|
// Check if element redirects to the correct page
|
||||||
|
const page = 'Networks'; |
||||||
|
await driver.clickElement({ text: page, tag: 'span' }); |
||||||
|
assert.equal( |
||||||
|
await driver.isElementPresent({ text: page, tag: 'div' }), |
||||||
|
true, |
||||||
|
`${settingsSearch.networks} item does not redirect to ${page} view`, |
||||||
|
); |
||||||
|
}, |
||||||
|
); |
||||||
|
}); |
||||||
|
it('should find element inside the Experimental tab', async function () { |
||||||
|
await withFixtures( |
||||||
|
{ |
||||||
|
dapp: true, |
||||||
|
fixtures: 'imported-account', |
||||||
|
ganacheOptions, |
||||||
|
title: this.test.title, |
||||||
|
}, |
||||||
|
async ({ driver }) => { |
||||||
|
await driver.navigate(); |
||||||
|
await driver.fill('#password', 'correct horse battery staple'); |
||||||
|
await driver.press('#password', driver.Key.ENTER); |
||||||
|
|
||||||
|
await driver.clickElement('.account-menu__icon'); |
||||||
|
await driver.clickElement({ text: 'Settings', tag: 'div' }); |
||||||
|
await driver.fill('#search-settings', settingsSearch.experimental); |
||||||
|
|
||||||
|
// Check if element redirects to the correct page
|
||||||
|
const page = 'Experimental'; |
||||||
|
await driver.clickElement({ text: page, tag: 'span' }); |
||||||
|
assert.equal( |
||||||
|
await driver.isElementPresent({ text: page, tag: 'div' }), |
||||||
|
true, |
||||||
|
`${settingsSearch.experimental} item not redirect to ${page} view`, |
||||||
|
); |
||||||
|
}, |
||||||
|
); |
||||||
|
}); |
||||||
|
it('should find element inside the About tab', async function () { |
||||||
|
await withFixtures( |
||||||
|
{ |
||||||
|
dapp: true, |
||||||
|
fixtures: 'imported-account', |
||||||
|
ganacheOptions, |
||||||
|
title: this.test.title, |
||||||
|
}, |
||||||
|
async ({ driver }) => { |
||||||
|
await driver.navigate(); |
||||||
|
await driver.fill('#password', 'correct horse battery staple'); |
||||||
|
await driver.press('#password', driver.Key.ENTER); |
||||||
|
|
||||||
|
await driver.clickElement('.account-menu__icon'); |
||||||
|
await driver.clickElement({ text: 'Settings', tag: 'div' }); |
||||||
|
await driver.fill('#search-settings', settingsSearch.about); |
||||||
|
|
||||||
|
// Check if element redirects to the correct page
|
||||||
|
const page = 'About'; |
||||||
|
await driver.clickElement({ text: page, tag: 'span' }); |
||||||
|
assert.equal( |
||||||
|
await driver.isElementPresent({ text: page, tag: 'div' }), |
||||||
|
true, |
||||||
|
`${settingsSearch.about} item does not redirect to ${page} view`, |
||||||
|
); |
||||||
|
}, |
||||||
|
); |
||||||
|
}); |
||||||
|
it('should display "Element not found" for a non-existing element', async function () { |
||||||
|
await withFixtures( |
||||||
|
{ |
||||||
|
dapp: true, |
||||||
|
fixtures: 'imported-account', |
||||||
|
ganacheOptions, |
||||||
|
title: this.test.title, |
||||||
|
}, |
||||||
|
async ({ driver }) => { |
||||||
|
await driver.navigate(); |
||||||
|
await driver.fill('#password', 'correct horse battery staple'); |
||||||
|
await driver.press('#password', driver.Key.ENTER); |
||||||
|
|
||||||
|
await driver.clickElement('.account-menu__icon'); |
||||||
|
await driver.clickElement({ text: 'Settings', tag: 'div' }); |
||||||
|
await driver.fill('#search-settings', 'Lorem ipsum'); |
||||||
|
|
||||||
|
const found = await driver.isElementPresent({ |
||||||
|
text: 'No matching results found', |
||||||
|
tag: 'span', |
||||||
|
}); |
||||||
|
assert.equal(found, true, 'Non existent element was found'); |
||||||
|
}, |
||||||
|
); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,94 @@ |
|||||||
|
const { strict: assert } = require('assert'); |
||||||
|
const { withFixtures } = require('../helpers'); |
||||||
|
|
||||||
|
describe('Swap Eth for another Token', function () { |
||||||
|
const ganacheOptions = { |
||||||
|
accounts: [ |
||||||
|
{ |
||||||
|
secretKey: |
||||||
|
'0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC', |
||||||
|
balance: 25000000000000000000, |
||||||
|
}, |
||||||
|
], |
||||||
|
}; |
||||||
|
it('Completes a Swap between Eth and Matic', async function () { |
||||||
|
await withFixtures( |
||||||
|
{ |
||||||
|
fixtures: 'imported-account', |
||||||
|
ganacheOptions, |
||||||
|
title: this.test.title, |
||||||
|
failOnConsoleError: false, |
||||||
|
}, |
||||||
|
async ({ driver }) => { |
||||||
|
await driver.navigate(); |
||||||
|
await driver.fill('#password', 'correct horse battery staple'); |
||||||
|
await driver.press('#password', driver.Key.ENTER); |
||||||
|
await driver.clickElement( |
||||||
|
'.wallet-overview__buttons .icon-button:nth-child(3)', |
||||||
|
); |
||||||
|
await driver.clickElement( |
||||||
|
'[class*="dropdown-search-list"] + div[class*="MuiFormControl-root MuiTextField-root"]', |
||||||
|
); |
||||||
|
await driver.fill('input[placeholder*="0"]', '2'); |
||||||
|
await driver.clickElement( |
||||||
|
'[class*="dropdown-search-list"] + div[class*="MuiFormControl-root MuiTextField-root"]', |
||||||
|
); |
||||||
|
await driver.clickElement( |
||||||
|
'[class="dropdown-search-list__closed-primary-label dropdown-search-list__select-default"]', |
||||||
|
); |
||||||
|
await driver.clickElement('[placeholder="Search for a token"]'); |
||||||
|
await driver.clickElement('[placeholder="Search for a token"]'); |
||||||
|
await driver.fill('[placeholder="Search for a token"]', 'DAI'); |
||||||
|
await driver.waitForSelector( |
||||||
|
'[class="searchable-item-list__primary-label"]', |
||||||
|
); |
||||||
|
await driver.clickElement( |
||||||
|
'[class="searchable-item-list__primary-label"]', |
||||||
|
); |
||||||
|
await driver.clickElement({ text: 'Review Swap', tag: 'button' }); |
||||||
|
await driver.waitForSelector('[class*="box--align-items-center"]'); |
||||||
|
const estimatedEth = await driver.waitForSelector({ |
||||||
|
css: '[class*="box--align-items-center"]', |
||||||
|
text: 'Estimated gas fee', |
||||||
|
}); |
||||||
|
assert.equal(await estimatedEth.getText(), 'Estimated gas fee'); |
||||||
|
await driver.waitForSelector( |
||||||
|
'[class="exchange-rate-display main-quote-summary__exchange-rate-display"]', |
||||||
|
); |
||||||
|
await driver.waitForSelector( |
||||||
|
'[class="fee-card__info-tooltip-container"]', |
||||||
|
); |
||||||
|
await driver.waitForSelector({ |
||||||
|
css: '[class="countdown-timer__time"]', |
||||||
|
text: '0:24', |
||||||
|
}); |
||||||
|
await driver.clickElement({ text: 'Swap', tag: 'button' }); |
||||||
|
const sucessfulTransactionMessage = await driver.waitForSelector({ |
||||||
|
css: '[class="awaiting-swap__header"]', |
||||||
|
text: 'Transaction complete', |
||||||
|
}); |
||||||
|
assert.equal( |
||||||
|
await sucessfulTransactionMessage.getText(), |
||||||
|
'Transaction complete', |
||||||
|
); |
||||||
|
const sucessfulTransactionToken = await driver.waitForSelector({ |
||||||
|
css: '[class="awaiting-swap__amount-and-symbol"]', |
||||||
|
text: 'DAI', |
||||||
|
}); |
||||||
|
assert.equal(await sucessfulTransactionToken.getText(), 'DAI'); |
||||||
|
await driver.clickElement({ text: 'Close', tag: 'button' }); |
||||||
|
await driver.clickElement('[data-testid="home__activity-tab"]'); |
||||||
|
const swaptotal = await driver.waitForSelector({ |
||||||
|
css: '[class="transaction-list-item__primary-currency"]', |
||||||
|
text: '-2 TESTETH', |
||||||
|
}); |
||||||
|
assert.equal(await swaptotal.getText(), '-2 TESTETH'); |
||||||
|
const swaptotaltext = await driver.waitForSelector({ |
||||||
|
css: '[class="list-item__title"]', |
||||||
|
text: 'Swap TESTETH to DAI', |
||||||
|
}); |
||||||
|
assert.equal(await swaptotaltext.getText(), 'Swap TESTETH to DAI'); |
||||||
|
}, |
||||||
|
); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,36 @@ |
|||||||
|
import React from 'react'; |
||||||
|
import { useSelector } from 'react-redux'; |
||||||
|
import PropTypes from 'prop-types'; |
||||||
|
import classNames from 'classnames'; |
||||||
|
|
||||||
|
import Box from '../../../ui/box/box'; |
||||||
|
import Button from '../../../ui/button'; |
||||||
|
import { useI18nContext } from '../../../../hooks/useI18nContext'; |
||||||
|
import { getDetectedTokensInCurrentNetwork } from '../../../../selectors'; |
||||||
|
|
||||||
|
const DetectedTokensLink = ({ className = '', onClick }) => { |
||||||
|
const t = useI18nContext(); |
||||||
|
const detectedTokens = useSelector(getDetectedTokensInCurrentNetwork); |
||||||
|
|
||||||
|
return ( |
||||||
|
<Box |
||||||
|
className={classNames('detected-tokens-link', className)} |
||||||
|
marginTop={1} |
||||||
|
> |
||||||
|
<Button |
||||||
|
type="link" |
||||||
|
className="detected-tokens-link__link" |
||||||
|
onClick={onClick} |
||||||
|
> |
||||||
|
{t('numberOfNewTokensDetected', [detectedTokens.length])} |
||||||
|
</Button> |
||||||
|
</Box> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
DetectedTokensLink.propTypes = { |
||||||
|
onClick: PropTypes.func.isRequired, |
||||||
|
className: PropTypes.string, |
||||||
|
}; |
||||||
|
|
||||||
|
export default DetectedTokensLink; |
@ -0,0 +1,5 @@ |
|||||||
|
.detected-tokens-link { |
||||||
|
& &__link { |
||||||
|
@include H6; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,50 @@ |
|||||||
|
import React, { useContext, useState } from 'react'; |
||||||
|
import PropTypes from 'prop-types'; |
||||||
|
import { I18nContext } from '../../../../contexts/i18n'; |
||||||
|
|
||||||
|
import Box from '../../../ui/box'; |
||||||
|
import Button from '../../../ui/button'; |
||||||
|
import Typography from '../../../ui/typography/typography'; |
||||||
|
import { |
||||||
|
DISPLAY, |
||||||
|
FONT_WEIGHT, |
||||||
|
TYPOGRAPHY, |
||||||
|
} from '../../../../helpers/constants/design-system'; |
||||||
|
|
||||||
|
const DetectedTokenAggregators = ({ aggregatorsList }) => { |
||||||
|
const t = useContext(I18nContext); |
||||||
|
const numOfHiddenAggregators = parseInt(aggregatorsList.length, 10) - 2; |
||||||
|
const [displayMore, setDisplayMore] = useState(false); |
||||||
|
|
||||||
|
return ( |
||||||
|
<Box display={DISPLAY.INLINE_FLEX} className="detected-token-aggregators"> |
||||||
|
<Typography variant={TYPOGRAPHY.H7} fontWeight={FONT_WEIGHT.NORMAL}> |
||||||
|
{t('fromTokenLists', [ |
||||||
|
numOfHiddenAggregators > 0 && !displayMore ? ( |
||||||
|
<Typography variant={TYPOGRAPHY.H7} fontWeight={FONT_WEIGHT.NORMAL}> |
||||||
|
{`${aggregatorsList.slice(0, 2).join(', ')}`} |
||||||
|
<Button |
||||||
|
type="link" |
||||||
|
className="detected-token-aggregators__link" |
||||||
|
onClick={() => setDisplayMore(true)} |
||||||
|
key="detected-token-aggrgators-link" |
||||||
|
> |
||||||
|
{t('plusXMore', [numOfHiddenAggregators])} |
||||||
|
</Button> |
||||||
|
</Typography> |
||||||
|
) : ( |
||||||
|
<Typography variant={TYPOGRAPHY.H7} fontWeight={FONT_WEIGHT.NORMAL}> |
||||||
|
{`${aggregatorsList.join(', ')}.`} |
||||||
|
</Typography> |
||||||
|
), |
||||||
|
])} |
||||||
|
</Typography> |
||||||
|
</Box> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
DetectedTokenAggregators.propTypes = { |
||||||
|
aggregatorsList: PropTypes.array.isRequired, |
||||||
|
}; |
||||||
|
|
||||||
|
export default DetectedTokenAggregators; |
@ -0,0 +1,43 @@ |
|||||||
|
import React from 'react'; |
||||||
|
import { DISPLAY } from '../../../../helpers/constants/design-system'; |
||||||
|
|
||||||
|
import Box from '../../../ui/box'; |
||||||
|
import DetectedTokenAggregators from './detected-token-aggregators'; |
||||||
|
|
||||||
|
export default { |
||||||
|
title: 'Components/App/DetectedToken/DetectedTokenAggregators', |
||||||
|
id: __filename, |
||||||
|
argTypes: { |
||||||
|
aggregatorsList: { control: 'array' }, |
||||||
|
}, |
||||||
|
args: { |
||||||
|
aggregatorsList1: [ |
||||||
|
'Aave', |
||||||
|
'Bancor', |
||||||
|
'CMC', |
||||||
|
'Crypto.com', |
||||||
|
'CoinGecko', |
||||||
|
'1inch', |
||||||
|
'Paraswap', |
||||||
|
'PMM', |
||||||
|
'Synthetix', |
||||||
|
'Zapper', |
||||||
|
'Zerion', |
||||||
|
'0x', |
||||||
|
], |
||||||
|
aggregatorsList2: ['Aave', 'Bancor'], |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
const Template = (args) => { |
||||||
|
return ( |
||||||
|
<Box display={DISPLAY.GRID}> |
||||||
|
<DetectedTokenAggregators aggregatorsList={args.aggregatorsList1} /> |
||||||
|
<DetectedTokenAggregators aggregatorsList={args.aggregatorsList2} /> |
||||||
|
</Box> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export const DefaultStory = Template.bind({}); |
||||||
|
|
||||||
|
DefaultStory.storyName = 'Default'; |
@ -0,0 +1,43 @@ |
|||||||
|
import * as React from 'react'; |
||||||
|
import { |
||||||
|
renderWithProvider, |
||||||
|
screen, |
||||||
|
fireEvent, |
||||||
|
} from '../../../../../test/jest'; |
||||||
|
import configureStore from '../../../../store/store'; |
||||||
|
|
||||||
|
import DetectedTokenAggregators from './detected-token-aggregators'; |
||||||
|
|
||||||
|
describe('DetectedTokenAggregators', () => { |
||||||
|
const args = { |
||||||
|
aggregatorsList: [ |
||||||
|
'Aave', |
||||||
|
'Bancor', |
||||||
|
'CMC', |
||||||
|
'Crypto.com', |
||||||
|
'CoinGecko', |
||||||
|
'1inch', |
||||||
|
'Paraswap', |
||||||
|
'PMM', |
||||||
|
'Synthetix', |
||||||
|
'Zapper', |
||||||
|
'Zerion', |
||||||
|
'0x', |
||||||
|
], |
||||||
|
}; |
||||||
|
|
||||||
|
it('should render the detected token aggregators', async () => { |
||||||
|
const store = configureStore({}); |
||||||
|
renderWithProvider(<DetectedTokenAggregators {...args} />, store); |
||||||
|
|
||||||
|
expect(screen.getByText('From token lists:')).toBeInTheDocument(); |
||||||
|
expect(screen.getByText('Aave, Bancor')).toBeInTheDocument(); |
||||||
|
expect(screen.getByText('+ 10 more')).toBeInTheDocument(); |
||||||
|
fireEvent.click(screen.getByText('+ 10 more')); |
||||||
|
expect( |
||||||
|
screen.getByText( |
||||||
|
'Aave, Bancor, CMC, Crypto.com, CoinGecko, 1inch, Paraswap, PMM, Synthetix, Zapper, Zerion, 0x.', |
||||||
|
), |
||||||
|
).toBeInTheDocument(); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,13 @@ |
|||||||
|
.detected-token-aggregators { |
||||||
|
.typography { |
||||||
|
display: inline; |
||||||
|
} |
||||||
|
|
||||||
|
& &__link { |
||||||
|
@include H7; |
||||||
|
|
||||||
|
padding: 0; |
||||||
|
display: inline; |
||||||
|
margin-left: 4px; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,41 @@ |
|||||||
|
import React from 'react'; |
||||||
|
import { useSelector } from 'react-redux'; |
||||||
|
import PropTypes from 'prop-types'; |
||||||
|
|
||||||
|
import Box from '../../../ui/box'; |
||||||
|
import Identicon from '../../../ui/identicon'; |
||||||
|
import DetectedTokenValues from '../detected-token-values/detected-token-values'; |
||||||
|
import DetectedTokenAddress from '../detected-token-address/detected-token-address'; |
||||||
|
import DetectedTokenAggregators from '../detected-token-aggregators/detected-token-aggregators'; |
||||||
|
import { DISPLAY } from '../../../../helpers/constants/design-system'; |
||||||
|
import { getTokenList } from '../../../../selectors'; |
||||||
|
|
||||||
|
const DetectedTokenDetails = ({ tokenAddress }) => { |
||||||
|
const tokenList = useSelector(getTokenList); |
||||||
|
const token = tokenList[tokenAddress]; |
||||||
|
|
||||||
|
return ( |
||||||
|
<Box display={DISPLAY.FLEX} className="detected-token-details"> |
||||||
|
<Identicon |
||||||
|
className="detected-token-details__identicon" |
||||||
|
address={tokenAddress} |
||||||
|
diameter={40} |
||||||
|
/> |
||||||
|
<Box |
||||||
|
display={DISPLAY.GRID} |
||||||
|
marginLeft={2} |
||||||
|
className="detected-token-details__data" |
||||||
|
> |
||||||
|
<DetectedTokenValues token={token} /> |
||||||
|
<DetectedTokenAddress address={token.address} /> |
||||||
|
<DetectedTokenAggregators aggregatorsList={token.aggregators} /> |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
DetectedTokenDetails.propTypes = { |
||||||
|
tokenAddress: PropTypes.string, |
||||||
|
}; |
||||||
|
|
||||||
|
export default DetectedTokenDetails; |
@ -0,0 +1,26 @@ |
|||||||
|
import React from 'react'; |
||||||
|
|
||||||
|
import DetectedTokenDetails from './detected-token-details'; |
||||||
|
|
||||||
|
export default { |
||||||
|
title: 'Components/App/DetectedToken/DetectedTokenDetails', |
||||||
|
id: __filename, |
||||||
|
argTypes: { |
||||||
|
address: { control: 'text' }, |
||||||
|
}, |
||||||
|
args: { |
||||||
|
address: '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f', |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
const Template = (args) => { |
||||||
|
return ( |
||||||
|
<div style={{ width: '320px' }}> |
||||||
|
<DetectedTokenDetails tokenAddress={args.address} /> |
||||||
|
</div> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export const DefaultStory = Template.bind({}); |
||||||
|
|
||||||
|
DefaultStory.storyName = 'Default'; |
@ -0,0 +1,35 @@ |
|||||||
|
import * as React from 'react'; |
||||||
|
import { |
||||||
|
renderWithProvider, |
||||||
|
screen, |
||||||
|
fireEvent, |
||||||
|
} from '../../../../../test/jest'; |
||||||
|
import configureStore from '../../../../store/store'; |
||||||
|
import testData from '../../../../../.storybook/test-data'; |
||||||
|
|
||||||
|
import DetectedTokenDetails from './detected-token-details'; |
||||||
|
|
||||||
|
describe('DetectedTokenDetails', () => { |
||||||
|
const args = { |
||||||
|
tokenAddress: '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f', |
||||||
|
}; |
||||||
|
|
||||||
|
it('should render the detected token details', async () => { |
||||||
|
const store = configureStore(testData); |
||||||
|
renderWithProvider(<DetectedTokenDetails {...args} />, store); |
||||||
|
|
||||||
|
expect(screen.getByText('0 SNX')).toBeInTheDocument(); |
||||||
|
expect(screen.getByText('$0')).toBeInTheDocument(); |
||||||
|
expect(screen.getByText('Token address:')).toBeInTheDocument(); |
||||||
|
expect(screen.getByText('0xc01...2a6f')).toBeInTheDocument(); |
||||||
|
expect(screen.getByText('From token lists:')).toBeInTheDocument(); |
||||||
|
expect(screen.getByText('Aave, Bancor')).toBeInTheDocument(); |
||||||
|
expect(screen.getByText('+ 10 more')).toBeInTheDocument(); |
||||||
|
fireEvent.click(screen.getByText('+ 10 more')); |
||||||
|
expect( |
||||||
|
screen.getByText( |
||||||
|
'Aave, Bancor, CMC, Crypto.com, CoinGecko, 1inch, Paraswap, PMM, Synthetix, Zapper, Zerion, 0x.', |
||||||
|
), |
||||||
|
).toBeInTheDocument(); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,9 @@ |
|||||||
|
.detected-token-details { |
||||||
|
&__identicon { |
||||||
|
margin-top: 4px; |
||||||
|
} |
||||||
|
|
||||||
|
&__data { |
||||||
|
flex-grow: 1; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,57 @@ |
|||||||
|
import React, { useState } from 'react'; |
||||||
|
import PropTypes from 'prop-types'; |
||||||
|
|
||||||
|
import Box from '../../../ui/box'; |
||||||
|
import Typography from '../../../ui/typography'; |
||||||
|
import CheckBox from '../../../ui/check-box'; |
||||||
|
|
||||||
|
import { |
||||||
|
COLORS, |
||||||
|
DISPLAY, |
||||||
|
TYPOGRAPHY, |
||||||
|
} from '../../../../helpers/constants/design-system'; |
||||||
|
import { useTokenTracker } from '../../../../hooks/useTokenTracker'; |
||||||
|
import { useTokenFiatAmount } from '../../../../hooks/useTokenFiatAmount'; |
||||||
|
|
||||||
|
const DetectedTokenValues = ({ token }) => { |
||||||
|
const [selectedTokens, setSelectedTokens] = useState(false); |
||||||
|
const { tokensWithBalances } = useTokenTracker([token]); |
||||||
|
const balanceToRender = tokensWithBalances[0]?.string; |
||||||
|
const balance = tokensWithBalances[0]?.balance; |
||||||
|
const formattedFiatBalance = useTokenFiatAmount( |
||||||
|
token.address, |
||||||
|
balanceToRender, |
||||||
|
token.symbol, |
||||||
|
); |
||||||
|
|
||||||
|
return ( |
||||||
|
<Box display={DISPLAY.INLINE_FLEX} className="detected-token-values"> |
||||||
|
<Box marginBottom={1}> |
||||||
|
<Typography variant={TYPOGRAPHY.H4}> |
||||||
|
{`${balance || '0'} ${token.symbol}`} |
||||||
|
</Typography> |
||||||
|
<Typography variant={TYPOGRAPHY.H7} color={COLORS.TEXT_ALTERNATIVE}> |
||||||
|
{formattedFiatBalance || '$0'} |
||||||
|
</Typography> |
||||||
|
</Box> |
||||||
|
<Box className="detected-token-values__checkbox"> |
||||||
|
<CheckBox |
||||||
|
checked={selectedTokens} |
||||||
|
onClick={() => setSelectedTokens((checked) => !checked)} |
||||||
|
/> |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
DetectedTokenValues.propTypes = { |
||||||
|
token: PropTypes.shape({ |
||||||
|
address: PropTypes.string.isRequired, |
||||||
|
decimals: PropTypes.number, |
||||||
|
symbol: PropTypes.string, |
||||||
|
iconUrl: PropTypes.string, |
||||||
|
aggregators: PropTypes.array, |
||||||
|
}), |
||||||
|
}; |
||||||
|
|
||||||
|
export default DetectedTokenValues; |
@ -0,0 +1,43 @@ |
|||||||
|
import React from 'react'; |
||||||
|
|
||||||
|
import DetectedTokenValues from './detected-token-values'; |
||||||
|
|
||||||
|
export default { |
||||||
|
title: 'Components/App/DetectedToken/DetectedTokenValues', |
||||||
|
id: __filename, |
||||||
|
argTypes: { |
||||||
|
address: { control: 'text' }, |
||||||
|
symbol: { control: 'text' }, |
||||||
|
decimals: { control: 'text' }, |
||||||
|
iconUrl: { control: 'text' }, |
||||||
|
aggregators: { control: 'array' }, |
||||||
|
}, |
||||||
|
args: { |
||||||
|
address: '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f', |
||||||
|
symbol: 'SNX', |
||||||
|
decimals: 18, |
||||||
|
iconUrl: 'https://assets.coingecko.com/coins/images/3406/large/SNX.png', |
||||||
|
aggregators: [ |
||||||
|
'aave', |
||||||
|
'bancor', |
||||||
|
'cmc', |
||||||
|
'cryptocom', |
||||||
|
'coinGecko', |
||||||
|
'oneInch', |
||||||
|
'paraswap', |
||||||
|
'pmm', |
||||||
|
'synthetix', |
||||||
|
'zapper', |
||||||
|
'zerion', |
||||||
|
'zeroEx', |
||||||
|
], |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
const Template = (args) => { |
||||||
|
return <DetectedTokenValues token={args} />; |
||||||
|
}; |
||||||
|
|
||||||
|
export const DefaultStory = Template.bind({}); |
||||||
|
|
||||||
|
DefaultStory.storyName = 'Default'; |
@ -0,0 +1,37 @@ |
|||||||
|
import * as React from 'react'; |
||||||
|
import { renderWithProvider, screen } from '../../../../../test/jest'; |
||||||
|
import configureStore from '../../../../store/store'; |
||||||
|
import testData from '../../../../../.storybook/test-data'; |
||||||
|
|
||||||
|
import DetectedTokenValues from './detected-token-values'; |
||||||
|
|
||||||
|
describe('DetectedTokenValues', () => { |
||||||
|
const args = { |
||||||
|
address: '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f', |
||||||
|
symbol: 'SNX', |
||||||
|
decimals: 18, |
||||||
|
iconUrl: 'https://assets.coingecko.com/coins/images/3406/large/SNX.png', |
||||||
|
aggregators: [ |
||||||
|
'aave', |
||||||
|
'bancor', |
||||||
|
'cmc', |
||||||
|
'cryptocom', |
||||||
|
'coinGecko', |
||||||
|
'oneInch', |
||||||
|
'paraswap', |
||||||
|
'pmm', |
||||||
|
'synthetix', |
||||||
|
'zapper', |
||||||
|
'zerion', |
||||||
|
'zeroEx', |
||||||
|
], |
||||||
|
}; |
||||||
|
|
||||||
|
it('should render the detected token address', async () => { |
||||||
|
const store = configureStore(testData); |
||||||
|
renderWithProvider(<DetectedTokenValues token={args} />, store); |
||||||
|
|
||||||
|
expect(screen.getByText('0 SNX')).toBeInTheDocument(); |
||||||
|
expect(screen.getByText('$0')).toBeInTheDocument(); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,5 @@ |
|||||||
|
.detected-token-values { |
||||||
|
&__checkbox { |
||||||
|
margin-left: auto; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,192 @@ |
|||||||
|
import React, { useCallback, useContext, useRef, useState } from 'react'; |
||||||
|
import PropTypes from 'prop-types'; |
||||||
|
import Button from '../../ui/button'; |
||||||
|
import { I18nContext } from '../../../contexts/i18n'; |
||||||
|
import Box from '../../ui/box/box'; |
||||||
|
import { |
||||||
|
ALIGN_ITEMS, |
||||||
|
DISPLAY, |
||||||
|
JUSTIFY_CONTENT, |
||||||
|
} from '../../../helpers/constants/design-system'; |
||||||
|
|
||||||
|
const radius = 14; |
||||||
|
const strokeWidth = 2; |
||||||
|
const radiusWithStroke = radius - strokeWidth / 2; |
||||||
|
|
||||||
|
export default function HoldToRevealButton({ buttonText, onLongPressed }) { |
||||||
|
const t = useContext(I18nContext); |
||||||
|
const isLongPressing = useRef(false); |
||||||
|
const [isUnlocking, setIsUnlocking] = useState(false); |
||||||
|
const [hasTriggeredUnlock, setHasTriggeredUnlock] = useState(false); |
||||||
|
|
||||||
|
/** |
||||||
|
* Prevent animation events from propogating up |
||||||
|
* |
||||||
|
* @param e - Native animation event - React.AnimationEvent<HTMLDivElement> |
||||||
|
*/ |
||||||
|
const preventPropogation = (e) => { |
||||||
|
e.stopPropagation(); |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* Event for mouse click down |
||||||
|
*/ |
||||||
|
const onMouseDown = () => { |
||||||
|
isLongPressing.current = true; |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* Event for mouse click up |
||||||
|
*/ |
||||||
|
const onMouseUp = () => { |
||||||
|
isLongPressing.current = false; |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* 1. Progress cirle completed. Begin next animation phase (Shrink halo and show unlocked padlock) |
||||||
|
*/ |
||||||
|
const onProgressComplete = () => { |
||||||
|
isLongPressing.current && setIsUnlocking(true); |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* 2. Trigger onLongPressed callback. Begin next animation phase (Shrink unlocked padlock and fade in original content) |
||||||
|
* |
||||||
|
* @param e - Native animation event - React.AnimationEvent<HTMLDivElement> |
||||||
|
*/ |
||||||
|
const triggerOnLongPressed = (e) => { |
||||||
|
onLongPressed(); |
||||||
|
setHasTriggeredUnlock(true); |
||||||
|
preventPropogation(e); |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* 3. Reset animation states |
||||||
|
*/ |
||||||
|
const resetAnimationStates = () => { |
||||||
|
setIsUnlocking(false); |
||||||
|
setHasTriggeredUnlock(false); |
||||||
|
}; |
||||||
|
|
||||||
|
const renderPreCompleteContent = useCallback(() => { |
||||||
|
return ( |
||||||
|
<Box |
||||||
|
className={`hold-to-reveal-button__absolute-fill ${ |
||||||
|
isUnlocking ? 'hold-to-reveal-button__invisible' : null |
||||||
|
} ${ |
||||||
|
hasTriggeredUnlock ? 'hold-to-reveal-button__main-icon-show' : null |
||||||
|
}`}
|
||||||
|
> |
||||||
|
<Box className="hold-to-reveal-button__absolute-fill"> |
||||||
|
<svg className="hold-to-reveal-button__circle-svg"> |
||||||
|
<circle |
||||||
|
className="hold-to-reveal-button__circle-background" |
||||||
|
cx={radius} |
||||||
|
cy={radius} |
||||||
|
r={radiusWithStroke} |
||||||
|
/> |
||||||
|
</svg> |
||||||
|
</Box> |
||||||
|
<Box className="hold-to-reveal-button__absolute-fill"> |
||||||
|
<svg className="hold-to-reveal-button__circle-svg"> |
||||||
|
<circle |
||||||
|
onTransitionEnd={onProgressComplete} |
||||||
|
className="hold-to-reveal-button__circle-foreground" |
||||||
|
cx={radius} |
||||||
|
cy={radius} |
||||||
|
r={radiusWithStroke} |
||||||
|
/> |
||||||
|
</svg> |
||||||
|
</Box> |
||||||
|
<Box |
||||||
|
display={DISPLAY.FLEX} |
||||||
|
alignItems={ALIGN_ITEMS.CENTER} |
||||||
|
justifyContent={JUSTIFY_CONTENT.CENTER} |
||||||
|
className="hold-to-reveal-button__lock-icon-container" |
||||||
|
> |
||||||
|
<img |
||||||
|
src="images/lock-icon.svg" |
||||||
|
alt={t('padlock')} |
||||||
|
className="hold-to-reveal-button__lock-icon" |
||||||
|
/> |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
); |
||||||
|
}, [isUnlocking, hasTriggeredUnlock, t]); |
||||||
|
|
||||||
|
const renderPostCompleteContent = useCallback(() => { |
||||||
|
return isUnlocking ? ( |
||||||
|
<div |
||||||
|
className={`hold-to-reveal-button__absolute-fill ${ |
||||||
|
hasTriggeredUnlock ? 'hold-to-reveal-button__unlock-icon-hide' : null |
||||||
|
}`}
|
||||||
|
onAnimationEnd={resetAnimationStates} |
||||||
|
> |
||||||
|
<div |
||||||
|
onAnimationEnd={preventPropogation} |
||||||
|
className="hold-to-reveal-button__absolute-fill hold-to-reveal-button__circle-static-outer-container" |
||||||
|
> |
||||||
|
<svg className="hold-to-reveal-button__circle-svg"> |
||||||
|
<circle |
||||||
|
className="hold-to-reveal-button__circle-static-outer" |
||||||
|
cx={14} |
||||||
|
cy={14} |
||||||
|
r={14} |
||||||
|
/> |
||||||
|
</svg> |
||||||
|
</div> |
||||||
|
<div |
||||||
|
onAnimationEnd={preventPropogation} |
||||||
|
className="hold-to-reveal-button__absolute-fill hold-to-reveal-button__circle-static-inner-container" |
||||||
|
> |
||||||
|
<svg className="hold-to-reveal-button__circle-svg"> |
||||||
|
<circle |
||||||
|
className="hold-to-reveal-button__circle-static-inner" |
||||||
|
cx={14} |
||||||
|
cy={14} |
||||||
|
r={12} |
||||||
|
/> |
||||||
|
</svg> |
||||||
|
</div> |
||||||
|
<div |
||||||
|
className="hold-to-reveal-button__unlock-icon-container" |
||||||
|
onAnimationEnd={triggerOnLongPressed} |
||||||
|
> |
||||||
|
<img |
||||||
|
src="images/unlock-icon.svg" |
||||||
|
alt={t('padlock')} |
||||||
|
className="hold-to-reveal-button__unlock-icon" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
) : null; |
||||||
|
}, [isUnlocking, hasTriggeredUnlock, t]); |
||||||
|
|
||||||
|
return ( |
||||||
|
<Button |
||||||
|
onMouseDown={onMouseDown} |
||||||
|
onMouseUp={onMouseUp} |
||||||
|
type="primary" |
||||||
|
icon={ |
||||||
|
<Box marginRight={2} className="hold-to-reveal-button__icon-container"> |
||||||
|
{renderPreCompleteContent()} |
||||||
|
{renderPostCompleteContent()} |
||||||
|
</Box> |
||||||
|
} |
||||||
|
className="hold-to-reveal-button__button-hold" |
||||||
|
> |
||||||
|
{buttonText} |
||||||
|
</Button> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
HoldToRevealButton.propTypes = { |
||||||
|
/** |
||||||
|
* Text to be displayed on the button |
||||||
|
*/ |
||||||
|
buttonText: PropTypes.string.isRequired, |
||||||
|
/** |
||||||
|
* Function to be called after the animation is finished |
||||||
|
*/ |
||||||
|
onLongPressed: PropTypes.func.isRequired, |
||||||
|
}; |
@ -0,0 +1,22 @@ |
|||||||
|
import React from 'react'; |
||||||
|
import HoldToRevealButton from './hold-to-reveal-button'; |
||||||
|
|
||||||
|
export default { |
||||||
|
title: 'Components/App/HoldToRevealButton', |
||||||
|
id: __filename, |
||||||
|
argTypes: { |
||||||
|
buttonText: { control: 'text' }, |
||||||
|
onLongPressed: { action: 'Revealing the SRP' }, |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
export const DefaultStory = (args) => { |
||||||
|
return <HoldToRevealButton {...args} />; |
||||||
|
}; |
||||||
|
|
||||||
|
DefaultStory.storyName = 'Default'; |
||||||
|
|
||||||
|
DefaultStory.args = { |
||||||
|
buttonText: 'Hold to reveal SRP', |
||||||
|
onLongPressed: () => console.log('Revealed'), |
||||||
|
}; |
@ -0,0 +1,72 @@ |
|||||||
|
import React from 'react'; |
||||||
|
import { render, fireEvent, waitFor } from '@testing-library/react'; |
||||||
|
import HoldToRevealButton from './hold-to-reveal-button'; |
||||||
|
|
||||||
|
describe('HoldToRevealButton', () => { |
||||||
|
let props = {}; |
||||||
|
|
||||||
|
beforeEach(() => { |
||||||
|
const mockOnLongPressed = jest.fn(); |
||||||
|
|
||||||
|
props = { |
||||||
|
onLongPressed: mockOnLongPressed, |
||||||
|
buttonText: 'Hold to reveal SRP', |
||||||
|
}; |
||||||
|
}); |
||||||
|
|
||||||
|
it('should render a button with label', () => { |
||||||
|
const { getByText } = render(<HoldToRevealButton {...props} />); |
||||||
|
|
||||||
|
expect(getByText('Hold to reveal SRP')).toBeInTheDocument(); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should render a button when mouse is down and up', () => { |
||||||
|
const { getByText } = render(<HoldToRevealButton {...props} />); |
||||||
|
|
||||||
|
const button = getByText('Hold to reveal SRP'); |
||||||
|
|
||||||
|
fireEvent.mouseDown(button); |
||||||
|
|
||||||
|
expect(button).toBeDefined(); |
||||||
|
|
||||||
|
fireEvent.mouseUp(button); |
||||||
|
|
||||||
|
expect(button).toBeDefined(); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should not show the locked padlock when a button is long pressed and then should show it after it was lifted off before the animation concludes', () => { |
||||||
|
const { getByText } = render(<HoldToRevealButton {...props} />); |
||||||
|
|
||||||
|
const button = getByText('Hold to reveal SRP'); |
||||||
|
|
||||||
|
fireEvent.mouseDown(button); |
||||||
|
|
||||||
|
waitFor(() => { |
||||||
|
expect(button.firstChild).toHaveClass( |
||||||
|
'hold-to-reveal-button__lock-icon-container', |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
fireEvent.mouseUp(button); |
||||||
|
|
||||||
|
waitFor(() => { |
||||||
|
expect(button.firstChild).not.toHaveClass( |
||||||
|
'hold-to-reveal-button__lock-icon-container', |
||||||
|
); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should show the unlocked padlock when a button is long pressed for the duration of the animation', () => { |
||||||
|
const { getByText } = render(<HoldToRevealButton {...props} />); |
||||||
|
|
||||||
|
const button = getByText('Hold to reveal SRP'); |
||||||
|
|
||||||
|
fireEvent.mouseDown(button); |
||||||
|
|
||||||
|
waitFor(() => { |
||||||
|
expect(button.firstChild).toHaveClass( |
||||||
|
'hold-to-reveal-button__unlock-icon-container', |
||||||
|
); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1 @@ |
|||||||
|
export { default } from './hold-to-reveal-button'; |
@ -0,0 +1,164 @@ |
|||||||
|
// Variables |
||||||
|
$circle-radius: 14px; |
||||||
|
$circle-diameter: $circle-radius * 2; |
||||||
|
// Circumference ~ (2*PI*R). We reduced the number a little to create a snappier interaction |
||||||
|
$circle-circumference: 82; |
||||||
|
$circle-stroke-width: 2px; |
||||||
|
|
||||||
|
// Keyframes |
||||||
|
@keyframes collapse { |
||||||
|
from { |
||||||
|
transform: scale(1); |
||||||
|
} |
||||||
|
|
||||||
|
to { |
||||||
|
transform: scale(0); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@keyframes expand { |
||||||
|
from { |
||||||
|
transform: scale(0); |
||||||
|
} |
||||||
|
|
||||||
|
to { |
||||||
|
transform: scale(1); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@keyframes fadeIn { |
||||||
|
from { |
||||||
|
opacity: 0; |
||||||
|
} |
||||||
|
|
||||||
|
to { |
||||||
|
opacity: 1; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.hold-to-reveal-button { |
||||||
|
// Shared styles |
||||||
|
&__absolute-fill { |
||||||
|
position: absolute; |
||||||
|
top: 0; |
||||||
|
left: 0; |
||||||
|
bottom: 0; |
||||||
|
right: 0; |
||||||
|
} |
||||||
|
|
||||||
|
&__icon { |
||||||
|
height: $circle-diameter; |
||||||
|
width: $circle-diameter; |
||||||
|
} |
||||||
|
|
||||||
|
&__circle-shared { |
||||||
|
fill: transparent; |
||||||
|
stroke-width: $circle-stroke-width; |
||||||
|
} |
||||||
|
|
||||||
|
// Class styles |
||||||
|
&__button-hold { |
||||||
|
padding: 6px 13px 6px 9px !important; |
||||||
|
transform: scale(1) !important; |
||||||
|
transition: 0.5s transform !important; |
||||||
|
|
||||||
|
&:active { |
||||||
|
background-color: var(--primary-1) !important; |
||||||
|
transform: scale(1.05) !important; |
||||||
|
|
||||||
|
.hold-to-reveal-button__circle-foreground { |
||||||
|
stroke-dashoffset: 0 !important; |
||||||
|
} |
||||||
|
|
||||||
|
.hold-to-reveal-button__lock-icon-container { |
||||||
|
opacity: 0 !important; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&__absolute-fill { |
||||||
|
@extend .hold-to-reveal-button__absolute-fill; |
||||||
|
} |
||||||
|
|
||||||
|
&__icon-container { |
||||||
|
@extend .hold-to-reveal-button__icon; |
||||||
|
|
||||||
|
position: relative; |
||||||
|
} |
||||||
|
|
||||||
|
&__main-icon-show { |
||||||
|
animation: 0.4s fadeIn 1.2s forwards; |
||||||
|
} |
||||||
|
|
||||||
|
&__invisible { |
||||||
|
opacity: 0; |
||||||
|
} |
||||||
|
|
||||||
|
&__circle-svg { |
||||||
|
@extend .hold-to-reveal-button__icon; |
||||||
|
|
||||||
|
transform: rotate(-90deg); |
||||||
|
} |
||||||
|
|
||||||
|
&__circle-background { |
||||||
|
@extend .hold-to-reveal-button__circle-shared; |
||||||
|
|
||||||
|
stroke: var(--primary-3); |
||||||
|
} |
||||||
|
|
||||||
|
&__circle-foreground { |
||||||
|
@extend .hold-to-reveal-button__circle-shared; |
||||||
|
|
||||||
|
stroke: var(--ui-white); |
||||||
|
stroke-dasharray: $circle-circumference; |
||||||
|
stroke-dashoffset: $circle-circumference; |
||||||
|
transition: 1s stroke-dashoffset; |
||||||
|
} |
||||||
|
|
||||||
|
&__lock-icon-container { |
||||||
|
@extend .hold-to-reveal-button__absolute-fill; |
||||||
|
|
||||||
|
transition: 0.3s opacity; |
||||||
|
opacity: 1; |
||||||
|
} |
||||||
|
|
||||||
|
&__lock-icon { |
||||||
|
width: 7.88px; |
||||||
|
height: 9px; |
||||||
|
} |
||||||
|
|
||||||
|
&__unlock-icon-hide { |
||||||
|
animation: 0.3s collapse 1s forwards; |
||||||
|
} |
||||||
|
|
||||||
|
&__circle-static-outer-container { |
||||||
|
animation: 0.25s collapse forwards; |
||||||
|
} |
||||||
|
|
||||||
|
&__circle-static-outer { |
||||||
|
fill: var(--ui-white); |
||||||
|
} |
||||||
|
|
||||||
|
&__circle-static-inner-container { |
||||||
|
animation: 0.125s collapse forwards; |
||||||
|
} |
||||||
|
|
||||||
|
&__circle-static-inner { |
||||||
|
fill: var(--primary-1); |
||||||
|
} |
||||||
|
|
||||||
|
&__unlock-icon-container { |
||||||
|
@extend .hold-to-reveal-button__absolute-fill; |
||||||
|
|
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: center; |
||||||
|
transform: scale(0); |
||||||
|
animation: 0.175s expand 0.2s forwards; |
||||||
|
} |
||||||
|
|
||||||
|
&__unlock-icon { |
||||||
|
width: 14px; |
||||||
|
height: 11px; |
||||||
|
} |
||||||
|
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue