From 5c9a9fa2041eb69f2f2cca9bdab95f6f3a72541a Mon Sep 17 00:00:00 2001 From: Rogue Alexander Date: Sat, 10 Apr 2021 20:00:46 +0200 Subject: [PATCH 1/6] feature(ChannelWallet): Integrate into header --- src/components/ChannelWalletStatus/index.tsx | 192 +++++++++++++++++++ src/components/Header/index.tsx | 4 + src/state/user/actions.ts | 2 + src/state/user/hooks.tsx | 15 +- src/state/user/reducer.ts | 9 +- 5 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 src/components/ChannelWalletStatus/index.tsx diff --git a/src/components/ChannelWalletStatus/index.tsx b/src/components/ChannelWalletStatus/index.tsx new file mode 100644 index 0000000..52a6168 --- /dev/null +++ b/src/components/ChannelWalletStatus/index.tsx @@ -0,0 +1,192 @@ +import { useChannelWalletReact } from '@channel-wallet-react/core' +import { darken, lighten } from 'polished' +import React, { useMemo } from 'react' +import { Activity } from 'react-feather' +import { useTranslation } from 'react-i18next' +import styled, { css } from 'styled-components' +import { NetworkContextName } from '../../constants' +import useENSName from '../../hooks/useENSName' +import { useHasSocks } from '../../hooks/useSocksBalance' +import { useWalletModalToggle } from '../../state/application/hooks' +import { isTransactionRecent, useAllTransactions } from '../../state/transactions/hooks' +import { TransactionDetails } from '../../state/transactions/reducer' +import { shortenAddress } from '../../utils' +import { ButtonSecondary } from '../Button' + +import Loader from '../Loader' + +import { RowBetween } from '../Row' +import WalletModal from '../WalletModal' + + +const ChannelWalletStatusGeneric = styled(ButtonSecondary)` + ${({ theme }) => theme.flexRowNoWrap} + width: 100%; + align-items: center; + padding: 0.5rem; + border-radius: 12px; + cursor: pointer; + user-select: none; + :focus { + outline: none; + } +` +const ChannelWalletStatusError = styled(ChannelWalletStatusGeneric)` + background-color: ${({ theme }) => theme.red1}; + border: 1px solid ${({ theme }) => theme.red1}; + color: ${({ theme }) => theme.white}; + font-weight: 500; + :hover, + :focus { + background-color: ${({ theme }) => darken(0.1, theme.red1)}; + } +` + +const ChannelWalletStatusConnect = styled(ChannelWalletStatusGeneric)<{ faded?: boolean }>` + background-color: ${({ theme }) => theme.primary4}; + border: none; + color: ${({ theme }) => theme.primaryText1}; + font-weight: 500; + + :hover, + :focus { + border: 1px solid ${({ theme }) => darken(0.05, theme.primary4)}; + color: ${({ theme }) => theme.primaryText1}; + } + + ${({ faded }) => + faded && + css` + background-color: ${({ theme }) => theme.primary5}; + border: 1px solid ${({ theme }) => theme.primary5}; + color: ${({ theme }) => theme.primaryText1}; + + :hover, + :focus { + border: 1px solid ${({ theme }) => darken(0.05, theme.primary4)}; + color: ${({ theme }) => darken(0.05, theme.primaryText1)}; + } + `} +` + +const ChannelWalletStatusConnected = styled(ChannelWalletStatusGeneric)<{ pending?: boolean }>` + background-color: ${({ pending, theme }) => (pending ? theme.primary1 : theme.bg2)}; + border: 1px solid ${({ pending, theme }) => (pending ? theme.primary1 : theme.bg3)}; + color: ${({ pending, theme }) => (pending ? theme.white : theme.text1)}; + font-weight: 500; + :hover, + :focus { + background-color: ${({ pending, theme }) => (pending ? darken(0.05, theme.primary1) : lighten(0.05, theme.bg2))}; + + :focus { + border: 1px solid ${({ pending, theme }) => (pending ? darken(0.1, theme.primary1) : darken(0.1, theme.bg3))}; + } + } +` + +const Text = styled.p` + flex: 1 1 auto; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin: 0 0.5rem 0 0.25rem; + font-size: 1rem; + width: fit-content; + font-weight: 500; +` + +const NetworkIcon = styled(Activity)` + margin-left: 0.25rem; + margin-right: 0.5rem; + width: 16px; + height: 16px; +` + +// we want the latest one to come first, so return negative if a is after b +function newTransactionsFirst(a: TransactionDetails, b: TransactionDetails) { + return b.addedTime - a.addedTime +} + +const SOCK = ( + + 🧦 + +) + +function ChannelWalletStatusInner() { + const { t } = useTranslation() + const { account, error } = useChannelWalletReact() + + const { ENSName } = useENSName(account ?? undefined) + + const allTransactions = useAllTransactions() + + const sortedRecentTransactions = useMemo(() => { + const txs = Object.values(allTransactions) + return txs.filter(isTransactionRecent).sort(newTransactionsFirst) + }, [allTransactions]) + + const pending = sortedRecentTransactions.filter(tx => !tx.receipt).map(tx => tx.hash) + + const hasPendingTransactions = !!pending.length + const hasSocks = useHasSocks() + const toggleWalletModal = useWalletModalToggle() + + if (account) { + return ( + + {hasPendingTransactions ? ( + + {pending?.length} Pending + + ) : ( + <> + {hasSocks ? SOCK : null} + {ENSName || shortenAddress(account)} + + )} + + ) + } else if (error) { + return ( + + + {'Error'} + + ) + } else { + return ( + + {t('Create Wallet')} + + ) + } +} + +export default function ChannelWalletStatus() { + const { active, account } = useChannelWalletReact() + const contextNetwork = useChannelWalletReact(NetworkContextName) + + const { ENSName } = useENSName(account ?? undefined) + + const allTransactions = useAllTransactions() + + const sortedRecentTransactions = useMemo(() => { + const txs = Object.values(allTransactions) + return txs.filter(isTransactionRecent).sort(newTransactionsFirst) + }, [allTransactions]) + + const pending = sortedRecentTransactions.filter(tx => !tx.receipt).map(tx => tx.hash) + const confirmed = sortedRecentTransactions.filter(tx => tx.receipt).map(tx => tx.hash) + + if (!contextNetwork.active && !active) { + return null + } + + return ( + <> + + + + ) +} diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx index 8c62666..8b0abe8 100644 --- a/src/components/Header/index.tsx +++ b/src/components/Header/index.tsx @@ -360,6 +360,7 @@ export default function Header() { {aggregateBalance && ( setShowUniBalanceModal(true)}> + {/* TODO: The usd accumulator will be polled against the stakenet api */} + + + diff --git a/src/state/user/actions.ts b/src/state/user/actions.ts index f6701c3..e51bc7a 100644 --- a/src/state/user/actions.ts +++ b/src/state/user/actions.ts @@ -13,6 +13,8 @@ export interface SerializedPair { token1: SerializedToken } +export const updateChannelWalletExists = createAction<{ channelWalletExists: boolean}>('user/updateChannelWalletExists') + export const updateMatchesDarkMode = createAction<{ matchesDarkMode: boolean }>('user/updateMatchesDarkMode') export const updateUserDarkMode = createAction<{ userDarkMode: boolean }>('user/updateUserDarkMode') export const updateUserExpertMode = createAction<{ userExpertMode: boolean }>('user/updateUserExpertMode') diff --git a/src/state/user/hooks.tsx b/src/state/user/hooks.tsx index 828ad8a..1773c17 100644 --- a/src/state/user/hooks.tsx +++ b/src/state/user/hooks.tsx @@ -2,7 +2,7 @@ import { ChainId, Pair, Token } from '@uniswap/sdk' import flatMap from 'lodash.flatmap' import ReactGA from 'react-ga' import { useCallback, useMemo } from 'react' -import { useDispatch, useSelector } from 'react-redux' +import { shallowEqual, useDispatch, useSelector } from 'react-redux' import { BASES_TO_TRACK_LIQUIDITY_FOR, PINNED_PAIRS } from '../../constants' import { useActiveWeb3React } from '../../hooks' @@ -42,6 +42,19 @@ function deserializeToken(serializedToken: SerializedToken): Token { ) } +export function useChannelWalletState(): boolean { + const { channelWalletExists } = useSelector< + AppState, + { channelWalletExists: boolean } + >( + ({ user: { channelWalletExists } }) => ({ + channelWalletExists, + }), + shallowEqual + ) + return channelWalletExists +} + export function useIsDarkMode(): boolean { return true // const { userDarkMode, matchesDarkMode } = useSelector< diff --git a/src/state/user/reducer.ts b/src/state/user/reducer.ts index c0dd22a..38efe32 100644 --- a/src/state/user/reducer.ts +++ b/src/state/user/reducer.ts @@ -14,13 +14,15 @@ import { updateUserSlippageTolerance, updateUserDeadline, toggleURLWarning, - updateUserSingleHopOnly + updateUserSingleHopOnly, + updateChannelWalletExists } from './actions' const currentTimestamp = () => new Date().getTime() export interface UserState { // the timestamp of the last updateVersion action + channelWalletExists: boolean lastUpdateVersionTimestamp?: number userDarkMode: boolean | null // the user's choice for dark mode or light mode @@ -58,6 +60,7 @@ function pairKey(token0Address: string, token1Address: string) { } export const initialState: UserState = { + channelWalletExists: false, userDarkMode: null, matchesDarkMode: false, userExpertMode: false, @@ -72,6 +75,10 @@ export const initialState: UserState = { export default createReducer(initialState, builder => builder + .addCase(updateChannelWalletExists, (state, action) => { + state.channelWalletExists = action.payload.channelWalletExists, + state.timestamp = currentTimestamp() + }) .addCase(updateVersion, state => { // slippage isnt being tracked in local storage, reset to default // noinspection SuspiciousTypeOfGuard From bafc4885d670ee36ef2442e294112afe10aa5653 Mon Sep 17 00:00:00 2001 From: Rogue Alexander Date: Sat, 10 Apr 2021 20:16:01 +0200 Subject: [PATCH 2/6] feature(ChannelWallet): Update channel wallet state to match web3 state --- src/components/ChannelWalletStatus/index.tsx | 21 +++++++++----------- src/state/user/actions.ts | 3 ++- src/state/user/hooks.tsx | 13 ++++++------ 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/components/ChannelWalletStatus/index.tsx b/src/components/ChannelWalletStatus/index.tsx index 52a6168..9ef3aac 100644 --- a/src/components/ChannelWalletStatus/index.tsx +++ b/src/components/ChannelWalletStatus/index.tsx @@ -1,10 +1,9 @@ -import { useChannelWalletReact } from '@channel-wallet-react/core' import { darken, lighten } from 'polished' import React, { useMemo } from 'react' import { Activity } from 'react-feather' import { useTranslation } from 'react-i18next' +import { useChannelWalletState } from 'state/user/hooks' import styled, { css } from 'styled-components' -import { NetworkContextName } from '../../constants' import useENSName from '../../hooks/useENSName' import { useHasSocks } from '../../hooks/useSocksBalance' import { useWalletModalToggle } from '../../state/application/hooks' @@ -115,9 +114,9 @@ const SOCK = ( function ChannelWalletStatusInner() { const { t } = useTranslation() - const { account, error } = useChannelWalletReact() + const { address, error } = useChannelWalletState() - const { ENSName } = useENSName(account ?? undefined) + const { ENSName } = useENSName(address ?? undefined) const allTransactions = useAllTransactions() @@ -132,7 +131,7 @@ function ChannelWalletStatusInner() { const hasSocks = useHasSocks() const toggleWalletModal = useWalletModalToggle() - if (account) { + if (address) { return ( {hasPendingTransactions ? ( @@ -142,7 +141,7 @@ function ChannelWalletStatusInner() { ) : ( <> {hasSocks ? SOCK : null} - {ENSName || shortenAddress(account)} + {ENSName || shortenAddress(address)} )} @@ -156,7 +155,7 @@ function ChannelWalletStatusInner() { ) } else { return ( - + {t('Create Wallet')} ) @@ -164,10 +163,8 @@ function ChannelWalletStatusInner() { } export default function ChannelWalletStatus() { - const { active, account } = useChannelWalletReact() - const contextNetwork = useChannelWalletReact(NetworkContextName) - - const { ENSName } = useENSName(account ?? undefined) + const { active, address } = useChannelWalletState() + const { ENSName } = useENSName(address ?? undefined) const allTransactions = useAllTransactions() @@ -179,7 +176,7 @@ export default function ChannelWalletStatus() { const pending = sortedRecentTransactions.filter(tx => !tx.receipt).map(tx => tx.hash) const confirmed = sortedRecentTransactions.filter(tx => tx.receipt).map(tx => tx.hash) - if (!contextNetwork.active && !active) { + if (!active) { return null } diff --git a/src/state/user/actions.ts b/src/state/user/actions.ts index e51bc7a..edaca91 100644 --- a/src/state/user/actions.ts +++ b/src/state/user/actions.ts @@ -13,7 +13,8 @@ export interface SerializedPair { token1: SerializedToken } -export const updateChannelWalletExists = createAction<{ channelWalletExists: boolean}>('user/updateChannelWalletExists') +export const updateChannelWalletAddress = createAction<{ channelWalletAddress?: string }>('user/updateChannelWalletAddress') +export const updateChannelWalletError = createAction<{ channelWalletError?: Error }>('user/updateChannelWalletError') export const updateMatchesDarkMode = createAction<{ matchesDarkMode: boolean }>('user/updateMatchesDarkMode') export const updateUserDarkMode = createAction<{ userDarkMode: boolean }>('user/updateUserDarkMode') diff --git a/src/state/user/hooks.tsx b/src/state/user/hooks.tsx index 1773c17..bda1505 100644 --- a/src/state/user/hooks.tsx +++ b/src/state/user/hooks.tsx @@ -42,17 +42,18 @@ function deserializeToken(serializedToken: SerializedToken): Token { ) } -export function useChannelWalletState(): boolean { - const { channelWalletExists } = useSelector< +export function useChannelWalletState(): { address: string | undefined, error: Error | undefined } { + const { channelWalletAddress, channelWalletError } = useSelector< AppState, - { channelWalletExists: boolean } + { channelWalletAddress: string | undefined, channelWalletError: Error | undefined } >( - ({ user: { channelWalletExists } }) => ({ - channelWalletExists, + ({ user: { channelWalletAddress, channelWalletError } }) => ({ + channelWalletAddress, + channelWalletError, }), shallowEqual ) - return channelWalletExists + return { address: channelWalletAddress, error: channelWalletError } } export function useIsDarkMode(): boolean { From f3f8a8d1c4c2c66dcdc3d6d7d567a1455093589d Mon Sep 17 00:00:00 2001 From: Rogue Alexander Date: Sat, 10 Apr 2021 20:16:34 +0200 Subject: [PATCH 3/6] feature(ChannelWallet): Include in prev commit --- src/state/user/reducer.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/state/user/reducer.ts b/src/state/user/reducer.ts index 38efe32..e54af40 100644 --- a/src/state/user/reducer.ts +++ b/src/state/user/reducer.ts @@ -15,14 +15,19 @@ import { updateUserDeadline, toggleURLWarning, updateUserSingleHopOnly, - updateChannelWalletExists + updateChannelWalletAddress, + updateChannelWalletError, } from './actions' const currentTimestamp = () => new Date().getTime() export interface UserState { // the timestamp of the last updateVersion action - channelWalletExists: boolean + channelWalletAddress?: string + channelWalletError?: Error + channelWalletActive?: boolean + + lastUpdateVersionTimestamp?: number userDarkMode: boolean | null // the user's choice for dark mode or light mode @@ -60,7 +65,6 @@ function pairKey(token0Address: string, token1Address: string) { } export const initialState: UserState = { - channelWalletExists: false, userDarkMode: null, matchesDarkMode: false, userExpertMode: false, @@ -75,8 +79,12 @@ export const initialState: UserState = { export default createReducer(initialState, builder => builder - .addCase(updateChannelWalletExists, (state, action) => { - state.channelWalletExists = action.payload.channelWalletExists, + .addCase(updateChannelWalletAddress, (state, action) => { + state.channelWalletAddress = action.payload.channelWalletAddress + state.timestamp = currentTimestamp() + }) + .addCase(updateChannelWalletError, (state, action) => { + state.channelWalletError = action.payload.channelWalletError state.timestamp = currentTimestamp() }) .addCase(updateVersion, state => { From 364d5ddcf0718a6c557bcf88859cef267e11d824 Mon Sep 17 00:00:00 2001 From: Rogue Alexander Date: Sat, 10 Apr 2021 20:18:20 +0200 Subject: [PATCH 4/6] feature(ChannelWallet): Update state to exist as one entity inside user, may be extracted in futuer --- src/state/user/actions.ts | 7 +++++-- src/state/user/reducer.ts | 9 +++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/state/user/actions.ts b/src/state/user/actions.ts index edaca91..9badfbe 100644 --- a/src/state/user/actions.ts +++ b/src/state/user/actions.ts @@ -13,8 +13,11 @@ export interface SerializedPair { token1: SerializedToken } -export const updateChannelWalletAddress = createAction<{ channelWalletAddress?: string }>('user/updateChannelWalletAddress') -export const updateChannelWalletError = createAction<{ channelWalletError?: Error }>('user/updateChannelWalletError') +export const updateChannelWalletState = createAction<{ + channelWalletAddress?: string, + channelWalletError?: Error, + channelWalletActive?: boolean, +}>('user/updateChannelWalletState') export const updateMatchesDarkMode = createAction<{ matchesDarkMode: boolean }>('user/updateMatchesDarkMode') export const updateUserDarkMode = createAction<{ userDarkMode: boolean }>('user/updateUserDarkMode') diff --git a/src/state/user/reducer.ts b/src/state/user/reducer.ts index e54af40..76bbe0f 100644 --- a/src/state/user/reducer.ts +++ b/src/state/user/reducer.ts @@ -15,8 +15,7 @@ import { updateUserDeadline, toggleURLWarning, updateUserSingleHopOnly, - updateChannelWalletAddress, - updateChannelWalletError, + updateChannelWalletState, } from './actions' const currentTimestamp = () => new Date().getTime() @@ -79,12 +78,10 @@ export const initialState: UserState = { export default createReducer(initialState, builder => builder - .addCase(updateChannelWalletAddress, (state, action) => { + .addCase(updateChannelWalletState, (state, action) => { state.channelWalletAddress = action.payload.channelWalletAddress - state.timestamp = currentTimestamp() - }) - .addCase(updateChannelWalletError, (state, action) => { state.channelWalletError = action.payload.channelWalletError + state.channelWalletActive = action.payload.channelWalletActive state.timestamp = currentTimestamp() }) .addCase(updateVersion, state => { From 3c915bcd903836c9fae6da3477827f8f1c4b41c2 Mon Sep 17 00:00:00 2001 From: Rogue Alexander Date: Sat, 10 Apr 2021 20:59:49 +0200 Subject: [PATCH 5/6] feature(CopyIcon): Extract copy icon to separate file for reuse --- src/components/AccountDetails/index.tsx | 2 +- src/components/{AccountDetails/Copy.tsx => CopyIcon/index.tsx} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/components/{AccountDetails/Copy.tsx => CopyIcon/index.tsx} (100%) diff --git a/src/components/AccountDetails/index.tsx b/src/components/AccountDetails/index.tsx index a7d185a..f6794ca 100644 --- a/src/components/AccountDetails/index.tsx +++ b/src/components/AccountDetails/index.tsx @@ -6,7 +6,7 @@ import { AppDispatch } from '../../state' import { clearAllTransactions } from '../../state/transactions/actions' import { shortenAddress } from '../../utils' import { AutoRow } from '../Row' -import Copy from './Copy' +import Copy from '../CopyIcon' import Transaction from './Transaction' import { SUPPORTED_WALLETS } from '../../constants' diff --git a/src/components/AccountDetails/Copy.tsx b/src/components/CopyIcon/index.tsx similarity index 100% rename from src/components/AccountDetails/Copy.tsx rename to src/components/CopyIcon/index.tsx From b1bae71cb708c3c5f3e8ef74fd22950a3ea088f0 Mon Sep 17 00:00:00 2001 From: Rogue Alexander Date: Sat, 10 Apr 2021 21:00:02 +0200 Subject: [PATCH 6/6] feature(ChannelWallet): Begin creation of channel wallet creation and recovery flow --- .../Transaction.tsx | 62 +++ .../ChannelWalletAccountDetails/index.tsx | 411 ++++++++++++++++++ src/components/ChannelWalletModal/Option.tsx | 140 ++++++ .../ChannelWalletModal/PendingView.tsx | 127 ++++++ src/components/ChannelWalletModal/index.tsx | 245 +++++++++++ src/state/user/hooks.tsx | 11 +- 6 files changed, 991 insertions(+), 5 deletions(-) create mode 100644 src/components/ChannelWalletAccountDetails/Transaction.tsx create mode 100644 src/components/ChannelWalletAccountDetails/index.tsx create mode 100644 src/components/ChannelWalletModal/Option.tsx create mode 100644 src/components/ChannelWalletModal/PendingView.tsx create mode 100644 src/components/ChannelWalletModal/index.tsx diff --git a/src/components/ChannelWalletAccountDetails/Transaction.tsx b/src/components/ChannelWalletAccountDetails/Transaction.tsx new file mode 100644 index 0000000..8ffe6ad --- /dev/null +++ b/src/components/ChannelWalletAccountDetails/Transaction.tsx @@ -0,0 +1,62 @@ +import React from 'react' +import styled from 'styled-components' +import { CheckCircle, Triangle } from 'react-feather' + +import { useActiveWeb3React } from '../../hooks' +import { getEtherscanLink } from '../../utils' +import { ExternalLink } from '../../theme' +import { useAllTransactions } from '../../state/transactions/hooks' +import { RowFixed } from '../Row' +import Loader from '../Loader' + +const TransactionWrapper = styled.div`` + +const TransactionStatusText = styled.div` + margin-right: 0.5rem; + display: flex; + align-items: center; + :hover { + text-decoration: underline; + } +` + +const TransactionState = styled(ExternalLink)<{ pending: boolean; success?: boolean }>` + display: flex; + justify-content: space-between; + align-items: center; + text-decoration: none !important; + border-radius: 0.5rem; + padding: 0.25rem 0rem; + font-weight: 500; + font-size: 0.825rem; + color: ${({ theme }) => theme.primary1}; +` + +const IconWrapper = styled.div<{ pending: boolean; success?: boolean }>` + color: ${({ pending, success, theme }) => (pending ? theme.primary1 : success ? theme.green1 : theme.red1)}; +` + +export default function Transaction({ hash }: { hash: string }) { + const { chainId } = useActiveWeb3React() + const allTransactions = useAllTransactions() + + const tx = allTransactions?.[hash] + const summary = tx?.summary + const pending = !tx?.receipt + const success = !pending && tx && (tx.receipt?.status === 1 || typeof tx.receipt?.status === 'undefined') + + if (!chainId) return null + + return ( + + + + {summary ?? hash} ↗ + + + {pending ? : success ? : } + + + + ) +} diff --git a/src/components/ChannelWalletAccountDetails/index.tsx b/src/components/ChannelWalletAccountDetails/index.tsx new file mode 100644 index 0000000..84e944f --- /dev/null +++ b/src/components/ChannelWalletAccountDetails/index.tsx @@ -0,0 +1,411 @@ +import React, { useCallback, useContext } from 'react' +import { useDispatch } from 'react-redux' +import styled, { ThemeContext } from 'styled-components' +import { useActiveWeb3React } from '../../hooks' +import { AppDispatch } from '../../state' +import { clearAllTransactions } from '../../state/transactions/actions' +import { shortenAddress } from '../../utils' +import { AutoRow } from '../Row' +import Copy from '../CopyIcon' +import Transaction from './Transaction' + +import { SUPPORTED_WALLETS } from '../../constants' +import { ReactComponent as Close } from '../../assets/images/x.svg' +import { getEtherscanLink } from '../../utils' +import { injected, walletconnect, walletlink, fortmatic, portis } from '../../connectors' +import CoinbaseWalletIcon from '../../assets/images/coinbaseWalletIcon.svg' +import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg' +import FortmaticIcon from '../../assets/images/fortmaticIcon.png' +import PortisIcon from '../../assets/images/portisIcon.png' +import Identicon from '../Identicon' +import { ButtonSecondary } from '../Button' +import { ExternalLink as LinkIcon } from 'react-feather' +import { ExternalLink, LinkStyledButton, TYPE } from '../../theme' + +const HeaderRow = styled.div` + ${({ theme }) => theme.flexRowNoWrap}; + padding: 1rem 1rem; + font-weight: 500; + color: ${props => (props.color === 'blue' ? ({ theme }) => theme.primary1 : 'inherit')}; + ${({ theme }) => theme.mediaWidth.upToMedium` + padding: 1rem; + `}; +` + +const UpperSection = styled.div` + position: relative; + + h5 { + margin: 0; + margin-bottom: 0.5rem; + font-size: 1rem; + font-weight: 400; + } + + h5:last-child { + margin-bottom: 0px; + } + + h4 { + margin-top: 0; + font-weight: 500; + } +` + +const InfoCard = styled.div` + padding: 1rem; + border: 1px solid ${({ theme }) => theme.bg3}; + border-radius: 20px; + position: relative; + display: grid; + grid-row-gap: 12px; + margin-bottom: 20px; +` + +const AccountGroupingRow = styled.div` + ${({ theme }) => theme.flexRowNoWrap}; + justify-content: space-between; + align-items: center; + font-weight: 400; + color: ${({ theme }) => theme.text1}; + + div { + ${({ theme }) => theme.flexRowNoWrap} + align-items: center; + } +` + +const AccountSection = styled.div` + background-color: ${({ theme }) => theme.bg1}; + padding: 0rem 1rem; + ${({ theme }) => theme.mediaWidth.upToMedium`padding: 0rem 1rem 1.5rem 1rem;`}; +` + +const YourAccount = styled.div` + h5 { + margin: 0 0 1rem 0; + font-weight: 400; + } + + h4 { + margin: 0; + font-weight: 500; + } +` + +const LowerSection = styled.div` + ${({ theme }) => theme.flexColumnNoWrap} + padding: 1.5rem; + flex-grow: 1; + overflow: auto; + background-color: ${({ theme }) => theme.bg2}; + border-bottom-left-radius: 20px; + border-bottom-right-radius: 20px; + + h5 { + margin: 0; + font-weight: 400; + color: ${({ theme }) => theme.text3}; + } +` + +const AccountControl = styled.div` + display: flex; + justify-content: space-between; + min-width: 0; + width: 100%; + + font-weight: 500; + font-size: 1.25rem; + + a:hover { + text-decoration: underline; + } + + p { + min-width: 0; + margin: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +` + +const AddressLink = styled(ExternalLink)<{ hasENS: boolean; isENS: boolean }>` + font-size: 0.825rem; + color: ${({ theme }) => theme.text3}; + margin-left: 1rem; + font-size: 0.825rem; + display: flex; + :hover { + color: ${({ theme }) => theme.text2}; + } +` + +const CloseIcon = styled.div` + position: absolute; + right: 1rem; + top: 14px; + &:hover { + cursor: pointer; + opacity: 0.6; + } +` + +const CloseColor = styled(Close)` + path { + stroke: ${({ theme }) => theme.text4}; + } +` + +const WalletName = styled.div` + width: initial; + font-size: 0.825rem; + font-weight: 500; + color: ${({ theme }) => theme.text3}; +` + +const IconWrapper = styled.div<{ size?: number }>` + ${({ theme }) => theme.flexColumnNoWrap}; + align-items: center; + justify-content: center; + margin-right: 8px; + & > img, + span { + height: ${({ size }) => (size ? size + 'px' : '32px')}; + width: ${({ size }) => (size ? size + 'px' : '32px')}; + } + ${({ theme }) => theme.mediaWidth.upToMedium` + align-items: flex-end; + `}; +` + +const TransactionListWrapper = styled.div` + ${({ theme }) => theme.flexColumnNoWrap}; +` + +const WalletAction = styled(ButtonSecondary)` + width: fit-content; + font-weight: 400; + margin-left: 8px; + font-size: 0.825rem; + padding: 4px 6px; + :hover { + cursor: pointer; + text-decoration: underline; + } +` + +const MainWalletAction = styled(WalletAction)` + color: ${({ theme }) => theme.primary1}; +` + +function renderTransactions(transactions: string[]) { + return ( + + {transactions.map((hash, i) => { + return + })} + + ) +} + +interface AccountDetailsProps { + toggleWalletModal: () => void + pendingTransactions: string[] + confirmedTransactions: string[] + ENSName?: string + openOptions: () => void +} + +export default function ChannelWalletAccountDetails({ + toggleWalletModal, + pendingTransactions, + confirmedTransactions, + ENSName, + openOptions +}: AccountDetailsProps) { + const { chainId, account, connector } = useActiveWeb3React() + const theme = useContext(ThemeContext) + const dispatch = useDispatch() + + function formatConnectorName() { + const { ethereum } = window + const isMetaMask = !!(ethereum && ethereum.isMetaMask) + const name = Object.keys(SUPPORTED_WALLETS) + .filter( + k => + SUPPORTED_WALLETS[k].connector === connector && (connector !== injected || isMetaMask === (k === 'METAMASK')) + ) + .map(k => SUPPORTED_WALLETS[k].name)[0] + return Connected with {name} + } + + function getStatusIcon() { + if (connector === injected) { + return ( + + + + ) + } else if (connector === walletconnect) { + return ( + + {'wallet + + ) + } else if (connector === walletlink) { + return ( + + {'coinbase + + ) + } else if (connector === fortmatic) { + return ( + + {'fortmatic + + ) + } else if (connector === portis) { + return ( + <> + + {'portis + { + portis.portis.showPortis() + }} + > + Show Portis + + + + ) + } + return null + } + + const clearAllTransactionsCallback = useCallback(() => { + if (chainId) dispatch(clearAllTransactions({ chainId })) + }, [dispatch, chainId]) + + return ( + <> + + + + + Account + + + + + {formatConnectorName()} +
+ {connector !== injected && connector !== walletlink && ( + { + ;(connector as any).close() + }} + > + Disconnect + + )} + { + openOptions() + }} + > + Change + +
+
+ + + {ENSName ? ( + <> +
+ {getStatusIcon()} +

{ENSName}

+
+ + ) : ( + <> +
+ {getStatusIcon()} +

{account && shortenAddress(account)}

+
+ + )} +
+
+ + {ENSName ? ( + <> + +
+ {account && ( + + Copy Address + + )} + {chainId && account && ( + + + View on Etherscan + + )} +
+
+ + ) : ( + <> + +
+ {account && ( + + Copy Address + + )} + {chainId && account && ( + + + View on Etherscan + + )} +
+
+ + )} +
+
+
+
+
+ {!!pendingTransactions.length || !!confirmedTransactions.length ? ( + + + Recent Transactions + (clear all) + + {renderTransactions(pendingTransactions)} + {renderTransactions(confirmedTransactions)} + + ) : ( + + Your transactions will appear here... + + )} + + ) +} diff --git a/src/components/ChannelWalletModal/Option.tsx b/src/components/ChannelWalletModal/Option.tsx new file mode 100644 index 0000000..51bc342 --- /dev/null +++ b/src/components/ChannelWalletModal/Option.tsx @@ -0,0 +1,140 @@ +import React from 'react' +import styled from 'styled-components' +import { ExternalLink } from '../../theme' + +const InfoCard = styled.button<{ active?: boolean }>` + background-color: ${({ theme, active }) => (active ? theme.bg3 : theme.bg2)}; + padding: 1rem; + outline: none; + border: 1px solid; + border-radius: 12px; + width: 100% !important; + &:focus { + box-shadow: 0 0 0 1px ${({ theme }) => theme.primary1}; + } + border-color: ${({ theme, active }) => (active ? 'transparent' : theme.bg3)}; +` + +const OptionCard = styled(InfoCard as any)` + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + margin-top: 2rem; + padding: 1rem; +` + +const OptionCardLeft = styled.div` + ${({ theme }) => theme.flexColumnNoWrap}; + justify-content: center; + height: 100%; +` + +const OptionCardClickable = styled(OptionCard as any)<{ clickable?: boolean }>` + margin-top: 0; + &:hover { + cursor: ${({ clickable }) => (clickable ? 'pointer' : '')}; + border: ${({ clickable, theme }) => (clickable ? `1px solid ${theme.primary1}` : ``)}; + } + opacity: ${({ disabled }) => (disabled ? '0.5' : '1')}; +` + +const GreenCircle = styled.div` + ${({ theme }) => theme.flexRowNoWrap} + justify-content: center; + align-items: center; + + &:first-child { + height: 8px; + width: 8px; + margin-right: 8px; + background-color: ${({ theme }) => theme.green1}; + border-radius: 50%; + } +` + +const CircleWrapper = styled.div` + color: ${({ theme }) => theme.green1}; + display: flex; + justify-content: center; + align-items: center; +` + +const HeaderText = styled.div` + ${({ theme }) => theme.flexRowNoWrap}; + color: ${props => (props.color === 'blue' ? ({ theme }) => theme.primary1 : ({ theme }) => theme.text1)}; + font-size: 1rem; + font-weight: 500; +` + +const SubHeader = styled.div` + color: ${({ theme }) => theme.text1}; + margin-top: 10px; + font-size: 12px; +` + +const IconWrapper = styled.div<{ size?: number | null }>` + ${({ theme }) => theme.flexColumnNoWrap}; + align-items: center; + justify-content: center; + & > img, + span { + height: ${({ size }) => (size ? size + 'px' : '24px')}; + width: ${({ size }) => (size ? size + 'px' : '24px')}; + } + ${({ theme }) => theme.mediaWidth.upToMedium` + align-items: flex-end; + `}; +` + +export default function Option({ + link = null, + clickable = true, + size, + onClick = null, + color, + header, + subheader = null, + icon, + active = false, + id +}: { + link?: string | null + clickable?: boolean + size?: number | null + onClick?: null | (() => void) + color: string + header: React.ReactNode + subheader: React.ReactNode | null + icon: string + active?: boolean + id: string +}) { + const content = ( + + + + {active ? ( + + +
+ + + ) : ( + '' + )} + {header} + + {subheader && {subheader}} + + + {'Icon'} + + + ) + if (link) { + return {content} + } + + return content +} diff --git a/src/components/ChannelWalletModal/PendingView.tsx b/src/components/ChannelWalletModal/PendingView.tsx new file mode 100644 index 0000000..72b840c --- /dev/null +++ b/src/components/ChannelWalletModal/PendingView.tsx @@ -0,0 +1,127 @@ +import { AbstractConnector } from '@web3-react/abstract-connector' +import React from 'react' +import styled from 'styled-components' +import Option from './Option' +import { SUPPORTED_WALLETS } from '../../constants' +import { injected } from '../../connectors' +import { darken } from 'polished' +import Loader from '../Loader' + +const PendingSection = styled.div` + ${({ theme }) => theme.flexColumnNoWrap}; + align-items: center; + justify-content: center; + width: 100%; + & > * { + width: 100%; + } +` + +const StyledLoader = styled(Loader)` + margin-right: 1rem; +` + +const LoadingMessage = styled.div<{ error?: boolean }>` + ${({ theme }) => theme.flexRowNoWrap}; + align-items: center; + justify-content: flex-start; + border-radius: 12px; + margin-bottom: 20px; + color: ${({ theme, error }) => (error ? theme.red1 : 'inherit')}; + border: 1px solid ${({ theme, error }) => (error ? theme.red1 : theme.text4)}; + + & > * { + padding: 1rem; + } +` + +const ErrorGroup = styled.div` + ${({ theme }) => theme.flexRowNoWrap}; + align-items: center; + justify-content: flex-start; +` + +const ErrorButton = styled.div` + border-radius: 8px; + font-size: 12px; + color: ${({ theme }) => theme.text1}; + background-color: ${({ theme }) => theme.bg4}; + margin-left: 1rem; + padding: 0.5rem; + font-weight: 600; + user-select: none; + + &:hover { + cursor: pointer; + background-color: ${({ theme }) => darken(0.1, theme.text4)}; + } +` + +const LoadingWrapper = styled.div` + ${({ theme }) => theme.flexRowNoWrap}; + align-items: center; + justify-content: center; +` + +export default function ChannelWalletCreatePendingView({ + connector, + error = false, + setChannelWalletCreationError, +}: { + connector?: AbstractConnector + error?: boolean + setChannelWalletCreationError: (error: boolean) => void +}) { + const isMetamask = window?.ethereum?.isMetaMask + + return ( + + + + {error ? ( + +
Error connecting.
+ { + setChannelWalletCreationError(false) + }} + > + Try Again + +
+ ) : ( + <> + + Initializing... + + )} +
+
+ {Object.keys(SUPPORTED_WALLETS).map(key => { + const option = SUPPORTED_WALLETS[key] + if (option.connector === connector) { + if (option.connector === injected) { + if (isMetamask && option.name !== 'MetaMask') { + return null + } + if (!isMetamask && option.name === 'MetaMask') { + return null + } + } + return ( +
+ ) +} diff --git a/src/components/ChannelWalletModal/index.tsx b/src/components/ChannelWalletModal/index.tsx new file mode 100644 index 0000000..2153f7e --- /dev/null +++ b/src/components/ChannelWalletModal/index.tsx @@ -0,0 +1,245 @@ +import { AbstractConnector } from '@web3-react/abstract-connector' +import ChannelWalletAccountDetails from 'components/ChannelWalletAccountDetails' +import React, { useEffect, useState } from 'react' +import { useChannelWalletState } from 'state/user/hooks' +import styled from 'styled-components' +import { ReactComponent as Close } from '../../assets/images/x.svg' +import usePrevious from '../../hooks/usePrevious' +import { ApplicationModal } from '../../state/application/actions' +import { useModalOpen, useWalletModalToggle } from '../../state/application/hooks' +import { ExternalLink } from '../../theme' + +import Modal from '../Modal' +import ChannelWalletCreatePendingView from './PendingView' + +const CloseIcon = styled.div` + position: absolute; + right: 1rem; + top: 14px; + &:hover { + cursor: pointer; + opacity: 0.6; + } +` + +const CloseColor = styled(Close)` + path { + stroke: ${({ theme }) => theme.text4}; + } +` + +const Wrapper = styled.div` + ${({ theme }) => theme.flexColumnNoWrap} + margin: 0; + padding: 0; + width: 100%; +` + +const HeaderRow = styled.div` + ${({ theme }) => theme.flexRowNoWrap}; + padding: 1rem 1rem; + font-weight: 500; + color: ${props => (props.color === 'blue' ? ({ theme }) => theme.primary1 : 'inherit')}; + ${({ theme }) => theme.mediaWidth.upToMedium` + padding: 1rem; + `}; +` + +const ContentWrapper = styled.div` + background-color: ${({ theme }) => theme.bg2}; + padding: 2rem; + border-bottom-left-radius: 20px; + border-bottom-right-radius: 20px; + + ${({ theme }) => theme.mediaWidth.upToMedium`padding: 1rem`}; +` + +const UpperSection = styled.div` + position: relative; + + h5 { + margin: 0; + margin-bottom: 0.5rem; + font-size: 1rem; + font-weight: 400; + } + + h5:last-child { + margin-bottom: 0px; + } + + h4 { + margin-top: 0; + font-weight: 500; + } +` + +const Blurb = styled.div` + ${({ theme }) => theme.flexRowNoWrap} + align-items: center; + justify-content: center; + flex-wrap: wrap; + margin-top: 2rem; + ${({ theme }) => theme.mediaWidth.upToMedium` + margin: 1rem; + font-size: 12px; + `}; +` + +const OptionGrid = styled.div` + display: grid; + grid-gap: 10px; + ${({ theme }) => theme.mediaWidth.upToMedium` + grid-template-columns: 1fr; + grid-gap: 10px; + `}; +` + +const HoverText = styled.div` + :hover { + cursor: pointer; + } +` + +const WALLET_VIEWS = { + CREATE_OR_RECOVER: 'create_or_recover', + CREATING_WALLET: 'creating_wallet', + SEED_PHRASE_RECOVERY: 'seed_phrase_recovery', + ACCOUNT: 'account', +} + +export default function ChannelWalletModal({ + pendingTransactions, + confirmedTransactions, + ENSName +}: { + pendingTransactions: string[] // hashes of pending + confirmedTransactions: string[] // hashes of confirmed + ENSName?: string +}) { + // Wallet Model Flow + // If Wallet connected, show wallet connected screen with chain the wallet SC exists on and pending txns + // If no wallet created, show: + // Option to Create Wallet on either Binance or Ethereum chains + // If encrypted seed phrase in storage show seed phrase unlock input (alternative enter seed phrase button below) + // If no encrypted seed phrase in storage show 'recover with seed phrase' + + // important that these are destructed from the account-specific web3-react context + const { active, address, error } = useChannelWalletState() + // const { active, account, connector, activate, error } = useWeb3React() + + const [walletView, setWalletView] = useState(WALLET_VIEWS.ACCOUNT) + + const [channelWalletCreationError, setChannelWalletCreationError] = useState() + + const walletModalOpen = useModalOpen(ApplicationModal.WALLET) + const toggleWalletModal = useWalletModalToggle() + + const prevActive = usePrevious(active) + + // close on connection, when logged out before + useEffect(() => { + if (address && !prevActive && walletModalOpen) { + toggleWalletModal() + } + }, [address, prevActive, toggleWalletModal, walletModalOpen]) + + // always reset to account view + useEffect(() => { + if (walletModalOpen) { + setChannelWalletCreationError(false) + setWalletView(WALLET_VIEWS.ACCOUNT) + } + }, [walletModalOpen]) + + // close modal when a connection is successful + const activePrevious = usePrevious(active) + useEffect(() => { + if (walletModalOpen && (active && !activePrevious)) { + setWalletView(WALLET_VIEWS.ACCOUNT) + } + }, [setWalletView, active, error, walletModalOpen, activePrevious]) + + function getModalContent() { + if (error) { + return ( + + + + + Error creating or connecting to channel wallet + + Error connecting to the channel wallet, either your seed phrase is wrong or the channel states could not be loaded. + + + ) + } + if (address && walletView === WALLET_VIEWS.ACCOUNT) { + return ( + + ) + } + return ( + + + + + {walletView === WALLET_VIEWS.CREATE_OR_RECOVER && + + Create or recover channel wallet + + } + {walletView !== WALLET_VIEWS.ACCOUNT ? ( + + { + setChannelWalletCreationError(false) + setWalletView(WALLET_VIEWS.ACCOUNT) + }} + > + Back + + + ) : ( + + Connect to a wallet + + )} + + {walletView === WALLET_VIEWS.CREATE_OR_RECOVER && + <> + + New to Layer 2?  {' '} + Learn more about layer 2 channel wallets here. + + + } + {walletView === WALLET_VIEWS.SEED_PHRASE_RECOVERY && + <> + + Enter your channel wallet seed phrase below to recover your wallet (NO OTHER SITES WILL ASK FOR THIS) + + + } + {walletView === WALLET_VIEWS.CREATING_WALLET && + + } + + + ) + } + + return ( + + {getModalContent()} + + ) +} diff --git a/src/state/user/hooks.tsx b/src/state/user/hooks.tsx index bda1505..4c247c2 100644 --- a/src/state/user/hooks.tsx +++ b/src/state/user/hooks.tsx @@ -42,18 +42,19 @@ function deserializeToken(serializedToken: SerializedToken): Token { ) } -export function useChannelWalletState(): { address: string | undefined, error: Error | undefined } { - const { channelWalletAddress, channelWalletError } = useSelector< +export function useChannelWalletState(): { address: string | undefined, error: Error | undefined, active: boolean | undefined } { + const { channelWalletAddress, channelWalletError, channelWalletActive } = useSelector< AppState, - { channelWalletAddress: string | undefined, channelWalletError: Error | undefined } + { channelWalletAddress: string | undefined, channelWalletError: Error | undefined, channelWalletActive: boolean | undefined } >( - ({ user: { channelWalletAddress, channelWalletError } }) => ({ + ({ user: { channelWalletAddress, channelWalletError, channelWalletActive } }) => ({ channelWalletAddress, channelWalletError, + channelWalletActive, }), shallowEqual ) - return { address: channelWalletAddress, error: channelWalletError } + return { address: channelWalletAddress, error: channelWalletError, active: channelWalletActive } } export function useIsDarkMode(): boolean {