diff --git a/__tests__/addaccount.test.js b/__tests__/addaccount.test.js index 30ebc406384..6da47912883 100644 --- a/__tests__/addaccount.test.js +++ b/__tests__/addaccount.test.js @@ -1,4 +1,4 @@ -import { getByText } from '@testing-library/testcafe'; +import { getAllByText, getByText } from '@testing-library/testcafe'; import AddAccountPage from './addaccount-page.po'; import DashboardPage from './dashboard-page.po'; @@ -42,3 +42,13 @@ test('Should be able to add a web3 address', async () => { await dashboardPage.expectAddressToBePresent(FIXTURE_WEB3_ADDRESS); await dashboardPage.expectAccountTableToMatchCount(1); }); + +test('Should be able to redirect to the correct flow from wallet id', async (t) => { + await addAccountPage.navigateTo(`${PAGES.ADD_ACCOUNT}/portis`); + await addAccountPage.waitForPage(PAGES.ADD_ACCOUNT_VIEWONLY); + + await addAccountPage.selectEthereumNetwork(); + + const title = getAllByText('Portis'); + await t.expect(title.exists).ok(); +}); diff --git a/__tests__/fixtures.js b/__tests__/fixtures.js index 07144b40e31..6c1c0edc159 100644 --- a/__tests__/fixtures.js +++ b/__tests__/fixtures.js @@ -33,7 +33,8 @@ const PAGES = { SWAP: `${FIXTURES_CONST.BASE_URL}/swap`, BUY_MEMBERSHIP: `${FIXTURES_CONST.BASE_URL}/membership/buy`, INTERACT_WITH_CONTRACTS: `${FIXTURES_CONST.BASE_URL}/interact-with-contracts`, - DEPLOY_CONTRACTS: `${FIXTURES_CONST.BASE_URL}/deploy-contracts` + DEPLOY_CONTRACTS: `${FIXTURES_CONST.BASE_URL}/deploy-contracts`, + ONBOARDING: `${FIXTURES_CONST.BASE_URL}/onboarding` }; const FIXTURE_ETHEREUM = 'Ethereum'; diff --git a/__tests__/onboarding-page.po.js b/__tests__/onboarding-page.po.js new file mode 100644 index 00000000000..558c6ff495a --- /dev/null +++ b/__tests__/onboarding-page.po.js @@ -0,0 +1,12 @@ +import BasePage from './base-page.po'; +import { PAGES } from './fixtures'; + +export default class OnboardingPage extends BasePage { + async navigateToPage(wallet) { + this.navigateTo(`${PAGES.ONBOARDING}/${wallet}`); + } + + async waitPageLoaded(wallet, timeToWait) { + await this.waitForPage(`${PAGES.ONBOARDING}/${wallet}`, timeToWait); + } +} diff --git a/__tests__/onboarding.test.js b/__tests__/onboarding.test.js new file mode 100644 index 00000000000..6bc219b6197 --- /dev/null +++ b/__tests__/onboarding.test.js @@ -0,0 +1,24 @@ +import { getAllByText } from '@testing-library/testcafe'; + +import { PAGES } from './fixtures'; +import OnboardingPage from './onboarding-page.po'; + +const onboardingPage = new OnboardingPage(); + +fixture('Onboarding').page(PAGES.ONBOARDING); + +test('Should be able to show a custodial exchange onboarding', async (t) => { + await onboardingPage.navigateToPage('kraken'); + await onboardingPage.waitPageLoaded('kraken'); + + const title = getAllByText('Kraken'); + await t.expect(title.exists).ok(); +}); + +test('Should be able to show a non-custodial exchange onboarding', async (t) => { + await onboardingPage.navigateToPage('jaxx'); + await onboardingPage.waitPageLoaded('jaxx'); + + const title = getAllByText('Jaxx'); + await t.expect(title.exists).ok(); +}); diff --git a/package.json b/package.json index 5ca7c9cbf14..71d161387ac 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@mycrypto/eth-scan": "3.4.4", "@mycrypto/ui": "0.24.1", "@mycrypto/unlock-scan": "1.2.0", + "@mycrypto/wallet-list": "1.3.0", "@mycrypto/wallets": "1.3.4", "@reduxjs/toolkit": "1.6.0", "@styled-system/should-forward-prop": "5.1.5", @@ -267,4 +268,4 @@ "pre-push": "yarn test" } } -} \ No newline at end of file +} diff --git a/src/assets/icons/desktopTag.svg b/src/assets/icons/desktopTag.svg new file mode 100755 index 00000000000..5c0ede52096 --- /dev/null +++ b/src/assets/icons/desktopTag.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/exchangeTag.svg b/src/assets/icons/exchangeTag.svg new file mode 100755 index 00000000000..012cff775ea --- /dev/null +++ b/src/assets/icons/exchangeTag.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/hardwareTag.svg b/src/assets/icons/hardwareTag.svg new file mode 100755 index 00000000000..0515c38e649 --- /dev/null +++ b/src/assets/icons/hardwareTag.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/mobileTag.svg b/src/assets/icons/mobileTag.svg new file mode 100755 index 00000000000..5543c5aaf20 --- /dev/null +++ b/src/assets/icons/mobileTag.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/otherTag.svg b/src/assets/icons/otherTag.svg new file mode 100755 index 00000000000..5be8d6410ec --- /dev/null +++ b/src/assets/icons/otherTag.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/icons/walletconnectTag.svg b/src/assets/icons/walletconnectTag.svg new file mode 100755 index 00000000000..b1aa625eb6c --- /dev/null +++ b/src/assets/icons/walletconnectTag.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/icons/webTag.svg b/src/assets/icons/webTag.svg new file mode 100755 index 00000000000..4541dbf1a63 --- /dev/null +++ b/src/assets/icons/webTag.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/wallet-connect.png b/src/assets/images/wallet-connect.png new file mode 100755 index 00000000000..6a64cf80a82 Binary files /dev/null and b/src/assets/images/wallet-connect.png differ diff --git a/src/components/Icon/Icon.test.tsx b/src/components/Icon/Icon.test.tsx index d3b0b222c2f..ce2098a4207 100644 --- a/src/components/Icon/Icon.test.tsx +++ b/src/components/Icon/Icon.test.tsx @@ -16,6 +16,10 @@ describe('Icon', () => { const { container } = renderComponent({ type: 'add' }); expect(container.querySelector('img')).toBeInTheDocument(); }); + it('renders a SVG Icon with a fill and a stroke', () => { + const { container } = renderComponent({ type: 'other-tag' }); + expect(container.querySelector('svg')).toBeInTheDocument(); + }); it('renders a PNG Icon by type', () => { const { container } = renderComponent({ type: 'uni-logo' }); expect(container.querySelector('img')).toBeInTheDocument(); diff --git a/src/components/Icon/Icon.tsx b/src/components/Icon/Icon.tsx index f3127d720ae..dcfcb610857 100644 --- a/src/components/Icon/Icon.tsx +++ b/src/components/Icon/Icon.tsx @@ -32,7 +32,11 @@ import remove from '@assets/icons/actions/remove.svg'; import logoMyCryptoTextBlue from '@assets/icons/brand/logo-text-blue.svg'; import logoMyCryptoText from '@assets/icons/brand/logo-text.svg'; import logoMyCrypto from '@assets/icons/brand/logo.svg'; +import desktopTag from '@assets/icons/desktopTag.svg'; +import exchangeTag from '@assets/icons/exchangeTag.svg'; import feedback from '@assets/icons/feedback.svg'; +import hardwareTag from '@assets/icons/hardwareTag.svg'; +import mobileTag from '@assets/icons/mobileTag.svg'; import navAddAccount from '@assets/icons/navigation/add-account.svg'; import navAssets from '@assets/icons/navigation/assets.svg'; import navBitcoin from '@assets/icons/navigation/bitcoin.svg'; @@ -84,6 +88,7 @@ import navTxStatus from '@assets/icons/navigation/tx-status.svg'; import navUnstoppable from '@assets/icons/navigation/unstoppable.svg'; import navVerifyMessage from '@assets/icons/navigation/verify-message.svg'; import newsletter from '@assets/icons/newsletter.svg'; +import otherTag from '@assets/icons/otherTag.svg'; import coinmarketcap from '@assets/icons/social/coinmarketcap.svg'; import facebook from '@assets/icons/social/facebook.svg'; import github from '@assets/icons/social/github.svg'; @@ -93,7 +98,9 @@ import telegram from '@assets/icons/social/telegram.svg'; import twitter from '@assets/icons/social/twitter.svg'; import telegramIcon from '@assets/icons/telegram.svg'; import twitterIcon from '@assets/icons/twitter.svg'; +import walletconnectTag from '@assets/icons/walletconnectTag.svg'; import website from '@assets/icons/website.svg'; +import webTag from '@assets/icons/webTag.svg'; import whitepaper from '@assets/icons/whitepaper.svg'; import antLogo from '@assets/images/ant-logo.png'; import arrowRight from '@assets/images/arrow-right.svg'; @@ -136,6 +143,7 @@ import membership from '@assets/images/membership/membership-none.svg'; import nodeLogo from '@assets/images/node-logo.svg'; import repLogo from '@assets/images/rep-logo.svg'; import uniLogo from '@assets/images/uni-logo.png'; +import walletConnectLogo from '@assets/images/wallet-connect.png'; import ledgerIcon from '@assets/images/wallets/ledger.svg'; import trezorIcon from '@assets/images/wallets/trezor.svg'; @@ -225,6 +233,15 @@ const svgIcons = { 'tx-receive': receiveIcon, 'tx-network': networkIcon, + /* Wallet Tags */ + 'desktop-tag': desktopTag, + 'exchange-tag': exchangeTag, + 'hardware-tag': hardwareTag, + 'mobile-tag': mobileTag, + 'other-tag': otherTag, + 'walletconnect-tag': walletconnectTag, + 'web-tag': webTag, + /* Navigation */ 'nav-home': navHome, 'nav-send': navSend, @@ -284,7 +301,8 @@ const pngIcons = { 'lend-logo': lendLogo, 'ant-logo': antLogo, 'gol-logo': golemLogo, - 'node-logo': nodeLogo + 'node-logo': nodeLogo, + 'wallet-connect': walletConnectLogo }; type SvgIcons = keyof typeof svgIcons; @@ -322,6 +340,16 @@ const SStrokeIcon = styled(SInlineSVG)` stroke: ${({ color }) => color && color}; `; +const SStrokeFillIcon = styled(SInlineSVG)` + &&&& { + fill: ${({ color, theme }) => (color ? theme.colors[color] : color)}; + } + &&&&:hover { + fill: ${({ color, theme }) => (color ? theme.colors[color] : color)}; + } + stroke: ${({ color, theme }) => (color ? theme.colors[color] : color)}; +`; + const SExpandableIcon = styled(SInlineSVG)` cursor: pointer; transition: all 0.3s ease-out; @@ -366,8 +394,15 @@ export const isPNGType = (type: TIcon): type is PngIcons => export const getSVGIcon = (type: SvgIcons) => svgIcons[type]; const Icon = ({ type, color, ...props }: Props) => { - if (type === 'website' || type === 'faucet-icon' || type === 'nav-nft') { + if ( + type === 'website' || + type === 'faucet-icon' || + type === 'nav-nft' || + type === 'walletconnect-tag' + ) { return ; + } else if (type === 'other-tag') { + return ; } else if (type === 'expandable') { return ; } else if (type === 'sort') { diff --git a/src/components/WalletIcon.test.tsx b/src/components/WalletIcon.test.tsx new file mode 100644 index 00000000000..09bf44046fb --- /dev/null +++ b/src/components/WalletIcon.test.tsx @@ -0,0 +1,29 @@ +import { IWallet, wallets } from '@mycrypto/wallet-list'; +import { simpleRender } from 'test-utils'; + +import { TIcon } from './Icon'; +import { WalletIcon } from './WalletIcon'; + +const renderComponent = ({ wallet, interfaceIcon }: { wallet: IWallet; interfaceIcon?: TIcon }) => { + return simpleRender(); +}; + +describe('WalletIcon', () => { + it('can render a WalletIcon', () => { + const wallet = wallets[0]; + const props = { wallet: wallet }; + const { getByText, container } = renderComponent(props); + + expect(getByText(wallet.name, { exact: false })).toBeInTheDocument(); + expect(container.querySelector('img')).toBeInTheDocument(); + }); + + it('can render a WalletIcon with an interface icon', () => { + const wallet = wallets[0]; + const props = { wallet: wallet, interfaceIcon: 'wallet-connect' as TIcon }; + const { getByText, container } = renderComponent(props); + + expect(getByText(wallet.name, { exact: false })).toBeInTheDocument(); + expect(container.querySelector('div[data-testid="interface-icon"]')).toBeInTheDocument(); + }); +}); diff --git a/src/components/WalletIcon.tsx b/src/components/WalletIcon.tsx new file mode 100644 index 00000000000..17ef590a251 --- /dev/null +++ b/src/components/WalletIcon.tsx @@ -0,0 +1,46 @@ +import { defaultWallet, IWallet } from '@mycrypto/wallet-list'; + +import { Box, Icon, Text, TIcon } from '@components'; + +export const WalletIcon = ({ + wallet, + interfaceIcon +}: { + wallet: IWallet; + interfaceIcon?: TIcon; +}) => ( + + + + {wallet.name} + + {interfaceIcon && ( + + + + )} + +); diff --git a/src/components/WalletTag.test.tsx b/src/components/WalletTag.test.tsx new file mode 100644 index 00000000000..87f937a29bb --- /dev/null +++ b/src/components/WalletTag.test.tsx @@ -0,0 +1,19 @@ +import { WalletTags } from '@mycrypto/wallet-list'; +import { simpleRender } from 'test-utils'; + +import { translateRaw } from '@translations'; + +import { WalletTag } from './WalletTag'; + +const renderComponent = ({ tag }: { tag: WalletTags }) => { + return simpleRender(); +}; + +describe('WalletTag', () => { + test('it can render a WalletTag', () => { + const props = { tag: WalletTags.Desktop }; + const { getByText } = renderComponent(props); + + expect(getByText(translateRaw('WALLET_TAG_DESKTOP'), { exact: false })).toBeInTheDocument(); + }); +}); diff --git a/src/components/WalletTag.tsx b/src/components/WalletTag.tsx new file mode 100644 index 00000000000..925c6128350 --- /dev/null +++ b/src/components/WalletTag.tsx @@ -0,0 +1,17 @@ +import { WalletTags } from '@mycrypto/wallet-list'; + +import { Body, Box, Icon } from '@components'; +import { getWalletTag } from '@config'; + +export const WalletTag = ({ tag }: { tag: WalletTags }) => { + const tagConfig = getWalletTag(tag); + + return ( + + + + {tagConfig.text} + + + ); +}; diff --git a/src/components/WalletUnlock/ViewOnly.test.tsx b/src/components/WalletUnlock/ViewOnly.test.tsx new file mode 100644 index 00000000000..d39e27d9e99 --- /dev/null +++ b/src/components/WalletUnlock/ViewOnly.test.tsx @@ -0,0 +1,35 @@ +import { IWallet, wallets } from '@mycrypto/wallet-list'; +import { simpleRender } from 'test-utils'; + +import { translateRaw } from '@translations'; +import { FormData } from '@types'; + +import { ViewOnlyDecrypt } from './ViewOnly'; + +const defaultProps: React.ComponentProps = { + formData: ({ network: 'Ethereum' } as unknown) as FormData, + onUnlock: jest.fn() +}; + +const getComponent = ({ walletInfos }: { walletInfos?: IWallet }) => { + return simpleRender(); +}; + +describe('ViewOnly', () => { + it('render', () => { + const { getByText } = getComponent({}); + + expect(getByText(translateRaw('INPUT_PUBLIC_ADDRESS_LABEL'))).toBeInTheDocument(); + }); + + it('render with wallet infos', () => { + const wallet = wallets.find((wallet) => wallet.id === 'portis')!; + const props = { walletInfos: wallet }; + const { getByText, getAllByText } = getComponent(props); + + expect(getAllByText('Portis')).toBeTruthy(); + expect( + getByText(translateRaw('VIEW_ONLY_HEADING', { $wallet: wallet.name })) + ).toBeInTheDocument(); + }); +}); diff --git a/src/components/WalletUnlock/ViewOnly.tsx b/src/components/WalletUnlock/ViewOnly.tsx index 565d0a54a9c..220949418f4 100644 --- a/src/components/WalletUnlock/ViewOnly.tsx +++ b/src/components/WalletUnlock/ViewOnly.tsx @@ -1,10 +1,11 @@ import { useState } from 'react'; +import { IWallet } from '@mycrypto/wallet-list'; import { Form, Formik } from 'formik'; import equals from 'ramda/src/equals'; import styled from 'styled-components'; -import { Body, Box, Button, ContactLookupField, Heading } from '@components'; +import { Body, Box, Button, ContactLookupField, Heading, WalletIcon, WalletTag } from '@components'; import { getKBHelpArticle, KB_HELP_ARTICLE } from '@config'; import { useNetworks } from '@services/Store'; import { WalletFactory } from '@services/WalletService'; @@ -24,6 +25,7 @@ const ButtonWrapper = styled(Button)` interface Props { formData: FormData; onUnlock(param: any): void; + walletInfos?: IWallet; } const WalletService = WalletFactory[WalletId.VIEW_ONLY]; @@ -39,7 +41,7 @@ const initialFormikValues: FormValues = { } }; -export function ViewOnlyDecrypt({ formData, onUnlock }: Props) { +export function ViewOnlyDecrypt({ formData, onUnlock, walletInfos }: Props) { const { getNetworkById } = useNetworks(); const [isResolvingDomain, setIsResolvingDomain] = useState(false); const [network] = useState(getNetworkById(formData.network)); @@ -55,13 +57,25 @@ export function ViewOnlyDecrypt({ formData, onUnlock }: Props) { return ( + {walletInfos && ( + + + + {walletInfos.tags && walletInfos.tags.map((tag, i) => )} + + + )} - {translateRaw('INPUT_PUBLIC_ADDRESS_LABEL')} + {walletInfos + ? translateRaw('VIEW_ONLY_HEADING', { $wallet: walletInfos.name }) + : translateRaw('INPUT_PUBLIC_ADDRESS_LABEL')} - {translate('VIEW_ONLY_ADDR_DISCLAIMER', { - $link: getKBHelpArticle(KB_HELP_ARTICLE.HOW_DOES_VIEW_ADDRESS_WORK) - })} + {walletInfos + ? translateRaw('VIEW_ONLY_SUBHEADING') + : translate('VIEW_ONLY_ADDR_DISCLAIMER', { + $link: getKBHelpArticle(KB_HELP_ARTICLE.HOW_DOES_VIEW_ADDRESS_WORK) + })} {({ errors, touched, values, setFieldError, setFieldTouched, setFieldValue }) => ( @@ -76,6 +90,9 @@ export function ViewOnlyDecrypt({ formData, onUnlock }: Props) { setFieldValue={setFieldValue} setFieldTouched={setFieldTouched} setFieldError={setFieldError} + placeholder={ + walletInfos && translateRaw('VIEW_ONLY_PLACEHOLDER', { $wallet: walletInfos.name }) + } /> = { + useWalletConnectProps: ({ + state: { + detectedAddress: '0x0000' as TAddress, + requestConnection: jest.fn(), + signMessage: jest.fn(), + kill: jest.fn() + } + } as unknown) as IUseWalletConnect, + goToPreviousStep: jest.fn(), + onUnlock: jest.fn() +}; + +const getComponent = ({ walletInfos }: { walletInfos?: IWallet }) => { + return simpleRender(); +}; + +describe('WalletConnect', () => { + it('render', () => { + const { getByText } = getComponent({}); + + expect( + getByText( + translateRaw('SIGNER_SELECT_WALLETCONNECT', { + $walletId: translateRaw('X_WALLETCONNECT') + }) + ) + ).toBeInTheDocument(); + expect( + getByText( + translateRaw('SIGNER_SELECT_WALLET_QR', { $walletId: translateRaw('X_WALLETCONNECT') }) + ) + ).toBeInTheDocument(); + }); + + it('render with wallet infos', () => { + const wallet = wallets.find((wallet) => wallet.id === 'argent')!; + const props = { walletInfos: wallet }; + const { getByText, getAllByText } = getComponent(props); + + expect(getAllByText('Argent')).toBeTruthy(); + expect( + getByText(translateRaw('WALLET_CONNECT_HEADER', { $wallet: wallet.name })) + ).toBeInTheDocument(); + }); +}); diff --git a/src/components/WalletUnlock/WalletConnect.tsx b/src/components/WalletUnlock/WalletConnect.tsx index 312be64bb3f..a38f86e6641 100644 --- a/src/components/WalletUnlock/WalletConnect.tsx +++ b/src/components/WalletUnlock/WalletConnect.tsx @@ -1,9 +1,19 @@ import { useEffect } from 'react'; +import { IWallet } from '@mycrypto/wallet-list'; import isEmpty from 'ramda/src/isEmpty'; import styled, { css } from 'styled-components'; -import { BusyBottom, Button, Overlay, QRCodeContainer, Typography } from '@components'; +import { + Box, + BusyBottom, + Button, + Overlay, + QRCodeContainer, + Typography, + WalletIcon, + WalletTag +} from '@components'; import { IUseWalletConnect, WalletFactory } from '@services/WalletService'; import { BREAK_POINTS, COLORS, FONT_SIZE } from '@theme'; import translate, { translateRaw } from '@translations'; @@ -13,6 +23,7 @@ interface OwnProps { useWalletConnectProps: IUseWalletConnect; onUnlock(param: any): void; goToPreviousStep(): void; + walletInfos?: IWallet; } const SHeader = styled.div` @@ -65,7 +76,7 @@ const SContainer = styled.div` const WalletService = WalletFactory[WalletId.WALLETCONNECT]; -export function WalletConnectDecrypt({ onUnlock, useWalletConnectProps }: OwnProps) { +export function WalletConnectDecrypt({ onUnlock, useWalletConnectProps, walletInfos }: OwnProps) { const { state, requestConnection, signMessage, kill } = useWalletConnectProps; useEffect(() => { @@ -81,14 +92,26 @@ export function WalletConnectDecrypt({ onUnlock, useWalletConnectProps }: OwnPro return ( <> + {walletInfos && ( + + + + {walletInfos.tags && walletInfos.tags.map((tag, i) => )} + + + )} - {translateRaw('SIGNER_SELECT_WALLETCONNECT', { - $walletId: translateRaw('X_WALLETCONNECT') - })} + {walletInfos + ? translateRaw('WALLET_CONNECT_HEADER', { $wallet: walletInfos.name }) + : translateRaw('SIGNER_SELECT_WALLETCONNECT', { + $walletId: translateRaw('X_WALLETCONNECT') + })} - {translate('SIGNER_SELECT_WALLET_QR', { $walletId: translateRaw('X_WALLETCONNECT') })} + {walletInfos + ? translateRaw('WALLET_CONNECT_SUBHEADING', { $wallet: walletInfos.name }) + : translate('SIGNER_SELECT_WALLET_QR', { $walletId: translateRaw('X_WALLETCONNECT') })} diff --git a/src/components/index.ts b/src/components/index.ts index 9961e3cfc48..e155f3fb5dd 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -80,3 +80,5 @@ export { Switch } from './Switch'; export { BusyBottom } from './BusyBottom'; export { TransactionFeeEIP1559 } from './TransactionFeeEIP1559'; export { Identicon } from './Identicon'; +export { WalletIcon } from './WalletIcon'; +export { WalletTag } from './WalletTag'; diff --git a/src/config/index.ts b/src/config/index.ts index df8d57ae636..c08f378ba16 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -39,3 +39,4 @@ export * from './txTypes'; export { STATIC_CONTACTS } from './staticContacts'; export { getFiat } from './fiats'; export * from './poapPromos'; +export * from './walletTags'; diff --git a/src/config/routePaths.ts b/src/config/routePaths.ts index 8d76ebc070d..231c0002116 100644 --- a/src/config/routePaths.ts +++ b/src/config/routePaths.ts @@ -147,6 +147,11 @@ const PATHS: IRoutePath[] = [ name: 'NFT_DASHBOARD', title: 'NFT Dashboard', path: '/nft-dashboard' + }, + { + name: 'ONBOARDING', + title: 'Onboarding', + path: '/onboarding' } ]; diff --git a/src/config/walletTags.test.ts b/src/config/walletTags.test.ts new file mode 100644 index 00000000000..71b6cd6f252 --- /dev/null +++ b/src/config/walletTags.test.ts @@ -0,0 +1,44 @@ +import { WalletTags } from '@mycrypto/wallet-list'; + +import { translateRaw } from '@translations'; + +import { getWalletTag } from './walletTags'; + +describe('getWalletTag()', () => { + it('can return WalletTag config', () => { + expect(getWalletTag(WalletTags.Hardware)).toEqual({ + icon: 'hardware-tag', + text: translateRaw('WALLET_TAG_HARDWARE') + }); + + expect(getWalletTag(WalletTags.Web)).toEqual({ + icon: 'web-tag', + text: translateRaw('WALLET_TAG_WEB') + }); + + expect(getWalletTag(WalletTags.Mobile)).toEqual({ + icon: 'mobile-tag', + text: translateRaw('WALLET_TAG_MOBILE') + }); + + expect(getWalletTag(WalletTags.Desktop)).toEqual({ + icon: 'desktop-tag', + text: translateRaw('WALLET_TAG_DESKTOP') + }); + + expect(getWalletTag(WalletTags.Exchange)).toEqual({ + icon: 'exchange-tag', + text: translateRaw('WALLET_TAG_EXCHANGE') + }); + + expect(getWalletTag(WalletTags.WalletConnect)).toEqual({ + icon: 'walletconnect-tag', + text: translateRaw('WALLET_TAG_WALLET_CONNECT') + }); + + expect(getWalletTag(WalletTags.Other)).toEqual({ + icon: 'other-tag', + text: translateRaw('WALLET_TAG_OTHER') + }); + }); +}); diff --git a/src/config/walletTags.ts b/src/config/walletTags.ts new file mode 100644 index 00000000000..0e41d573f01 --- /dev/null +++ b/src/config/walletTags.ts @@ -0,0 +1,44 @@ +import { WalletTags } from '@mycrypto/wallet-list'; + +import { TIcon } from '@components'; +import { translateRaw } from '@translations'; + +export const getWalletTag = (tag: WalletTags): { icon: TIcon; text: string } => { + switch (tag) { + case WalletTags.Hardware: + return { + icon: 'hardware-tag', + text: translateRaw('WALLET_TAG_HARDWARE') + }; + case WalletTags.Web: + return { + icon: 'web-tag', + text: translateRaw('WALLET_TAG_WEB') + }; + case WalletTags.Mobile: + return { + icon: 'mobile-tag', + text: translateRaw('WALLET_TAG_MOBILE') + }; + case WalletTags.Desktop: + return { + icon: 'desktop-tag', + text: translateRaw('WALLET_TAG_DESKTOP') + }; + case WalletTags.Exchange: + return { + icon: 'exchange-tag', + text: translateRaw('WALLET_TAG_EXCHANGE') + }; + case WalletTags.WalletConnect: + return { + icon: 'walletconnect-tag', + text: translateRaw('WALLET_TAG_WALLET_CONNECT') + }; + case WalletTags.Other: + return { + icon: 'other-tag', + text: translateRaw('WALLET_TAG_OTHER') + }; + } +}; diff --git a/src/features/AddAccount/AddAccountFlow.tsx b/src/features/AddAccount/AddAccountFlow.tsx index 35ecc3d941f..2ece23745df 100644 --- a/src/features/AddAccount/AddAccountFlow.tsx +++ b/src/features/AddAccount/AddAccountFlow.tsx @@ -1,5 +1,6 @@ import { useEffect, useReducer, useState } from 'react'; +import { IWallet, WalletConnectivity } from '@mycrypto/wallet-list'; import { withRouter } from 'react-router-dom'; import { CSSTransition, TransitionGroup } from 'react-transition-group'; @@ -8,6 +9,7 @@ import { IWalletConfig, ROUTE_PATHS, WALLETS_CONFIG } from '@config'; import { useDispatch } from '@store'; import { addNewAccounts } from '@store/account.slice'; import { IStory, WalletId } from '@types'; +import { getFromWalletList, isValidWalletListId } from '@utils'; import { useUpdateEffect } from '@vendor'; import { formReducer, initialState } from './AddAccountForm.reducer'; @@ -30,6 +32,21 @@ export const isValidWalletId = (id: WalletId | string | undefined) => { return !!(id && Object.values(WalletId).includes(id as WalletId)); }; +export const getAccountTypeFromWallet = (wallet: IWallet) => { + switch (wallet.connectivity) { + case WalletConnectivity.WalletConnect: + return WalletId.WALLETCONNECT; + case WalletConnectivity.ViewOnly: + case WalletConnectivity.MigrateNonCustodial: + return WalletId.VIEW_ONLY; + case WalletConnectivity.Web3: + return WalletId.WEB3; + case WalletConnectivity.Ledger: + return WalletId.LEDGER_NANO_S_NEW; + case WalletConnectivity.Trezor: + return WalletId.TREZOR_NEW; + } +}; /* Flow to add an account to database. The default view of the component displays a list of wallets (e.g. stories) that each posses their own number @@ -39,6 +56,7 @@ export const isValidWalletId = (id: WalletId | string | undefined) => { */ const AddAccountFlow = withRouter(({ history, match }) => { const dispatch = useDispatch(); + const [wallet, setWallet] = useState(); const [step, setStep] = useState(0); // The current Step inside the Wallet Story. const [formData, updateFormState] = useReducer(formReducer, initialState); // The data that we want to save at the end. @@ -60,13 +78,22 @@ const AddAccountFlow = withRouter(({ history, match }) => { const { walletId } = match.params; // Read the walletName parameter from the URL if (!walletId) { return; - } else if (!isValidWalletId(walletId.toUpperCase())) { + } else if (!isValidWalletId(walletId.toUpperCase()) && !isValidWalletListId(walletId)) { goToStart(); } else if (walletId.toUpperCase() !== storyName) { - updateFormState({ - type: ActionType.SELECT_ACCOUNT_TYPE, - payload: { accountType: walletId.toUpperCase() } - }); + const selectedWallet = getFromWalletList(walletId); + if (selectedWallet) { + setWallet(selectedWallet); + updateFormState({ + type: ActionType.SELECT_ACCOUNT_TYPE, + payload: { accountType: getAccountTypeFromWallet(selectedWallet) } + }); + } else { + updateFormState({ + type: ActionType.SELECT_ACCOUNT_TYPE, + payload: { accountType: walletId.toUpperCase() } + }); + } } }, [match.params]); @@ -123,6 +150,7 @@ const AddAccountFlow = withRouter(({ history, match }) => { { }); }); +describe('getAccountTypeFromWallet()', () => { + it('returns correct WalletId', () => { + const walletConnectWallet = wallets.find( + (wallet) => wallet.connectivity === WalletConnectivity.WalletConnect + ); + const viewOnlyWallet = wallets.find( + (wallet) => wallet.connectivity === WalletConnectivity.ViewOnly + ); + const web3Wallet = wallets.find((wallet) => wallet.connectivity === WalletConnectivity.Web3); + const ledgerWallet = wallets.find( + (wallet) => wallet.connectivity === WalletConnectivity.Ledger + ); + const trezorWallet = wallets.find( + (wallet) => wallet.connectivity === WalletConnectivity.Trezor + ); + expect(getAccountTypeFromWallet(walletConnectWallet!)).toEqual(WalletId.WALLETCONNECT); + expect(getAccountTypeFromWallet(viewOnlyWallet!)).toEqual(WalletId.VIEW_ONLY); + expect(getAccountTypeFromWallet(web3Wallet!)).toEqual(WalletId.WEB3); + expect(getAccountTypeFromWallet(ledgerWallet!)).toEqual(WalletId.LEDGER_NANO_S_NEW); + expect(getAccountTypeFromWallet(trezorWallet!)).toEqual(WalletId.TREZOR_NEW); + }); +}); + /* Test components */ describe('AddAccountFlow', () => { let history: any; diff --git a/src/features/Onboarding/OnboardingFlow.tsx b/src/features/Onboarding/OnboardingFlow.tsx new file mode 100644 index 00000000000..59e0df796df --- /dev/null +++ b/src/features/Onboarding/OnboardingFlow.tsx @@ -0,0 +1,46 @@ +import { useEffect, useState } from 'react'; + +import { IWallet, WalletConnectivity } from '@mycrypto/wallet-list'; +import { useHistory, useRouteMatch } from 'react-router'; + +import { AppLoading, ExtendedContentPanel } from '@components'; +import { ROUTE_PATHS } from '@config'; +import { getFromWalletList, isValidWalletListId } from '@utils'; + +import { Migrate } from './components'; + +export const isMigratableWallet = (wallet: IWallet) => + wallet.connectivity === WalletConnectivity.MigrateCustodial || + wallet.connectivity === WalletConnectivity.MigrateNonCustodial; + +const OnboardingFlow = () => { + const [wallet, setWallet] = useState(); + const history = useHistory(); + const match = useRouteMatch<{ walletId: string | undefined }>(); + + useEffect(() => { + const { walletId } = match.params; + if (!walletId) { + return; + } else if (!isValidWalletListId(walletId)) { + history.replace(`${ROUTE_PATHS.ADD_ACCOUNT.path}`); + } else { + const selectedWallet = getFromWalletList(walletId); + if (selectedWallet && isMigratableWallet(selectedWallet)) { + setWallet(selectedWallet); + } else if (selectedWallet && !isMigratableWallet(selectedWallet)) { + history.replace(`${ROUTE_PATHS.ADD_ACCOUNT.path}/${selectedWallet.id}`); + } else { + history.replace(`${ROUTE_PATHS.ADD_ACCOUNT.path}`); + } + } + }, [match.params]); + + return ( + + {wallet ? : } + + ); +}; + +export default OnboardingFlow; diff --git a/src/features/Onboarding/__tests__/Migrate.test.tsx b/src/features/Onboarding/__tests__/Migrate.test.tsx new file mode 100644 index 00000000000..6e817df56aa --- /dev/null +++ b/src/features/Onboarding/__tests__/Migrate.test.tsx @@ -0,0 +1,26 @@ +import { IWallet, wallets } from '@mycrypto/wallet-list'; +import { simpleRender } from 'test-utils'; + +import { translateRaw } from '@translations'; + +import Migrate from '../components/Migrate'; + +const renderComponent = ({ walletInfos }: { walletInfos: IWallet }) => { + return simpleRender(); +}; + +describe('Migrate', () => { + it('Can render a Migration guide', () => { + const wallet = wallets.find((wallet) => wallet.id === 'kraken')!; + const props = { walletInfos: wallet }; + const { getAllByText, getByText } = renderComponent(props); + + expect(getAllByText(wallet.name, { exact: false })).toBeTruthy(); + expect(getByText(translateRaw('WALLET_TAG_EXCHANGE'), { exact: false })).toBeInTheDocument(); + expect( + getByText(translateRaw('MIGRATE_GET_HELP_LINK', { $exchange: wallet.name }), { + exact: false + }) + ).toBeInTheDocument(); + }); +}); diff --git a/src/features/Onboarding/__tests__/config.test.ts b/src/features/Onboarding/__tests__/config.test.ts new file mode 100644 index 00000000000..d267cbd950c --- /dev/null +++ b/src/features/Onboarding/__tests__/config.test.ts @@ -0,0 +1,18 @@ +import { WalletConnectivity, wallets } from '@mycrypto/wallet-list'; + +import { getMigrationGuide } from '../config'; + +describe('getMigrationGuide()', () => { + it('can return migration config', () => { + const custodialExchange = wallets.find( + (wallet) => wallet.connectivity === WalletConnectivity.MigrateCustodial + ); + + const nonCustodialExchange = wallets.find( + (wallet) => wallet.connectivity === WalletConnectivity.MigrateNonCustodial + ); + + expect(getMigrationGuide(custodialExchange!)).toBeTruthy(); + expect(getMigrationGuide(nonCustodialExchange!)).toBeTruthy(); + }); +}); diff --git a/src/features/Onboarding/components/Migrate.tsx b/src/features/Onboarding/components/Migrate.tsx new file mode 100644 index 00000000000..074f9faccb4 --- /dev/null +++ b/src/features/Onboarding/components/Migrate.tsx @@ -0,0 +1,63 @@ +import { IWallet } from '@mycrypto/wallet-list'; + +import { Body, Box, Button, Heading, LinkApp, WalletIcon, WalletTag } from '@components'; +import { translateRaw } from '@translations'; + +import { getMigrationGuide } from '../config'; + +const Migrate = ({ walletInfos }: { walletInfos: IWallet }) => { + const config = getMigrationGuide(walletInfos)!; + + return ( + + + + + {walletInfos.tags && walletInfos.tags.map((tag, i) => )} + + + + {translateRaw('MIGRATE_HEADING', { $exchange: walletInfos.name })} + + + {config.subheading} + + + + + + + + + {translateRaw('MIGRATE_HOW_TO_TRANSFER')} + + {config.steps.map((step, i) => ( + + + {`0${i + 1}`} + + {step} + + ))} + + + + + + + {walletInfos.urls.support && ( + + {translateRaw('MIGRATE_GET_HELP_TEXT')}{' '} + + {translateRaw('MIGRATE_GET_HELP_LINK', { $exchange: walletInfos.name })} + + + )} + + + ); +}; + +export default Migrate; diff --git a/src/features/Onboarding/components/index.ts b/src/features/Onboarding/components/index.ts new file mode 100644 index 00000000000..548f24291b9 --- /dev/null +++ b/src/features/Onboarding/components/index.ts @@ -0,0 +1 @@ +export { default as Migrate } from './Migrate'; diff --git a/src/features/Onboarding/config.ts b/src/features/Onboarding/config.ts new file mode 100644 index 00000000000..29b0223bc08 --- /dev/null +++ b/src/features/Onboarding/config.ts @@ -0,0 +1,49 @@ +import { IWallet, WalletConnectivity } from '@mycrypto/wallet-list'; + +import { ROUTE_PATHS } from '@config'; +import { translateRaw } from '@translations'; + +export const getMigrationGuide = (wallet: IWallet) => { + if (wallet.connectivity === WalletConnectivity.MigrateCustodial) + return { + subheading: translateRaw('MIGRATE_CUSTODIAL_SUBHEADING', { $exchange: wallet.name }), + topButton: { + to: ROUTE_PATHS.DOWNLOAD_DESKTOP_APP.path, + text: translateRaw('BUSY_BOTTOM_GENERAL_1') + }, + primaryButton: { + to: ROUTE_PATHS.DOWNLOAD_DESKTOP_APP.path, + text: translateRaw('BUSY_BOTTOM_GENERAL_1') + }, + secondaryButton: { + to: ROUTE_PATHS.ADD_ACCOUNT.path, + text: translateRaw('MIGRATE_CONNECT_ACCOUNT') + }, + steps: [ + translateRaw('MIGRATE_CUSTODIAL_01'), + translateRaw('MIGRATE_CUSTODIAL_02', { $exchange: wallet.name }), + translateRaw('MIGRATE_CUSTODIAL_03') + ] + }; + if (wallet.connectivity === WalletConnectivity.MigrateNonCustodial) + return { + subheading: translateRaw('MIGRATE_NON_CUSTODIAL_SUBHEADING', { $exchange: wallet.name }), + topButton: { + to: `${ROUTE_PATHS.ADD_ACCOUNT.path}/${wallet.id}`, + text: translateRaw('MIGRATE_NON_CUSTODIAL_TOP_BUTTON', { $exchange: wallet.name }) + }, + primaryButton: { + to: ROUTE_PATHS.DOWNLOAD_DESKTOP_APP.path, + text: translateRaw('BUSY_BOTTOM_GENERAL_1') + }, + secondaryButton: { + to: ROUTE_PATHS.ADD_ACCOUNT.path, + text: translateRaw('MIGRATE_CONNECT_ACCOUNT') + }, + steps: [ + translateRaw('MIGRATE_NON_CUSTODIAL_01', { $exchange: wallet.name }), + translateRaw('MIGRATE_NON_CUSTODIAL_02'), + translateRaw('MIGRATE_NON_CUSTODIAL_03', { $exchange: wallet.name }) + ] + }; +}; diff --git a/src/features/Onboarding/index.ts b/src/features/Onboarding/index.ts new file mode 100644 index 00000000000..17e3f57a7ab --- /dev/null +++ b/src/features/Onboarding/index.ts @@ -0,0 +1 @@ +export { default as OnboardingFlow } from './OnboardingFlow'; diff --git a/src/routing/routes.tsx b/src/routing/routes.tsx index 870d5f93346..d7a083daec3 100644 --- a/src/routing/routes.tsx +++ b/src/routing/routes.tsx @@ -95,6 +95,10 @@ const GolemTokenMigration = lazy(() => import(/* webpackChunkName: "TokenMigration" */ '@features/GolemTokenMigration') ); +const OnboardingFlow = lazy(() => + import(/* webpackChunkName: "Onboarding" */ '@features/Onboarding/OnboardingFlow') +); + export interface IAppRoutes { [K: string]: IAppRoute; } @@ -329,6 +333,14 @@ export const getStaticAppRoutes = (featureFlags: FeatureFlags): IAppRoute[] => [ requireAccounts: true, enabled: true, component: NftDashboard + }, + { + name: ROUTE_PATHS.ONBOARDING.name, + title: ROUTE_PATHS.ONBOARDING.title, + path: `${ROUTE_PATHS.ONBOARDING.path}/:walletId?`, + enabled: true, + exact: true, + component: OnboardingFlow } ]; diff --git a/src/translations/lang/en.json b/src/translations/lang/en.json index 7c49c42715f..7554d4ddbe4 100644 --- a/src/translations/lang/en.json +++ b/src/translations/lang/en.json @@ -1078,6 +1078,32 @@ "MEMBERSHIP_POAP": "Regular POAP NFT drops", "MEMBERSHIP_TREZOR": "10% Discount for Trezor Hardware Wallets", "MEMBERSHIP_FAUCET": "Extra Faucet Testnet ETH (Rinkeby, Ropsten, Kovan, Goerli)", - "MEMBERSHIP_CITADEL": "Access to the exclusive MyCrypto Citadel" + "MEMBERSHIP_CITADEL": "Access to the exclusive MyCrypto Citadel", + "VIEW_ONLY_HEADING": "We’re not fully integrated with $wallet yet", + "VIEW_ONLY_SUBHEADING": "You can still see your assets in view-only mode on your MyCrypto dashboard for now", + "VIEW_ONLY_PLACEHOLDER": "Enter your $wallet address", + "WALLET_CONNECT_HEADER": "Connect to $wallet via WalletConnect", + "WALLET_CONNECT_SUBHEADING": "Open your $wallet app and scan the WalletConnect QR code below.", + "MIGRATE_HEADING": "Migrate from $exchange", + "MIGRATE_CONNECT_ACCOUNT": "Connect Account", + "MIGRATE_CUSTODIAL_SUBHEADING": "$exchange is a custodial service, meaning they hold your crypto on your behalf. If you wish to take full control of your assets, MyCrypto will help you get there. You'll start by creating a new MyCrypto account, and then you'll migrate your crypto off of $exchange and to that new MyCrypto account. We'll walk you through it:", + "MIGRATE_CUSTODIAL_01": "Click below to create a new MyCrypto account or connect an existing account. Copy the account address to your clipboard.", + "MIGRATE_CUSTODIAL_02": "Login to your $exchange account and safely follow the steps for withdrawing your cryptocurrency. Make sure to only select cryptocurrencies supported by MyCrypto.", + "MIGRATE_CUSTODIAL_03": "When the time comes to insert a withdrawal address, input the address you created/connected in step one. Verify that it matches, and then continue with the withdrawal and send your cryptocurrency to that address.", + "MIGRATE_NON_CUSTODIAL_SUBHEADING": "Tracking your account is limited and you cannot take action on that account. To manage your $exchange assets on MyCrypto, you'll need to migrate.", + "MIGRATE_NON_CUSTODIAL_TOP_BUTTON": "Track your $exchange account", + "MIGRATE_NON_CUSTODIAL_01": "Find your $exchange recovery phrase within the $exchange app.", + "MIGRATE_NON_CUSTODIAL_02": "Download and install the MyCrypto Desktop App or the MyCrypto Signer App.", + "MIGRATE_NON_CUSTODIAL_03": "Click Add Account in the MyCrypto application that you've installed. Then select \"Mnemonic Phrase\" and enter the key you obtained from $exchange", + "MIGRATE_GET_HELP_TEXT": "Not sure where to start?", + "MIGRATE_GET_HELP_LINK": "Get migration help for $exchange", + "MIGRATE_HOW_TO_TRANSFER": "How to Transfer", + "WALLET_TAG_HARDWARE": "Hardware Wallet", + "WALLET_TAG_WEB": "Browser-Based", + "WALLET_TAG_MOBILE": "Mobile", + "WALLET_TAG_DESKTOP": "Desktop", + "WALLET_TAG_EXCHANGE": "Exchange", + "WALLET_TAG_WALLET_CONNECT": "via Wallet Connect", + "WALLET_TAG_OTHER": "Other" } } diff --git a/src/utils/wallets.ts b/src/utils/wallets.ts index b71ba1dec12..db5cdea0b38 100644 --- a/src/utils/wallets.ts +++ b/src/utils/wallets.ts @@ -1,3 +1,5 @@ +import { wallets } from '@mycrypto/wallet-list'; + import { NetworkId, StoreAccount, TAddress, WalletId } from '@types'; import { isSameAddress } from '@utils'; @@ -30,3 +32,7 @@ export const isSenderAccountPresent = ( accounts.some( ({ address, wallet }) => isSameAddress(address, addressToCheck) && !isViewOnlyWallet(wallet) ); + +export const isValidWalletListId = (id: string) => !!wallets.find((wallet) => wallet.id === id); + +export const getFromWalletList = (id: string) => wallets.find((wallet) => wallet.id === id); diff --git a/yarn.lock b/yarn.lock index 30994821332..d6cac27f3ad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3091,6 +3091,13 @@ dependencies: "@findeth/abi" "^0.7.1" +"@mycrypto/wallet-list@1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@mycrypto/wallet-list/-/wallet-list-1.3.0.tgz#c45cda8908775fa0378ae0259dfad6ac9e88129c" + integrity sha512-d+vinjhOnIGjWzjP99nOXam5pxGV9YEn4t+QQSZyZ22YCNbvRjOvrXXjShBUzZtLDgmTAcyMCIoWOrwzk06ktQ== + dependencies: + remove "^0.1.5" + "@mycrypto/wallets@1.3.4": version "1.3.4" resolved "https://registry.yarnpkg.com/@mycrypto/wallets/-/wallets-1.3.4.tgz#2189c4a86fe907366d49984d49ba0030b73b64b6" @@ -8119,6 +8126,13 @@ chai@^4.1.2: pathval "^1.1.0" type-detect "^4.0.5" +"chainsaw@>=0.0.7 <0.1": + version "0.0.9" + resolved "https://registry.yarnpkg.com/chainsaw/-/chainsaw-0.0.9.tgz#11a05102d1c4c785b6d0415d336d5a3a1612913e" + integrity sha1-EaBRAtHEx4W20EFdM21aOhYSkT4= + dependencies: + traverse ">=0.3.0 <0.4" + chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.0, chalk@^2.4.0, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -12385,6 +12399,13 @@ hash.js@1.1.7, hash.js@^1.0.0, hash.js@^1.0.3, hash.js@^1.1.7: inherits "^2.0.3" minimalistic-assert "^1.0.1" +"hashish@>=0.0.2 <0.1": + version "0.0.4" + resolved "https://registry.yarnpkg.com/hashish/-/hashish-0.0.4.tgz#6d60bc6ffaf711b6afd60e426d077988014e6554" + integrity sha1-bWC8b/r3Ebav1g5CbQd5iAFOZVQ= + dependencies: + traverse ">=0.2.4" + hast-to-hyperscript@^9.0.0: version "9.0.1" resolved "https://registry.yarnpkg.com/hast-to-hyperscript/-/hast-to-hyperscript-9.0.1.tgz#9b67fd188e4c81e8ad66f803855334173920218d" @@ -19040,6 +19061,13 @@ remove-trailing-separator@^1.0.1: resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= +remove@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/remove/-/remove-0.1.5.tgz#095ffd827d65c9f41ad97d33e416a75811079955" + integrity sha1-CV/9gn1lyfQa2X0z5BanWBEHmVU= + dependencies: + seq ">= 0.3.5" + renderkid@^2.0.4: version "2.0.5" resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.5.tgz#483b1ac59c6601ab30a7a596a5965cabccfdd0a5" @@ -19655,6 +19683,14 @@ send@0.17.1: range-parser "~1.2.1" statuses "~1.5.0" +"seq@>= 0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/seq/-/seq-0.3.5.tgz#ae02af3a424793d8ccbf212d69174e0c54dffe38" + integrity sha1-rgKvOkJHk9jMvyEtaRdODFTf/jg= + dependencies: + chainsaw ">=0.0.7 <0.1" + hashish ">=0.0.2 <0.1" + serialize-javascript@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61" @@ -21335,6 +21371,16 @@ tr46@^2.0.2: dependencies: punycode "^2.1.1" +traverse@>=0.2.4: + version "0.6.6" + resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137" + integrity sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc= + +"traverse@>=0.3.0 <0.4": + version "0.3.9" + resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" + integrity sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk= + tree-kill@^1.1.0, tree-kill@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"