Merge pull request #14478 from MetaMask/Version-v10.14.0
Version v10.14.0 RCfeature/default_network_editable
commit
7a627680a8
@ -1,24 +1,16 @@ |
||||
import React from 'react'; |
||||
import { |
||||
MetaMetricsProvider, |
||||
LegacyMetaMetricsProvider, |
||||
LegacyMetaMetricsProvider |
||||
} from '../ui/contexts/metametrics'; |
||||
import { |
||||
MetaMetricsProvider as NewMetaMetricsProvider, |
||||
LegacyMetaMetricsProvider as NewLegacyMetaMetricsProvider, |
||||
} from '../ui/contexts/metametrics.new'; |
||||
|
||||
const MetaMetricsProviderStorybook = (props) =>
|
||||
( |
||||
<MetaMetricsProvider> |
||||
<LegacyMetaMetricsProvider> |
||||
<NewMetaMetricsProvider> |
||||
<NewLegacyMetaMetricsProvider> |
||||
<MetaMetricsProvider> |
||||
<LegacyMetaMetricsProvider> |
||||
{props.children} |
||||
</NewLegacyMetaMetricsProvider> |
||||
</NewMetaMetricsProvider> |
||||
</LegacyMetaMetricsProvider> |
||||
</MetaMetricsProvider> |
||||
</LegacyMetaMetricsProvider> |
||||
</MetaMetricsProvider> |
||||
); |
||||
|
||||
export default MetaMetricsProviderStorybook |
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