From 6cdfd3b2b49f8a3677f7bfca15262edf6f22db11 Mon Sep 17 00:00:00 2001 From: Inokentii Mazhara Date: Tue, 24 Dec 2024 11:14:57 +0200 Subject: [PATCH] TW-1498: [EVM] Connect wallet to dapps (#1235) * TW-1498 Implement connection to EVM dApps (raw version) * TW-1498 Fix connection to dApps * TW-1498 Fix signing * TW-1498 Change UI for connection page * TW-1498 Add UI for signing EVM messages and data * TW-1498 Display active EVM connection on extension home page * TW-1498 Display EVM DApps in settings * TW-1498 Fix types errors * TW-1498 Fix behavior after a network that has a connection is disabled * TW-1498 Some refactoring * TW-1498 Fix 'Testnet mode' label * TW-1498 Move EVM RPC requests to the background * TW-1498 Minor refactoring * TW-1498 Refactoring according to comments * TW-1498 Unhardcode 'All Networks' label * TW-1498 Fix connectivity for Firefox * TW-1498 UI fixes * TW-1498 Fix requests freezing * TW-1498 Remove an unused export --- package.json | 2 + public/_locales/en/messages.json | 29 +- public/confirm.html | 2 +- src/app/ConfirmPage.tsx | 473 ------------------ src/app/ConfirmPage/confirm-dapp-form.tsx | 206 ++++++++ src/app/ConfirmPage/evm-confirm-dapp-form.tsx | 69 +++ src/app/ConfirmPage/index.tsx | 79 +++ src/app/ConfirmPage/payload-content.tsx | 89 ++++ .../selectors.ts} | 0 .../ConfirmPage/tezos-confirm-dapp-form.tsx | 125 +++++ src/app/atoms/DAppLogo/index.tsx | 12 +- src/app/atoms/DAppLogo/plug.svg | 6 - src/app/atoms/DAppLogo/unknown-dapp.svg | 18 + .../atoms/PageModal/actions-buttons-box.tsx | 2 +- src/app/atoms/PageModal/index.tsx | 42 +- src/app/atoms/ProgressAndNumbers.tsx | 22 + src/app/atoms/ReadOnlySecretField.tsx | 2 +- src/app/atoms/Spinner/Spinner.tsx | 6 +- src/app/atoms/TextCopyButton.tsx | 0 src/app/icons/base/unlock_fill.svg | 6 + src/app/icons/layers.svg | 1 - src/app/layouts/PageLayout/index.tsx | 6 +- .../reveal-private-key-modal/index.tsx | 5 +- .../Tokens/components/AddTokenModal/index.tsx | 5 +- src/app/pages/ImportWallet.tsx | 5 +- src/app/pages/Receive/Receive.tsx | 2 +- .../Send/modals/ConfirmSend/BaseContent.tsx | 2 +- .../pages/Send/modals/ConfirmSend/index.tsx | 2 +- .../pages/Send/modals/SelectAccount/index.tsx | 82 ++- .../pages/Send/modals/SelectAsset/index.tsx | 192 +------ src/app/pages/Settings/DApps.tsx | 124 +++-- src/app/pages/Welcome/Welcome.tsx | 5 +- src/app/storage/dapps/index.ts | 73 ++- src/app/storage/dapps/use-value.hook.ts | 6 +- src/app/templates/AccountBanner.tsx | 159 +++--- src/app/templates/AccountCard.tsx | 63 +++ src/app/templates/AccountsGroup.tsx | 23 + src/app/templates/AccountsModal.tsx | 19 + .../templates/AccountsModalContent/index.tsx | 229 +++++++++ .../selectors.ts | 0 src/app/templates/AppHeader/AccountsModal.tsx | 280 ----------- src/app/templates/AppHeader/index.tsx | 4 +- src/app/templates/Balance.tsx | 93 ++-- src/app/templates/ConnectBanner.tsx | 57 --- src/app/templates/DAppConnection/index.tsx | 25 +- .../DAppConnection/use-connections.ts | 28 +- src/app/templates/EvmOperationView.tsx | 14 + .../ImportAccountModal/forms/mnemonic.tsx | 3 +- .../templates/ImportAccountModal/index.tsx | 5 +- src/app/templates/ImportSeedForm/index.tsx | 1 - src/app/templates/ManualBackupModal/index.tsx | 9 +- .../ManualBackupModal/mnemonic-view.tsx | 8 +- src/app/templates/NetworkBanner.tsx | 121 +++-- src/app/templates/NewRawPayloadView.tsx | 60 +++ .../Synchronization/SyncSettings.tsx | 2 +- ...erationView.tsx => TezosOperationView.tsx} | 22 +- src/app/templates/network-popper/constants.ts | 3 + src/app/templates/network-popper/dropdown.tsx | 81 +++ src/app/templates/network-popper/index.tsx | 44 ++ src/app/templates/network-popper/option.tsx | 59 +++ src/app/templates/network-popper/types.ts | 20 + src/content-scripts/inpage.ts | 91 ++++ .../main.ts} | 77 ++- src/lib/analytics/custom-rpc.context.ts | 2 + src/lib/analytics/index.ts | 2 +- src/lib/analytics/use-analytics.hook.ts | 8 +- src/lib/constants.ts | 8 + src/lib/metadata/index.ts | 3 + src/lib/temple/back/actions.ts | 132 ++++- src/lib/temple/back/dapp.ts | 145 +----- src/lib/temple/back/evm-dapp.ts | 360 +++++++++++++ .../back/evm-validation-schemas/index.ts | 66 +++ .../back/evm-validation-schemas/utils.ts | 91 ++++ src/lib/temple/back/main.ts | 106 +++- src/lib/temple/back/request-confirm.ts | 129 +++++ src/lib/temple/back/store.ts | 18 +- src/lib/temple/back/vault/index.ts | 116 +++-- src/lib/temple/back/vault/vault.test.ts | 181 +++++++ src/lib/temple/front/client.ts | 23 +- src/lib/temple/front/provider.tsx | 14 +- src/lib/temple/types.ts | 111 +++- src/lib/ui/Popper.tsx | 2 +- src/lib/ui/search-networks.ts | 14 +- src/lib/utils/index.ts | 53 +- src/lib/utils/utils.test.ts | 29 +- src/temple/evm/constants.ts | 32 ++ src/temple/evm/evm-chains-rpc-urls.ts | 23 + src/temple/evm/get-read-only-evm.ts | 43 ++ src/temple/evm/index.ts | 20 +- src/temple/evm/typed-data-v1.ts | 307 ++++++++++++ src/temple/evm/types.ts | 12 + src/temple/evm/web3-provider.ts | 397 +++++++++++++++ src/temple/front/chains.ts | 2 + src/temple/front/ready/networks.ts | 18 +- src/temple/networks.ts | 8 + tailwind.config.js | 3 +- webpack.config.ts | 8 +- webpack/manifest.ts | 9 +- yarn.lock | 10 + 99 files changed, 4162 insertions(+), 1643 deletions(-) delete mode 100644 src/app/ConfirmPage.tsx create mode 100644 src/app/ConfirmPage/confirm-dapp-form.tsx create mode 100644 src/app/ConfirmPage/evm-confirm-dapp-form.tsx create mode 100644 src/app/ConfirmPage/index.tsx create mode 100644 src/app/ConfirmPage/payload-content.tsx rename src/app/{ConfirmPage.selectors.ts => ConfirmPage/selectors.ts} (100%) create mode 100644 src/app/ConfirmPage/tezos-confirm-dapp-form.tsx delete mode 100644 src/app/atoms/DAppLogo/plug.svg create mode 100644 src/app/atoms/DAppLogo/unknown-dapp.svg create mode 100644 src/app/atoms/ProgressAndNumbers.tsx create mode 100644 src/app/atoms/TextCopyButton.tsx create mode 100644 src/app/icons/base/unlock_fill.svg delete mode 100644 src/app/icons/layers.svg create mode 100644 src/app/templates/AccountCard.tsx create mode 100644 src/app/templates/AccountsGroup.tsx create mode 100644 src/app/templates/AccountsModal.tsx create mode 100644 src/app/templates/AccountsModalContent/index.tsx rename src/app/templates/{AppHeader => AccountsModalContent}/selectors.ts (100%) delete mode 100644 src/app/templates/AppHeader/AccountsModal.tsx delete mode 100644 src/app/templates/ConnectBanner.tsx create mode 100644 src/app/templates/EvmOperationView.tsx create mode 100644 src/app/templates/NewRawPayloadView.tsx rename src/app/templates/{OperationView.tsx => TezosOperationView.tsx} (90%) create mode 100644 src/app/templates/network-popper/constants.ts create mode 100644 src/app/templates/network-popper/dropdown.tsx create mode 100644 src/app/templates/network-popper/index.tsx create mode 100644 src/app/templates/network-popper/option.tsx create mode 100644 src/app/templates/network-popper/types.ts create mode 100644 src/content-scripts/inpage.ts rename src/{contentScript.ts => content-scripts/main.ts} (68%) create mode 100644 src/lib/temple/back/evm-dapp.ts create mode 100644 src/lib/temple/back/evm-validation-schemas/index.ts create mode 100644 src/lib/temple/back/evm-validation-schemas/utils.ts create mode 100644 src/lib/temple/back/request-confirm.ts create mode 100644 src/temple/evm/constants.ts create mode 100644 src/temple/evm/evm-chains-rpc-urls.ts create mode 100644 src/temple/evm/get-read-only-evm.ts create mode 100644 src/temple/evm/typed-data-v1.ts create mode 100644 src/temple/evm/web3-provider.ts diff --git a/package.json b/package.json index be49127c01..8825422b58 100644 --- a/package.json +++ b/package.json @@ -152,6 +152,7 @@ "lodash": "4.17.21", "memoizee": "^0.4.15", "mini-css-extract-plugin": "^2", + "multiformats": "^13.3.1", "nanoid": "3.1.31", "node-forge": "^1.3.1", "npm-run-all": "^4.1.5", @@ -204,6 +205,7 @@ "use-force-update": "1.0.7", "use-onclickoutside": "0.4.1", "util": "0.11.1", + "uuid": "^11.0.3", "viem": "^2.21.36", "wasm-themis": "0.14.6", "webextension-polyfill": "^0.10.0", diff --git a/public/_locales/en/messages.json b/public/_locales/en/messages.json index 6a563f5146..8f743da070 100644 --- a/public/_locales/en/messages.json +++ b/public/_locales/en/messages.json @@ -579,6 +579,12 @@ "signAction": { "message": "Sign" }, + "signatureRequest": { + "message": "Signature Request" + }, + "message": { + "message": "Message" + }, "appRequestsToSign": { "message": "$origin$ requests you to sign", "placeholders": { @@ -1402,6 +1408,9 @@ } } }, + "allowed": { + "message": "Allowed" + }, "notAllowed": { "message": "Not allowed" }, @@ -1471,6 +1480,9 @@ "network": { "message": "Network" }, + "networkDropdownPrefix": { + "message": "Network:" + }, "unknownNetwork": { "message": "Unknown:" }, @@ -1701,6 +1713,18 @@ "myAccounts": { "message": "My Accounts" }, + "permissions": { + "message": "Permissions" + }, + "viewWalletPermissionDescription": { + "message": "View wallet balance and activity" + }, + "transactionsPermissionDescription": { + "message": "Request approval for transactions" + }, + "signingPermissionDescription": { + "message": "Request signing messages and data" + }, "sendToMyAccounts": { "message": "Send to My Accounts" }, @@ -1768,6 +1792,9 @@ "connect": { "message": "Connect" }, + "connectAccount": { + "message": "Connect Account" + }, "confirmActionOnDevice": { "message": "Confirm the action on" }, @@ -3551,7 +3578,7 @@ "enterSeedPhrase": { "message": "enter" }, - "copyMnemonic": { + "copy": { "message": "Copy" }, "verifySeedPhraseInputTitle": { diff --git a/public/confirm.html b/public/confirm.html index ecdb62aa03..a3362c3456 100644 --- a/public/confirm.html +++ b/public/confirm.html @@ -1,5 +1,5 @@ - + diff --git a/src/app/ConfirmPage.tsx b/src/app/ConfirmPage.tsx deleted file mode 100644 index 637a1debf1..0000000000 --- a/src/app/ConfirmPage.tsx +++ /dev/null @@ -1,473 +0,0 @@ -import React, { FC, Fragment, memo, useCallback, useMemo, useState } from 'react'; - -import clsx from 'clsx'; - -import { AccountTypeBadge, Alert, FormSubmitButton, FormSecondaryButton } from 'app/atoms'; -import ConfirmLedgerOverlay from 'app/atoms/ConfirmLedgerOverlay'; -import DAppLogo from 'app/atoms/DAppLogo'; -import HashShortView from 'app/atoms/HashShortView'; -import Money from 'app/atoms/Money'; -import Name from 'app/atoms/Name'; -import Spinner from 'app/atoms/Spinner/Spinner'; -import SubTitle from 'app/atoms/SubTitle'; -import { SuspenseContainer } from 'app/atoms/SuspenseContainer'; -import { LAYOUT_CONTAINER_CLASSNAME } from 'app/layouts/containers'; -import Unlock from 'app/pages/Unlock/Unlock'; -import AccountBanner from 'app/templates/AccountBanner'; -import { TezosBalance } from 'app/templates/Balance'; -import ConnectBanner from 'app/templates/ConnectBanner'; -import CustomSelect, { OptionRenderProps } from 'app/templates/CustomSelect'; -import { ModifyFeeAndLimit } from 'app/templates/ExpensesView/ExpensesView'; -import NetworkBanner from 'app/templates/NetworkBanner'; -import OperationView from 'app/templates/OperationView'; -import { CustomTezosChainIdContext } from 'lib/analytics'; -import { T, t } from 'lib/i18n'; -import { getTezosGasMetadata } from 'lib/metadata'; -import { useRetryableSWR } from 'lib/swr'; -import { useTempleClient } from 'lib/temple/front/client'; -import { TempleAccountType, TempleDAppPayload } from 'lib/temple/types'; -import { useSafeState } from 'lib/ui/hooks'; -import { delay, isTruthy } from 'lib/utils'; -import { useLocation } from 'lib/woozie'; -import { AccountForTezos, getAccountForTezos, isAccountOfActableType } from 'temple/accounts'; -import { useAccountForTezos, useAllAccounts, useTezosChainIdLoadingValue } from 'temple/front'; -import { TezosNetworkEssentials } from 'temple/networks'; - -import { AccountAvatar } from './atoms/AccountAvatar'; -import { ConfirmPageSelectors } from './ConfirmPage.selectors'; - -const ConfirmPage = memo(() => { - const { ready } = useTempleClient(); - - if (ready) - return ( -
- -
- -
-
- } - > - - - - ); - - return ; -}); - -interface PayloadContentProps { - tezosNetwork: TezosNetworkEssentials; - accountPkhToConnect: string; - accounts: AccountForTezos[]; - setAccountPkhToConnect: (item: string) => void; - payload: TempleDAppPayload; - error?: any; - modifyFeeAndLimit: ModifyFeeAndLimit; -} - -const PayloadContent: React.FC = ({ - tezosNetwork, - accountPkhToConnect, - accounts, - setAccountPkhToConnect, - payload, - error, - modifyFeeAndLimit -}) => { - const AccountOptionContent = useMemo(() => AccountOptionContentHOC(tezosNetwork), [tezosNetwork]); - - return payload.type === 'connect' ? ( -
-

- - - - - - - -

- - - activeItemId={accountPkhToConnect} - getItemId={getPkh} - items={accounts} - maxHeight="8rem" - onSelect={setAccountPkhToConnect} - OptionIcon={AccountIcon} - OptionContent={AccountOptionContent} - autoFocus - /> -
- ) : ( - - ); -}; - -export default ConfirmPage; - -const CONTAINER_STYLE = { - width: 380, - height: 610 -}; - -const getPkh = (account: AccountForTezos) => account.address; - -const ConfirmDAppForm = memo(() => { - const { getDAppPayload, confirmDAppPermission, confirmDAppOperation, confirmDAppSign } = useTempleClient(); - - const allAccountsStored = useAllAccounts(); - const allAccounts = useMemo( - () => allAccountsStored.map(acc => (isAccountOfActableType(acc) ? getAccountForTezos(acc) : null)).filter(isTruthy), - [allAccountsStored] - ); - - const currentAccountForTezos = useAccountForTezos(); - - const [accountPkhToConnect, setAccountPkhToConnect] = useState( - () => currentAccountForTezos?.address || allAccounts[0]!.address - ); - - const loc = useLocation(); - const id = useMemo(() => { - const usp = new URLSearchParams(loc.search); - const pageId = usp.get('id'); - if (!pageId) { - throw new Error(t('notIdentified')); - } - return pageId; - }, [loc.search]); - - const { data } = useRetryableSWR(id, getDAppPayload, { - suspense: true, - shouldRetryOnError: false, - revalidateOnFocus: false, - revalidateOnReconnect: false - }); - const payload = data!; - const payloadError = data!.error; - - const tezosChainId = useTezosChainIdLoadingValue(payload.networkRpc, true)!; - - const tezosNetwork = useMemo( - () => ({ chainId: tezosChainId, rpcBaseURL: payload.networkRpc }), - [tezosChainId, payload.networkRpc] - ); - - const connectedAccount = useMemo(() => { - const address = payload.type === 'connect' ? accountPkhToConnect : payload.sourcePkh; - - return allAccounts.find(acc => acc.address === address); - }, [allAccounts, payload, accountPkhToConnect]); - - const onConfirm = useCallback( - async (confimed: boolean, modifiedTotalFee?: number, modifiedStorageLimit?: number) => { - switch (payload.type) { - case 'connect': - return confirmDAppPermission(id, confimed, accountPkhToConnect); - - case 'confirm_operations': - return confirmDAppOperation(id, confimed, modifiedTotalFee, modifiedStorageLimit); - - case 'sign': - return confirmDAppSign(id, confimed); - } - }, - [id, payload.type, confirmDAppPermission, confirmDAppOperation, confirmDAppSign, accountPkhToConnect] - ); - - const [error, setError] = useSafeState(null); - const [confirming, setConfirming] = useSafeState(false); - const [declining, setDeclining] = useSafeState(false); - - const revealFee = useMemo(() => { - if ( - payload.type === 'confirm_operations' && - payload.estimates && - payload.estimates.length === payload.opParams.length + 1 - ) { - return payload.estimates[0].suggestedFeeMutez; - } - - return 0; - }, [payload]); - - const [modifiedTotalFeeValue, setModifiedTotalFeeValue] = useSafeState( - (payload.type === 'confirm_operations' && - payload.opParams.reduce((sum, op) => sum + (op.fee ? +op.fee : 0), 0) + revealFee) || - 0 - ); - const [modifiedStorageLimitValue, setModifiedStorageLimitValue] = useSafeState( - (payload.type === 'confirm_operations' && payload.opParams[0].storageLimit) || 0 - ); - - const confirm = useCallback( - async (confirmed: boolean) => { - setError(null); - try { - await onConfirm(confirmed, modifiedTotalFeeValue - revealFee, modifiedStorageLimitValue); - } catch (err: any) { - console.error(err); - - // Human delay. - await delay(); - setError(err); - } - }, - [onConfirm, setError, modifiedTotalFeeValue, modifiedStorageLimitValue, revealFee] - ); - - const handleConfirmClick = useCallback(async () => { - if (confirming || declining) return; - - setConfirming(true); - await confirm(true); - setConfirming(false); - }, [confirming, declining, setConfirming, confirm]); - - const handleDeclineClick = useCallback(async () => { - if (confirming || declining) return; - - setDeclining(true); - await confirm(false); - setDeclining(false); - }, [confirming, declining, setDeclining, confirm]); - - const handleErrorAlertClose = useCallback(() => setError(null), [setError]); - - const content = useMemo(() => { - switch (payload.type) { - case 'connect': - return { - title: t('confirmAction', t('connection').toLowerCase()), - declineActionTitle: t('cancel'), - declineActionTestID: ConfirmPageSelectors.ConnectAction_CancelButton, - confirmActionTitle: error ? t('retry') : t('connect'), - confirmActionTestID: error - ? ConfirmPageSelectors.ConnectAction_RetryButton - : ConfirmPageSelectors.ConnectAction_ConnectButton, - want: ( -

- - {payload.origin} -
- - ]} - /> -

- ) - }; - - case 'confirm_operations': - return { - title: t('confirmAction', t('operations').toLowerCase()), - declineActionTitle: t('reject'), - declineActionTestID: ConfirmPageSelectors.ConfirmOperationsAction_RejectButton, - confirmActionTitle: error ? t('retry') : t('confirm'), - confirmActionTestID: error - ? ConfirmPageSelectors.ConfirmOperationsAction_RetryButton - : ConfirmPageSelectors.ConfirmOperationsAction_ConfirmButton, - want: ( -
-
- - - {payload.appMeta.name} - -
- - {payload.origin} - - ]} - /> -
- ) - }; - - case 'sign': - return { - title: t('confirmAction', t('signAction').toLowerCase()), - declineActionTitle: t('reject'), - declineActionTestID: ConfirmPageSelectors.SignAction_RejectButton, - confirmActionTitle: t('signAction'), - confirmActionTestID: ConfirmPageSelectors.SignAction_SignButton, - want: ( -
-
- - - {payload.appMeta.name} - -
- - {payload.origin} - - ]} - /> -
- ) - }; - } - }, [payload.type, payload.origin, payload.appMeta.name, payload.appMeta.icon, error]); - - const modifiedStorageLimitDisplayed = useMemo( - () => payload.type === 'confirm_operations' && payload.opParams.length < 2, - [payload] - ); - - const modifyFeeAndLimit = useMemo( - () => ({ - totalFee: modifiedTotalFeeValue, - onTotalFeeChange: v => setModifiedTotalFeeValue(v), - storageLimit: modifiedStorageLimitDisplayed ? modifiedStorageLimitValue : null, - onStorageLimitChange: v => setModifiedStorageLimitValue(v) - }), - [ - modifiedTotalFeeValue, - setModifiedTotalFeeValue, - modifiedStorageLimitValue, - setModifiedStorageLimitValue, - modifiedStorageLimitDisplayed - ] - ); - - return ( - -
-
- - {content.title} - - - {payload.type === 'connect' && ( - - )} - - {content.want} - - {payload.type === 'connect' && ( -

- -

- )} - - {error ? ( - - ) : ( - <> - {payload.type !== 'connect' && connectedAccount && ( - - )} - - - - - - )} -
- -
- -
-
- - {content.declineActionTitle} - -
- -
- - {content.confirmActionTitle} - -
-
- - -
- - ); -}); - -const AccountIcon: FC> = ({ item }) => ( - -); - -const AccountOptionContentHOC = (tezosNetwork: TezosNetworkEssentials) => - memo>(({ item: acc }) => { - const { symbol } = getTezosGasMetadata(tezosNetwork.chainId); - - return ( - <> -
- {acc.name} - -
- -
-
- -
- - - {bal => ( -
- {bal} - - {symbol} - -
- )} -
-
- - ); - }); diff --git a/src/app/ConfirmPage/confirm-dapp-form.tsx b/src/app/ConfirmPage/confirm-dapp-form.tsx new file mode 100644 index 0000000000..58ae2fa2e0 --- /dev/null +++ b/src/app/ConfirmPage/confirm-dapp-form.tsx @@ -0,0 +1,206 @@ +import React, { ReactNode, memo, useCallback, useMemo, useState } from 'react'; + +import clsx from 'clsx'; + +import { Alert, Anchor, IconBase } from 'app/atoms'; +import ConfirmLedgerOverlay from 'app/atoms/ConfirmLedgerOverlay'; +import DAppLogo from 'app/atoms/DAppLogo'; +import { Logo } from 'app/atoms/Logo'; +import { CloseButton, PageModal } from 'app/atoms/PageModal'; +import { ActionsButtonsBox } from 'app/atoms/PageModal/actions-buttons-box'; +import { ScrollView } from 'app/atoms/PageModal/scroll-view'; +import { ProgressAndNumbers } from 'app/atoms/ProgressAndNumbers'; +import { StyledButton } from 'app/atoms/StyledButton'; +import { ReactComponent as LinkIcon } from 'app/icons/base/link.svg'; +import { ReactComponent as OutLinkIcon } from 'app/icons/base/outLink.svg'; +import { AccountsModalContent } from 'app/templates/AccountsModalContent'; +import { T, t } from 'lib/i18n'; +import { useTempleClient } from 'lib/temple/front'; +import { StoredAccount, TempleAccountType, TempleDAppPayload } from 'lib/temple/types'; +import { useBooleanState, useSafeState } from 'lib/ui/hooks'; +import { delay } from 'lib/utils'; +import { useCurrentAccountId } from 'temple/front'; + +import { ConfirmPageSelectors } from './selectors'; + +interface ConfirmDAppFormProps { + accounts: StoredAccount[]; + payload: TempleDAppPayload; + onConfirm: (confirmed: boolean, selectedAccount: StoredAccount) => Promise; + children: (openAccountsModal: EmptyFn, selectedAccount: StoredAccount) => ReactNode | ReactNode[]; +} + +export const ConfirmDAppForm = memo(({ accounts, payload, onConfirm, children }) => { + const [accountsModalIsOpen, openAccountsModal, closeAccountsModal] = useBooleanState(false); + const [bottomEdgeIsVisible, setBottomEdgeIsVisible] = useState(true); + const [isConfirming, setIsConfirming] = useSafeState(false); + const [isDeclining, setIsDeclining] = useSafeState(false); + const [error, setError] = useSafeState(null); + + const currentAccountId = useCurrentAccountId(); + const selectedAccountId = useMemo( + () => (accounts.some(account => account.id === currentAccountId) ? currentAccountId : accounts[0].id), + [accounts, currentAccountId] + ); + const selectedAccount = useMemo( + () => accounts.find(account => account.id === selectedAccountId)!, + [accounts, selectedAccountId] + ); + const { dAppQueueCounters } = useTempleClient(); + const { length: requestsLeft, maxLength: totalRequestsCount } = dAppQueueCounters; + + const confirm = useCallback( + async (confirmed: boolean) => { + setError(null); + try { + await onConfirm(confirmed, selectedAccount); + } catch (err: any) { + console.error(err); + + // Human delay. + await delay(); + setError(err); + } + }, + [onConfirm, selectedAccount, setError] + ); + + const handleConfirmClick = useCallback(async () => { + if (isConfirming || isDeclining) return; + + setIsConfirming(true); + await confirm(true); + setIsDeclining(false); + }, [confirm, isConfirming, isDeclining, setIsConfirming, setIsDeclining]); + + const handleDeclineClick = useCallback(async () => { + if (isConfirming || isDeclining) return; + + setIsConfirming(true); + await confirm(false); + setIsDeclining(false); + }, [confirm, isConfirming, isDeclining, setIsConfirming, setIsDeclining]); + + const handleErrorAlertClose = useCallback(() => setError(null), [setError]); + + const { title, confirmButtonName, confirmTestID, declineTestID } = useMemo(() => { + switch (payload.type) { + case 'connect': + return { + title: , + confirmButtonName: , + confirmTestID: error + ? ConfirmPageSelectors.ConnectAction_RetryButton + : ConfirmPageSelectors.ConnectAction_ConnectButton, + declineTestID: ConfirmPageSelectors.ConnectAction_CancelButton + }; + case 'confirm_operations': + return { + title: } />, + confirmButtonName: , + confirmTestID: error + ? ConfirmPageSelectors.ConfirmOperationsAction_RetryButton + : ConfirmPageSelectors.ConfirmOperationsAction_ConfirmButton, + declineTestID: ConfirmPageSelectors.ConfirmOperationsAction_RejectButton + }; + default: + return { + title: , + confirmButtonName: , + confirmTestID: ConfirmPageSelectors.SignAction_SignButton, + declineTestID: ConfirmPageSelectors.SignAction_RejectButton + }; + } + }, [error, payload.type]); + + return ( + 1 ? ( + + ) : null + } + titleRight={accountsModalIsOpen ? : null} + animated={false} + onRequestClose={closeAccountsModal} + > + {accountsModalIsOpen ? ( + + ) : ( + <> + +
+
+
+ +
+
+ +
+
+ +
+
+ + + {payload.appMeta.name} + + +
+ + {error && ( + + )} + + {children(openAccountsModal, selectedAccount)} +
+ + + + + + + + {confirmButtonName} + + + + + + )} +
+ ); +}); diff --git a/src/app/ConfirmPage/evm-confirm-dapp-form.tsx b/src/app/ConfirmPage/evm-confirm-dapp-form.tsx new file mode 100644 index 0000000000..389b155896 --- /dev/null +++ b/src/app/ConfirmPage/evm-confirm-dapp-form.tsx @@ -0,0 +1,69 @@ +import React, { memo, useCallback, useMemo } from 'react'; + +import { CustomEvmChainIdContext } from 'lib/analytics'; +import { useTempleClient } from 'lib/temple/front/client'; +import { StoredAccount, TempleEvmDAppPayload } from 'lib/temple/types'; +import { getAccountForEvm, isAccountOfActableType } from 'temple/accounts'; +import { useAllAccounts, useAllEvmChains } from 'temple/front'; + +import { ConfirmDAppForm } from './confirm-dapp-form'; +import { EvmPayloadContent } from './payload-content'; + +interface EvmConfirmDAppFormProps { + payload: TempleEvmDAppPayload; + id: string; +} + +export const EvmConfirmDAppForm = memo(({ payload, id }) => { + const { confirmDAppPermission, confirmDAppSign } = useTempleClient(); + + const allAccountsStored = useAllAccounts(); + const allAccounts = useMemo( + () => allAccountsStored.filter(acc => isAccountOfActableType(acc) && getAccountForEvm(acc)), + [allAccountsStored] + ); + + const evmChains = useAllEvmChains(); + const payloadError = payload!.error; + const chainId = Number(payload.chainId); + const rpcBaseURL = evmChains[chainId].rpcBaseURL; + + const network = useMemo(() => ({ chainId, rpcBaseURL }), [chainId, rpcBaseURL]); + + const handleConfirm = useCallback( + async (confimed: boolean, selectedAccount: StoredAccount) => { + const accountPkh = getAccountForEvm(selectedAccount)!.address; + switch (payload.type) { + case 'connect': + return confirmDAppPermission(id, confimed, accountPkh); + + case 'personal_sign': + case 'sign_typed': + return confirmDAppSign(id, confimed); + } + }, + [id, payload.type, confirmDAppPermission, confirmDAppSign] + ); + + const renderPayload = useCallback( + (openAccountsModal: EmptyFn, selectedAccount: StoredAccount) => ( + + ), + [network, payload, payloadError] + ); + + return ( + + + {renderPayload} + + + ); +}); diff --git a/src/app/ConfirmPage/index.tsx b/src/app/ConfirmPage/index.tsx new file mode 100644 index 0000000000..f3814d753e --- /dev/null +++ b/src/app/ConfirmPage/index.tsx @@ -0,0 +1,79 @@ +import React, { memo, useMemo } from 'react'; + +import clsx from 'clsx'; + +import { FADABLE_CONTENT_CLASSNAME } from 'app/a11y/content-fader'; +import Spinner from 'app/atoms/Spinner/Spinner'; +import { SuspenseContainer } from 'app/atoms/SuspenseContainer'; +import { LAYOUT_CONTAINER_CLASSNAME } from 'app/layouts/containers'; +import Unlock from 'app/pages/Unlock/Unlock'; +import { t } from 'lib/i18n'; +import { useRetryableSWR } from 'lib/swr'; +import { useTempleClient } from 'lib/temple/front/client'; +import { TempleDAppPayload } from 'lib/temple/types'; +import { useLocation } from 'lib/woozie'; +import { TempleChainKind } from 'temple/types'; + +import { EvmConfirmDAppForm } from './evm-confirm-dapp-form'; +import { TezosConfirmDAppForm } from './tezos-confirm-dapp-form'; + +const ConfirmPage = memo(() => { + const { ready } = useTempleClient(); + + if (!ready) { + return ; + } + + return ( +
+ +
+ +
+
+ } + > + + +
+ ); +}); + +export default ConfirmPage; + +const ConfirmDAppForm = () => { + const { getDAppPayload } = useTempleClient(); + + const loc = useLocation(); + const id = useMemo(() => { + const usp = new URLSearchParams(loc.search); + const pageId = usp.get('id'); + if (!pageId) { + throw new Error(t('notIdentified')); + } + return pageId; + }, [loc.search]); + + const { data } = useRetryableSWR(id, getDAppPayload, { + suspense: true, + shouldRetryOnError: false, + revalidateOnFocus: false, + revalidateOnReconnect: false + }); + const payload = data!; + + return payload.chainType === TempleChainKind.EVM ? ( + + ) : ( + + ); +}; diff --git a/src/app/ConfirmPage/payload-content.tsx b/src/app/ConfirmPage/payload-content.tsx new file mode 100644 index 0000000000..10e42e08e5 --- /dev/null +++ b/src/app/ConfirmPage/payload-content.tsx @@ -0,0 +1,89 @@ +import React, { FC, memo } from 'react'; + +import { IconBase } from 'app/atoms'; +import { ReactComponent as UnlockFillIcon } from 'app/icons/base/unlock_fill.svg'; +import { AccountCard } from 'app/templates/AccountCard'; +import { EvmOperationView } from 'app/templates/EvmOperationView'; +import { ModifyFeeAndLimit } from 'app/templates/ExpensesView/ExpensesView'; +import TezosOperationView from 'app/templates/TezosOperationView'; +import { T, TID } from 'lib/i18n'; +import { StoredAccount, TempleEvmDAppPayload, TempleTezosDAppPayload } from 'lib/temple/types'; +import { NetworkEssentials } from 'temple/networks'; +import { TempleChainKind } from 'temple/types'; + +type DAppPayload = T extends TempleChainKind.EVM + ? TempleEvmDAppPayload + : TempleTezosDAppPayload; + +type OperationDAppPayload = Exclude, { type: 'connect' }>; + +interface OperationViewProps { + network: NetworkEssentials; + payload: OperationDAppPayload; + error?: any; + modifyFeeAndLimit?: ModifyFeeAndLimit; +} + +interface PayloadContentProps extends Omit, 'payload'> { + account: StoredAccount; + payload: DAppPayload; + openAccountsModal: EmptyFn; +} + +const permissionsDescriptionsI18nKeys: TID[] = [ + 'viewWalletPermissionDescription', + 'transactionsPermissionDescription', + 'signingPermissionDescription' +]; + +const ConnectView = memo(() => ( +
+

+ +

+ {permissionsDescriptionsI18nKeys.map(key => ( +
+ + + +
+ + + + + +
+
+ ))} +
+)); + +const PayloadContentHOC = + (OperationView: FC>) => + ({ network, payload, error, modifyFeeAndLimit, account, openAccountsModal }: PayloadContentProps) => + ( +
+ + {payload.type === 'connect' ? ( + + ) : ( + } + error={error} + modifyFeeAndLimit={modifyFeeAndLimit} + /> + )} +
+ ); + +export const TezosPayloadContent = PayloadContentHOC(TezosOperationView); + +export const EvmPayloadContent = PayloadContentHOC(EvmOperationView); diff --git a/src/app/ConfirmPage.selectors.ts b/src/app/ConfirmPage/selectors.ts similarity index 100% rename from src/app/ConfirmPage.selectors.ts rename to src/app/ConfirmPage/selectors.ts diff --git a/src/app/ConfirmPage/tezos-confirm-dapp-form.tsx b/src/app/ConfirmPage/tezos-confirm-dapp-form.tsx new file mode 100644 index 0000000000..a59c1790ab --- /dev/null +++ b/src/app/ConfirmPage/tezos-confirm-dapp-form.tsx @@ -0,0 +1,125 @@ +import React, { memo, useCallback, useMemo } from 'react'; + +import { ModifyFeeAndLimit } from 'app/templates/ExpensesView/ExpensesView'; +import { CustomTezosChainIdContext } from 'lib/analytics'; +import { useTempleClient } from 'lib/temple/front/client'; +import { StoredAccount, TempleTezosDAppPayload } from 'lib/temple/types'; +import { useSafeState } from 'lib/ui/hooks'; +import { getAccountForTezos, isAccountOfActableType } from 'temple/accounts'; +import { useAllAccounts, useTezosChainIdLoadingValue } from 'temple/front'; + +import { ConfirmDAppForm } from './confirm-dapp-form'; +import { TezosPayloadContent } from './payload-content'; + +interface TezosConfirmDAppFormProps { + payload: TempleTezosDAppPayload; + id: string; +} + +export const TezosConfirmDAppForm = memo(({ payload, id }) => { + const { confirmDAppPermission, confirmDAppOperation, confirmDAppSign } = useTempleClient(); + + const allAccountsStored = useAllAccounts(); + const allAccounts = useMemo( + () => allAccountsStored.filter(acc => isAccountOfActableType(acc) && getAccountForTezos(acc)), + [allAccountsStored] + ); + + const payloadError = payload!.error; + const tezosChainId = useTezosChainIdLoadingValue(payload.networkRpc, true)!; + + const network = useMemo( + () => ({ chainId: tezosChainId, rpcBaseURL: payload.networkRpc }), + [tezosChainId, payload.networkRpc] + ); + + const revealFee = useMemo(() => { + if ( + payload.type === 'confirm_operations' && + payload.estimates && + payload.estimates.length === payload.opParams.length + 1 + ) { + return payload.estimates[0].suggestedFeeMutez; + } + + return 0; + }, [payload]); + + const [modifiedTotalFeeValue, setModifiedTotalFeeValue] = useSafeState( + (payload.type === 'confirm_operations' && + payload.opParams.reduce((sum, op) => sum + (op.fee ? +op.fee : 0), 0) + revealFee) || + 0 + ); + const [modifiedStorageLimitValue, setModifiedStorageLimitValue] = useSafeState( + (payload.type === 'confirm_operations' && payload.opParams[0].storageLimit) || 0 + ); + + const modifiedStorageLimitDisplayed = useMemo( + () => payload.type === 'confirm_operations' && payload.opParams.length < 2, + [payload] + ); + + const modifyFeeAndLimit = useMemo( + () => ({ + totalFee: modifiedTotalFeeValue, + onTotalFeeChange: v => setModifiedTotalFeeValue(v), + storageLimit: modifiedStorageLimitDisplayed ? modifiedStorageLimitValue : null, + onStorageLimitChange: v => setModifiedStorageLimitValue(v) + }), + [ + modifiedTotalFeeValue, + setModifiedTotalFeeValue, + modifiedStorageLimitValue, + setModifiedStorageLimitValue, + modifiedStorageLimitDisplayed + ] + ); + + const handleConfirm = useCallback( + async (confimed: boolean, selectedAccount: StoredAccount) => { + const accountPkh = getAccountForTezos(selectedAccount)!.address; + switch (payload.type) { + case 'connect': + return confirmDAppPermission(id, confimed, accountPkh); + + case 'confirm_operations': + return confirmDAppOperation(id, confimed, modifiedTotalFeeValue - revealFee, modifiedStorageLimitValue); + + case 'sign': + return confirmDAppSign(id, confimed); + } + }, + [ + payload.type, + confirmDAppPermission, + id, + confirmDAppOperation, + modifiedTotalFeeValue, + revealFee, + modifiedStorageLimitValue, + confirmDAppSign + ] + ); + + const renderPayload = useCallback( + (openAccountsModal: EmptyFn, selectedAccount: StoredAccount) => ( + + ), + [modifyFeeAndLimit, network, payload, payloadError] + ); + + return ( + + + {renderPayload} + + + ); +}); diff --git a/src/app/atoms/DAppLogo/index.tsx b/src/app/atoms/DAppLogo/index.tsx index fa8c17a019..1a79d3bb52 100644 --- a/src/app/atoms/DAppLogo/index.tsx +++ b/src/app/atoms/DAppLogo/index.tsx @@ -1,12 +1,8 @@ import React, { CSSProperties, memo, useMemo } from 'react'; -import clsx from 'clsx'; - import { ImageStacked } from 'lib/ui/ImageStacked'; -import { IconBase } from '../IconBase'; - -import { ReactComponent as PlugSvg } from './plug.svg'; +import { ReactComponent as UnknownDAppIcon } from './unknown-dapp.svg'; interface DAppLogoProps { origin: string; @@ -21,11 +17,7 @@ const DAppLogo = memo(({ origin, size, icon, className, style }) const styleMemo = useMemo(() => ({ width: size, height: size, ...style }), [style, size]); - const placeholder = ( -
- -
- ); + const placeholder = ; return ( - - diff --git a/src/app/atoms/DAppLogo/unknown-dapp.svg b/src/app/atoms/DAppLogo/unknown-dapp.svg new file mode 100644 index 0000000000..72c312d10d --- /dev/null +++ b/src/app/atoms/DAppLogo/unknown-dapp.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + diff --git a/src/app/atoms/PageModal/actions-buttons-box.tsx b/src/app/atoms/PageModal/actions-buttons-box.tsx index deeee78171..795e0ac18d 100644 --- a/src/app/atoms/PageModal/actions-buttons-box.tsx +++ b/src/app/atoms/PageModal/actions-buttons-box.tsx @@ -62,7 +62,7 @@ export const ActionsButtonsBox = memo(
ReactElement); @@ -37,23 +36,24 @@ export const PageModal: FC = ({ title, opened, headerClassName, - shouldShowBackButton, - shouldShowCloseButton = true, + titleLeft, onRequestClose, - onGoBack, + titleRight = , children, testID, animated = true, contentPadding = false }) => { - const { fullPage } = useAppEnv(); + const { fullPage, confirmWindow } = useAppEnv(); const testnetModeEnabled = useTestnetModeEnabledSelector(); const baseOverlayClassNames = useMemo(() => { + if (confirmWindow) return 'pt-4'; + if (testnetModeEnabled) return fullPage ? 'pt-19 pb-8' : 'pt-10'; return fullPage ? 'pt-13 pb-8' : 'pt-4'; - }, [fullPage, testnetModeEnabled]); + }, [confirmWindow, fullPage, testnetModeEnabled]); return ( = ({ className={{ base: clsx( LAYOUT_CONTAINER_CLASSNAME, - 'h-full flex flex-col bg-white overflow-hidden', + 'h-full flex flex-col bg-white overflow-hidden outline-none', fullPage ? 'rounded-lg' : 'rounded-t-lg', ModStyles.base, animated && 'ease-out duration-300' @@ -81,20 +81,12 @@ export const PageModal: FC = ({ onRequestClose={onRequestClose} testId={testID} > -
-
- {shouldShowBackButton && ( - - )} -
+
+
{titleLeft}
{title}
-
- {shouldShowCloseButton && ( - - )} -
+
{titleRight}
@@ -105,3 +97,11 @@ export const PageModal: FC = ({ ); }; + +export const BackButton = memo<{ onClick?: EmptyFn }>(({ onClick }) => ( + +)); + +export const CloseButton = memo<{ onClick?: EmptyFn }>(({ onClick }) => ( + +)); diff --git a/src/app/atoms/ProgressAndNumbers.tsx b/src/app/atoms/ProgressAndNumbers.tsx new file mode 100644 index 0000000000..df3df351b9 --- /dev/null +++ b/src/app/atoms/ProgressAndNumbers.tsx @@ -0,0 +1,22 @@ +import React, { memo } from 'react'; + +interface ProgressAndNumbersProps { + progress: number; + total: number; +} + +export const ProgressAndNumbers = memo(({ progress, total }) => + total === 0 ? null : ( +
+

+ {progress}/{total} +

+
+
+
+
+ ) +); diff --git a/src/app/atoms/ReadOnlySecretField.tsx b/src/app/atoms/ReadOnlySecretField.tsx index 7e537416b8..c4a340e731 100644 --- a/src/app/atoms/ReadOnlySecretField.tsx +++ b/src/app/atoms/ReadOnlySecretField.tsx @@ -77,7 +77,7 @@ export const ReadOnlySecretField: FC = ({ onBlur={handleCopyButtonBlur} > - + diff --git a/src/app/atoms/Spinner/Spinner.tsx b/src/app/atoms/Spinner/Spinner.tsx index 08e61e6985..c793f85b72 100644 --- a/src/app/atoms/Spinner/Spinner.tsx +++ b/src/app/atoms/Spinner/Spinner.tsx @@ -19,17 +19,17 @@ const Spinner = memo(({ theme = 'primary', className, ...rest }) = (() => { switch (theme) { case 'primary': - return 'bg-primary-orange'; + return 'bg-primary'; case 'white': return 'bg-white shadow-sm'; case 'dark-gray': - return 'bg-gray-600'; + return 'bg-grey-1'; case 'gray': default: - return 'bg-gray-400'; + return 'bg-grey-3'; } })(), styles['bounce'], diff --git a/src/app/atoms/TextCopyButton.tsx b/src/app/atoms/TextCopyButton.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/icons/base/unlock_fill.svg b/src/app/icons/base/unlock_fill.svg new file mode 100644 index 0000000000..c0be6f5b73 --- /dev/null +++ b/src/app/icons/base/unlock_fill.svg @@ -0,0 +1,6 @@ + + + diff --git a/src/app/icons/layers.svg b/src/app/icons/layers.svg deleted file mode 100644 index 04c985dc3e..0000000000 --- a/src/app/icons/layers.svg +++ /dev/null @@ -1 +0,0 @@ - Layers diff --git a/src/app/layouts/PageLayout/index.tsx b/src/app/layouts/PageLayout/index.tsx index 476aac7e45..6204206a5e 100644 --- a/src/app/layouts/PageLayout/index.tsx +++ b/src/app/layouts/PageLayout/index.tsx @@ -65,7 +65,7 @@ const PageLayout: FC> = ({ topEdgeThreshold, ...headerProps }) => { - const { fullPage } = useAppEnv(); + const { fullPage, confirmWindow } = useAppEnv(); const { ready } = useTempleClient(); const [shouldBackupMnemonic] = useStorage(SHOULD_BACKUP_MNEMONIC_STORAGE_KEY, false); @@ -79,7 +79,7 @@ const PageLayout: FC> = ({ !IS_MISES_BROWSER && } -
+
{
diff --git a/src/app/pages/AccountSettings/reveal-private-key-modal/index.tsx b/src/app/pages/AccountSettings/reveal-private-key-modal/index.tsx index 0db1ca5fa9..08391e1b5a 100644 --- a/src/app/pages/AccountSettings/reveal-private-key-modal/index.tsx +++ b/src/app/pages/AccountSettings/reveal-private-key-modal/index.tsx @@ -1,6 +1,6 @@ import React, { memo, useCallback, useState } from 'react'; -import { PageModal } from 'app/atoms/PageModal'; +import { BackButton, PageModal } from 'app/atoms/PageModal'; import { ScrollView } from 'app/atoms/PageModal/scroll-view'; import { t } from 'lib/i18n'; @@ -23,8 +23,7 @@ export const RevealPrivateKeyModal = memo(({ private title={t('revealPrivateKey')} onRequestClose={onClose} opened - shouldShowBackButton={Boolean(selectedPrivateKey)} - onGoBack={unselectPrivateKey} + titleLeft={selectedPrivateKey ? : undefined} > {selectedPrivateKey ? ( diff --git a/src/app/pages/Home/OtherComponents/Tokens/components/AddTokenModal/index.tsx b/src/app/pages/Home/OtherComponents/Tokens/components/AddTokenModal/index.tsx index f255e75a16..6688a34413 100644 --- a/src/app/pages/Home/OtherComponents/Tokens/components/AddTokenModal/index.tsx +++ b/src/app/pages/Home/OtherComponents/Tokens/components/AddTokenModal/index.tsx @@ -1,6 +1,6 @@ import React, { memo, useCallback, useState } from 'react'; -import { PageModal } from 'app/atoms/PageModal'; +import { BackButton, PageModal } from 'app/atoms/PageModal'; import { useBooleanState } from 'lib/ui/hooks'; import { OneOfChains, useAccountAddressForTezos, useEthereumMainnetChain, useTezosMainnetChain } from 'temple/front'; @@ -46,8 +46,7 @@ export const AddTokenModal = memo(({ forCollectible, opened, onRequestClo : undefined} onRequestClose={totalClose} > {isNetworkSelectOpened ? ( diff --git a/src/app/pages/ImportWallet.tsx b/src/app/pages/ImportWallet.tsx index 4056d9cf85..32ce7eec91 100644 --- a/src/app/pages/ImportWallet.tsx +++ b/src/app/pages/ImportWallet.tsx @@ -1,6 +1,6 @@ import React, { memo, useCallback, useState } from 'react'; -import { PageModal } from 'app/atoms/PageModal'; +import { BackButton, PageModal } from 'app/atoms/PageModal'; import { SuspenseContainer } from 'app/atoms/SuspenseContainer'; import PageLayout from 'app/layouts/PageLayout'; import { CreatePasswordForm } from 'app/templates/CreatePasswordForm'; @@ -34,9 +34,8 @@ export const ImportWallet = memo(() => { : undefined} onRequestClose={goHome} - onGoBack={handleGoBack} > {shouldShowPasswordForm ? ( diff --git a/src/app/pages/Receive/Receive.tsx b/src/app/pages/Receive/Receive.tsx index 0ad30a4a05..0b3f3a7f21 100644 --- a/src/app/pages/Receive/Receive.tsx +++ b/src/app/pages/Receive/Receive.tsx @@ -3,7 +3,7 @@ import React, { memo, useCallback, useState } from 'react'; import { PageTitle } from 'app/atoms'; import { useSearchParamsBoolean } from 'app/hooks/use-search-params-boolean'; import PageLayout from 'app/layouts/PageLayout'; -import { AccountsModal } from 'app/templates/AppHeader/AccountsModal'; +import { AccountsModal } from 'app/templates/AccountsModal'; import { T, t } from 'lib/i18n'; import { useAccount, useAccountAddressForEvm, useAccountAddressForTezos } from 'temple/front'; import { TempleChainKind } from 'temple/types'; diff --git a/src/app/pages/Send/modals/ConfirmSend/BaseContent.tsx b/src/app/pages/Send/modals/ConfirmSend/BaseContent.tsx index 7f0e3de7e5..35b60f27ca 100644 --- a/src/app/pages/Send/modals/ConfirmSend/BaseContent.tsx +++ b/src/app/pages/Send/modals/ConfirmSend/BaseContent.tsx @@ -141,7 +141,7 @@ export const BaseContent = ({
- + diff --git a/src/app/pages/Send/modals/ConfirmSend/index.tsx b/src/app/pages/Send/modals/ConfirmSend/index.tsx index af8bde7406..e99afd0c25 100644 --- a/src/app/pages/Send/modals/ConfirmSend/index.tsx +++ b/src/app/pages/Send/modals/ConfirmSend/index.tsx @@ -15,7 +15,7 @@ interface ConfirmSendModalProps { } export const ConfirmSendModal: FC = ({ opened, onRequestClose, reviewData }) => ( - + {reviewData ? ( isEvmReviewData(reviewData) ? ( diff --git a/src/app/pages/Send/modals/SelectAccount/index.tsx b/src/app/pages/Send/modals/SelectAccount/index.tsx index 41c6f506f0..b8d54a1cf9 100644 --- a/src/app/pages/Send/modals/SelectAccount/index.tsx +++ b/src/app/pages/Send/modals/SelectAccount/index.tsx @@ -3,17 +3,21 @@ import React, { memo, Suspense, useCallback, useEffect, useMemo, useState } from import clsx from 'clsx'; import { useDebounce } from 'use-debounce'; -import { HashShortView, IconBase, Name } from 'app/atoms'; +import { HashShortView, IconBase } from 'app/atoms'; import { AccountAvatar } from 'app/atoms/AccountAvatar'; import { EmptyState } from 'app/atoms/EmptyState'; import { PageModal } from 'app/atoms/PageModal'; import { RadioButton } from 'app/atoms/RadioButton'; import { ReactComponent as CopyIcon } from 'app/icons/base/copy.svg'; import { SpinnerSection } from 'app/pages/Send/form/SpinnerSection'; +import { + AccountsGroup as GenericAccountsGroup, + AccountsGroupProps as GenericAccountsGroupProps +} from 'app/templates/AccountsGroup'; import { SearchBarField } from 'app/templates/SearchField'; import { toastSuccess } from 'app/toaster'; import { T } from 'lib/i18n'; -import { StoredAccount, TempleContact } from 'lib/temple/types'; +import { TempleContact } from 'lib/temple/types'; import { useScrollIntoViewOnMount } from 'lib/ui/use-scroll-into-view'; import { searchAndFilterItems } from 'lib/utils/search-items'; import { getAccountAddressForEvm, getAccountAddressForTezos } from 'temple/accounts'; @@ -125,9 +129,7 @@ export const SelectAccountModal = memo( } ); -interface AccountsGroupProps { - title: string; - accounts: StoredAccount[]; +interface AccountsGroupProps extends Omit { selectedAccountAddress: string; attractSelectedAccount: boolean; onAccountSelect: (address: string) => void; @@ -136,27 +138,23 @@ interface AccountsGroupProps { const AccountsGroup = memo( ({ title, accounts, selectedAccountAddress, attractSelectedAccount, onAccountSelect, evm = false }) => ( -
- {title} - -
- {accounts.map(account => { - const address = evm ? getAccountAddressForEvm(account) : getAccountAddressForTezos(account); - - return ( - - ); - })} -
-
+ + {account => { + const address = evm ? getAccountAddressForEvm(account) : getAccountAddressForTezos(account); + + return ( + + ); + }} + ) ); @@ -172,25 +170,19 @@ const AddressBookGroup = memo( if (!contacts.length) return null; return ( -
- - - - -
- {contacts.map(contact => ( - - ))} -
-
+ title={} accounts={contacts}> + {contact => ( + + )} + ); } ); diff --git a/src/app/pages/Send/modals/SelectAsset/index.tsx b/src/app/pages/Send/modals/SelectAsset/index.tsx index 7d11601821..b822f3e39c 100644 --- a/src/app/pages/Send/modals/SelectAsset/index.tsx +++ b/src/app/pages/Send/modals/SelectAsset/index.tsx @@ -1,31 +1,16 @@ import React, { memo, useCallback, useState, MouseEvent, useMemo, Suspense, useEffect } from 'react'; -import clsx from 'clsx'; import { useDebounce } from 'use-debounce'; import { Button, IconBase } from 'app/atoms'; -import { ActionsDropdownPopup } from 'app/atoms/ActionsDropdown'; -import { EmptyState } from 'app/atoms/EmptyState'; -import { Size } from 'app/atoms/IconBase'; -import { EvmNetworkLogo, TezosNetworkLogo } from 'app/atoms/NetworkLogo'; import { PageModal } from 'app/atoms/PageModal'; -import { ReactComponent as Browse } from 'app/icons/base/browse.svg'; import { ReactComponent as CompactDown } from 'app/icons/base/compact_down.svg'; import { SpinnerSection } from 'app/pages/Send/form/SpinnerSection'; import { useAssetsFilterOptionsSelector } from 'app/store/assets-filter-options/selectors'; import { FilterChain } from 'app/store/assets-filter-options/state'; +import { NetworkPopper } from 'app/templates/network-popper'; import { SearchBarField } from 'app/templates/SearchField'; -import Popper, { PopperRenderProps } from 'lib/ui/Popper'; -import { useScrollIntoViewOnMount } from 'lib/ui/use-scroll-into-view'; -import { - OneOfChains, - useAccountAddressForEvm, - useAccountAddressForTezos, - useAllEvmChains, - useAllTezosChains, - useEnabledEvmChains, - useEnabledTezosChains -} from 'temple/front'; +import { useAccountAddressForEvm, useAccountAddressForTezos } from 'temple/front'; import { TempleChainKind } from 'temple/types'; import { EvmAssetsList } from './EvmAssetsList'; @@ -142,167 +127,22 @@ export const SelectAssetModal = memo(({ onAssetSelect, op ); }); -const ALL_NETWORKS = 'All Networks'; - interface FilterNetworkPopperProps { selectedOption: FilterChain; onOptionSelect: (filterChain: FilterChain) => void; } -const FilterNetworkPopper = memo(({ selectedOption, onOptionSelect }) => { - const allTezosChains = useAllTezosChains(); - const allEvmChains = useAllEvmChains(); - - const selectedOptionName = useMemo(() => { - if (!selectedOption) return ALL_NETWORKS; - - if (selectedOption.kind === TempleChainKind.Tezos) { - return allTezosChains[selectedOption.chainId]?.name; - } - - return allEvmChains[selectedOption.chainId]?.name; - }, [allEvmChains, allTezosChains, selectedOption]); - - return ( - ( - - )} - > - {({ ref, toggleOpened }) => ( - - )} - - ); -}); - -interface FilterNetworkDropdownProps extends FilterNetworkPopperProps, PopperRenderProps {} - -const FilterNetworkDropdown = memo( - ({ opened, setOpened, selectedOption, onOptionSelect }) => { - const accountTezAddress = useAccountAddressForTezos(); - const accountEvmAddress = useAccountAddressForEvm(); - - const tezosChains = useEnabledTezosChains(); - const evmChains = useEnabledEvmChains(); - - const [searchValue, setSearchValue] = useState(''); - const [searchValueDebounced] = useDebounce(searchValue, 300); - - const [attractSelectedNetwork, setAttractSelectedNetwork] = useState(true); - - useEffect(() => { - if (searchValueDebounced) setAttractSelectedNetwork(false); - else if (!opened) setAttractSelectedNetwork(true); - }, [opened, searchValueDebounced]); - - const networks = useMemo( - () => [ALL_NETWORKS, ...(accountTezAddress ? tezosChains : []), ...(accountEvmAddress ? evmChains : [])], - [accountEvmAddress, accountTezAddress, evmChains, tezosChains] - ); - - const filteredNetworks = useMemo( - () => - searchValueDebounced.length - ? searchAndFilterNetworksByName(networks, searchValueDebounced) - : networks, - [searchValueDebounced, networks] - ); - - return ( - -
- -
- -
- {filteredNetworks.length === 0 && } - - {filteredNetworks.map(network => ( - { - onOptionSelect(typeof network === 'string' ? null : network); - setOpened(false); - }} - /> - ))} -
-
- ); - } -); - -type Network = OneOfChains | string; - -interface FilterOptionProps { - network: Network; - activeNetwork: FilterChain; - attractSelf: boolean; - iconSize?: Size; - onClick?: EmptyFn; -} - -const FilterOption = memo(({ network, activeNetwork, attractSelf, iconSize = 24, onClick }) => { - const isAllNetworks = typeof network === 'string'; - - const active = isAllNetworks ? activeNetwork === null : network.chainId === activeNetwork?.chainId; - - const elemRef = useScrollIntoViewOnMount(active && attractSelf); - - const Icon = useMemo(() => { - if (isAllNetworks) return ; - - if (network.kind === TempleChainKind.Tezos) return ; - - if (network.kind === TempleChainKind.EVM) - return ; - - return null; - }, [isAllNetworks, network, iconSize]); - - const handleClick = useCallback(() => { - if (active) return; - - onClick?.(); - }, [active, onClick]); - - return ( -
- {isAllNetworks ? ALL_NETWORKS : network.name} - {Icon} -
- ); -}); - -type SearchNetwork = string | { name: string }; - -/** @deprecated // Apply searchAndFilterChains() instead */ -const searchAndFilterNetworksByName = (networks: T[], searchValue: string) => { - const preparedSearchValue = searchValue.trim().toLowerCase(); - - return networks.filter(network => { - if (typeof network === 'string') return network.toLowerCase().includes(preparedSearchValue); - - return network.name.toLowerCase().includes(preparedSearchValue); - }); -}; +const FilterNetworkPopper = memo(({ selectedOption, onOptionSelect }) => ( + + {({ ref, toggleOpened, selectedOptionName }) => ( + + )} + +)); diff --git a/src/app/pages/Settings/DApps.tsx b/src/app/pages/Settings/DApps.tsx index 406fc1b097..ec1b2d4d16 100644 --- a/src/app/pages/Settings/DApps.tsx +++ b/src/app/pages/Settings/DApps.tsx @@ -6,12 +6,19 @@ import { EmptyState } from 'app/atoms/EmptyState'; import { ActionsButtonsBox } from 'app/atoms/PageModal/actions-buttons-box'; import { ScrollView } from 'app/atoms/ScrollView'; import { StyledButton } from 'app/atoms/StyledButton'; +import { ReactComponent as CompactDown } from 'app/icons/base/compact_down.svg'; import { ReactComponent as UnlinkSvg } from 'app/icons/base/unlink.svg'; -import type { TezosDAppSession } from 'app/storage/dapps'; +import { isTezosDAppSession, type DAppSession } from 'app/storage/dapps'; +import { FilterChain } from 'app/store/assets-filter-options/state'; +import { useActiveTabUrlOrigin } from 'app/templates/DAppConnection/use-active-tab'; import { useDAppsConnections } from 'app/templates/DAppConnection/use-connections'; +import { NetworkPopper } from 'app/templates/network-popper'; +import { T } from 'lib/i18n'; +import { useAllEvmChains } from 'temple/front'; +import { TempleChainKind } from 'temple/types'; export const DAppsSettings = memo(() => { - const { dapps, activeDApp, disconnectDApps, disconnectOne } = useDAppsConnections(); + const { dapps, activeDApp, disconnectDApps, disconnectOne, switchDAppEvmChain } = useDAppsConnections(); const displayedDapps = useMemo( () => (activeDApp ? dapps.filter(dapp => dapp !== activeDApp) : dapps), @@ -23,53 +30,102 @@ export const DAppsSettings = memo(() => { return ( <> {dapps.length ? ( - - {activeDApp ? ( -
- -
- ) : null} + <> + + {activeDApp ? ( +
+ +
+ ) : null} - {displayedDapps.length ? ( -
-
- {displayedDapps.map(([origin, dapp]) => ( - - ))} -
-
- ) : null} -
+ {displayedDapps.length ? ( +
+
+ {displayedDapps.map(([origin, dapp]) => ( + + ))} +
+
+ ) : null} +
+ + + Disconnect All + + + ) : ( )} - - - - Disconnect All - - ); }); interface DAppItemProps { - dapp: TezosDAppSession; + dapp: DAppSession; origin: string; onRemoveClick: SyncFn; + onEvmNetworkSelect?: (origin: string, chainId: number) => void; } -const DAppItem = memo(({ dapp, origin, onRemoveClick }) => ( -
- +const DAppItem = memo(({ dapp, origin, onRemoveClick, onEvmNetworkSelect }) => { + const activeTabOrigin = useActiveTabUrlOrigin(); + const evmChains = useAllEvmChains(); + const evmDAppNetwork = isTezosDAppSession(dapp) ? null : evmChains[dapp.chainId]; -
{dapp.appMeta.name}
+ const switchDAppNetwork = useCallback( + (chain: FilterChain) => { + if (chain?.kind === TempleChainKind.EVM && evmDAppNetwork?.chainId !== chain.chainId) { + onEvmNetworkSelect?.(origin, chain.chainId); + } + }, + [evmDAppNetwork?.chainId, onEvmNetworkSelect, origin] + ); - -
-)); + return ( +
+ + +
+ {dapp.appMeta.name} + {activeTabOrigin === origin && !isTezosDAppSession(dapp) && ( +
+ + + + + {({ ref, toggleOpened, selectedOptionName }) => ( + + )} + +
+ )} +
+ + +
+ ); +}); const Section: FC> = ({ title, children }) => (
diff --git a/src/app/pages/Welcome/Welcome.tsx b/src/app/pages/Welcome/Welcome.tsx index 49097253d7..8f674fc1a8 100644 --- a/src/app/pages/Welcome/Welcome.tsx +++ b/src/app/pages/Welcome/Welcome.tsx @@ -3,7 +3,7 @@ import React, { memo, useCallback, useState } from 'react'; import { IconBase } from 'app/atoms'; import { Lines } from 'app/atoms/Lines'; import { Logo } from 'app/atoms/Logo'; -import { PageModal } from 'app/atoms/PageModal'; +import { BackButton, PageModal } from 'app/atoms/PageModal'; import { SocialButton } from 'app/atoms/SocialButton'; import { StyledButton } from 'app/atoms/StyledButton'; import { SuspenseContainer } from 'app/atoms/SuspenseContainer'; @@ -61,9 +61,8 @@ const Welcome = memo(() => { : undefined} onRequestClose={closeModal} - onGoBack={handleGoBack} > {isImport && !shouldShowPasswordForm ? ( diff --git a/src/app/storage/dapps/index.ts b/src/app/storage/dapps/index.ts index 95cbaacbc8..1e504e66cf 100644 --- a/src/app/storage/dapps/index.ts +++ b/src/app/storage/dapps/index.ts @@ -1,21 +1,80 @@ -import type { TempleDAppMetadata, TempleDAppNetwork } from '@temple-wallet/dapp/dist/types'; +import type { TempleDAppNetwork } from '@temple-wallet/dapp/dist/types'; +import { WalletPermission } from 'viem'; import { fetchFromStorage, putToStorage } from 'lib/storage'; +import { DAppMetadata } from 'lib/temple/types'; +import { TempleChainKind } from 'temple/types'; -export const storageKey = 'dapp_sessions'; +export const tezosDAppStorageKey = 'dapp_sessions'; +export const evmDAppStorageKey = 'evm_dapp_sessions'; export type TezosDAppNetwork = TempleDAppNetwork | 'ghostnet'; export interface TezosDAppSession { network: TezosDAppNetwork; - appMeta: TempleDAppMetadata; + appMeta: DAppMetadata; pkh: string; publicKey: string; } -export type TezosDAppsSessionsRecord = Record; +export interface EvmDAppSession { + chainId: number; + appMeta: DAppMetadata; + pkh: string; + permissions: WalletPermission[]; +} + +export type DAppSession = T extends TempleChainKind.Tezos + ? TezosDAppSession + : EvmDAppSession; + +export function isTezosDAppSession(session: DAppSession): session is TezosDAppSession { + return 'network' in session; +} + +export type DAppsSessionsRecord = StringRecord>; -export const getStoredTezosDappsSessions = () => fetchFromStorage(storageKey); +export type TezosDAppsSessionsRecord = DAppsSessionsRecord; + +export type EvmDAppsSessionsRecord = DAppsSessionsRecord; + +function getStoredDAppsSessions(chainKind: T): Promise | null> { + return fetchFromStorage(chainKind === TempleChainKind.Tezos ? tezosDAppStorageKey : evmDAppStorageKey); +} + +function putStoredDappsSessions(chainKind: T, value: DAppsSessionsRecord) { + return putToStorage(chainKind === TempleChainKind.Tezos ? tezosDAppStorageKey : evmDAppStorageKey, value); +} -export const putStoredTezosDappsSessions = (value: TezosDAppsSessionsRecord) => - putToStorage(storageKey, value); +export async function getAllDApps(chainKind: T) { + return (await getStoredDAppsSessions(chainKind)) || {}; +} + +export async function getDApp( + chainKind: T, + origin: string +): Promise | undefined> { + const dApps = await getAllDApps(chainKind); + + return dApps[origin]; +} + +export async function setDApp( + chainKind: T, + origin: string, + session: DAppSession +): Promise> { + const current = await getAllDApps(chainKind); + const newDApps = { ...current, [origin]: session }; + await putStoredDappsSessions(chainKind, newDApps); + + return newDApps; +} + +export async function removeDApps(chainKind: T, origins: string[]) { + const dappsRecord = await getAllDApps(chainKind); + for (const origin of origins) delete dappsRecord[origin]; + await putStoredDappsSessions(chainKind, dappsRecord); + + return dappsRecord; +} diff --git a/src/app/storage/dapps/use-value.hook.ts b/src/app/storage/dapps/use-value.hook.ts index 4e16c02864..a3119ab80e 100644 --- a/src/app/storage/dapps/use-value.hook.ts +++ b/src/app/storage/dapps/use-value.hook.ts @@ -1,5 +1,7 @@ import { useStorage } from 'lib/temple/front'; -import { TezosDAppsSessionsRecord, storageKey } from './index'; +import { EvmDAppsSessionsRecord, TezosDAppsSessionsRecord, evmDAppStorageKey, tezosDAppStorageKey } from './index'; -export const useStoredTezosDappsSessions = () => useStorage(storageKey); +export const useStoredTezosDappsSessions = () => useStorage(tezosDAppStorageKey); + +export const useStoredEvmDappsSessions = () => useStorage(evmDAppStorageKey); diff --git a/src/app/templates/AccountBanner.tsx b/src/app/templates/AccountBanner.tsx index 934fa03ec7..676313388b 100644 --- a/src/app/templates/AccountBanner.tsx +++ b/src/app/templates/AccountBanner.tsx @@ -1,91 +1,72 @@ -import React, { HTMLAttributes, memo, ReactNode, useMemo } from 'react'; +import React, { FC, HTMLAttributes, memo, ReactNode, useMemo } from 'react'; import classNames from 'clsx'; import { AccountTypeBadge, Money, Name } from 'app/atoms'; import { AccountAvatar } from 'app/atoms/AccountAvatar'; -import { TezosBalance } from 'app/templates/Balance'; +import { BalanceProps, EvmBalance, TezosBalance } from 'app/templates/Balance'; import { t } from 'lib/i18n'; -import { getTezosGasMetadata } from 'lib/metadata'; +import { getTezosGasMetadata, TEZOS_METADATA, useEvmGasMetadata } from 'lib/metadata'; import { StoredAccount } from 'lib/temple/types'; import { AccountForChain, getAccountAddressForEvm, getAccountAddressForTezos } from 'temple/accounts'; -import { TezosNetworkEssentials } from 'temple/networks'; +import { DEFAULT_EVM_CURRENCY, EvmNetworkEssentials, NetworkEssentials, TezosNetworkEssentials } from 'temple/networks'; import { TempleChainKind } from 'temple/types'; interface Props extends HTMLAttributes { account: StoredAccount | AccountForChain; tezosNetwork?: TezosNetworkEssentials; + evmNetwork?: EvmNetworkEssentials; label?: ReactNode; labelDescription?: ReactNode; labelIndent?: 'sm' | 'md'; smallLabelIndent?: boolean; } -const AccountBanner = memo(({ tezosNetwork, account, className, label, smallLabelIndent, labelDescription }) => { - const labelWithFallback = label ?? t('account'); - - const [tezosAddress, evmAddress] = useMemo(() => { - if ('chain' in account && 'address' in account) { - return [ - getAccountForChainAddress(account, TempleChainKind.Tezos), - getAccountForChainAddress(account, TempleChainKind.EVM) - ]; - } - - return [getAccountAddressForTezos(account), getAccountAddressForEvm(account)]; - }, [account]); - - return ( -
- {(labelWithFallback || labelDescription) && ( -

- {labelWithFallback && {labelWithFallback}} - - {labelDescription && ( - {labelDescription} - )} -

- )} - -
- - -
-
- {account.name} - - -
- - {tezosAddress && ( -
- - - {tezosNetwork && ( - - {bal => ( -
- {bal} - - {getTezosGasMetadata(tezosNetwork.chainId).symbol} - -
- )} -
- )} +const AccountBanner = memo( + ({ tezosNetwork, evmNetwork, account, className, label, smallLabelIndent, labelDescription }) => { + const labelWithFallback = label ?? t('account'); + + const [tezosAddress, evmAddress] = useMemo(() => { + if ('chain' in account && 'address' in account) { + return [ + getAccountForChainAddress(account, TempleChainKind.Tezos), + getAccountForChainAddress(account, TempleChainKind.EVM) + ]; + } + + return [getAccountAddressForTezos(account), getAccountAddressForEvm(account)]; + }, [account]); + + return ( +
+ {(labelWithFallback || labelDescription) && ( +

+ {labelWithFallback && {labelWithFallback}} + + {labelDescription && ( + {labelDescription} + )} +

+ )} + +
+ + +
+
+ {account.name} + +
- )} - {evmAddress && ( -
- -
- )} + + +
-
- ); -}); + ); + } +); export default AccountBanner; @@ -93,6 +74,54 @@ interface AccountBannerAddressProps { address: string; } +interface BalanceViewProps { + network: NetworkEssentials; + address: string; +} + +const BalanceViewHOC = ( + Balance: FC>, + useGasMetadata: (chainId: NetworkEssentials['chainId']) => { symbol?: string } | undefined, + fallbackSymbol: string +) => + memo>(({ network, address }) => { + const gasMetadata = useGasMetadata(network.chainId); + + return ( + + {bal => ( +
+ {bal} + + {gasMetadata?.symbol ?? fallbackSymbol} + +
+ )} +
+ ); + }); + +const AccountAddressViewHOC = ( + Balance: FC>, + useGasMetadata: (chainId: NetworkEssentials['chainId']) => { symbol?: string } | undefined, + fallbackSymbol: string +) => { + const BalanceView = BalanceViewHOC(Balance, useGasMetadata, fallbackSymbol); + + return memo>>(({ network, address }) => + address ? ( +
+ + + {network && } +
+ ) : null + ); +}; + +const TezosAddressView = AccountAddressViewHOC(TezosBalance, getTezosGasMetadata, TEZOS_METADATA.symbol); +const EvmAddressView = AccountAddressViewHOC(EvmBalance, useEvmGasMetadata, DEFAULT_EVM_CURRENCY.symbol); + const AccountBannerAddress = memo(({ address }) => { const [start, end] = useMemo(() => { const ln = address.length; diff --git a/src/app/templates/AccountCard.tsx b/src/app/templates/AccountCard.tsx new file mode 100644 index 0000000000..2c5139cdc7 --- /dev/null +++ b/src/app/templates/AccountCard.tsx @@ -0,0 +1,63 @@ +import React, { memo } from 'react'; + +import clsx from 'clsx'; + +import { AccLabel } from 'app/atoms/AccLabel'; +import { AccountAvatar } from 'app/atoms/AccountAvatar'; +import { AccountName } from 'app/atoms/AccountName'; +import { RadioButton } from 'app/atoms/RadioButton'; +import { TotalEquity } from 'app/atoms/TotalEquity'; +import { StoredAccount } from 'lib/temple/types'; +import { useScrollIntoViewOnMount } from 'lib/ui/use-scroll-into-view'; + +export interface AccountCardProps { + account: StoredAccount; + isCurrent: boolean; + showRadioOnHover?: boolean; + searchValue: string; + attractSelf: boolean; + onClick?: EmptyFn; +} + +export const AccountCard = memo( + ({ account, isCurrent, attractSelf, showRadioOnHover = true, searchValue, onClick }) => { + const elemRef = useScrollIntoViewOnMount(isCurrent && attractSelf); + + return ( +
+
+ + + + +
+ + +
+ +
+
+
Total Balance:
+ +
+ +
+
+ + +
+
+ ); + } +); diff --git a/src/app/templates/AccountsGroup.tsx b/src/app/templates/AccountsGroup.tsx new file mode 100644 index 0000000000..cd9d50173e --- /dev/null +++ b/src/app/templates/AccountsGroup.tsx @@ -0,0 +1,23 @@ +import React, { ReactNode } from 'react'; + +import { Name } from 'app/atoms'; +import { StoredAccount } from 'lib/temple/types'; + +export interface AccountsGroupProps { + title: ReactNode; + accounts: T[]; + children: (account: T) => ReactNode | ReactNode[]; +} + +// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-constraint +export const AccountsGroup = ({ + title, + accounts, + children +}: AccountsGroupProps) => ( +
+ {title} + +
{accounts.map(account => children(account))}
+
+); diff --git a/src/app/templates/AccountsModal.tsx b/src/app/templates/AccountsModal.tsx new file mode 100644 index 0000000000..f28b31e15b --- /dev/null +++ b/src/app/templates/AccountsModal.tsx @@ -0,0 +1,19 @@ +import React, { memo } from 'react'; + +import { PageModal } from 'app/atoms/PageModal'; +import { t } from 'lib/i18n'; + +import { AccountsModalContent, AccountsModalContentProps } from './AccountsModalContent'; + +export const AccountsModal = memo( + ({ accounts, currentAccountId, opened, onRequestClose }) => ( + + + + ) +); diff --git a/src/app/templates/AccountsModalContent/index.tsx b/src/app/templates/AccountsModalContent/index.tsx new file mode 100644 index 0000000000..c414f382d6 --- /dev/null +++ b/src/app/templates/AccountsModalContent/index.tsx @@ -0,0 +1,229 @@ +import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; + +import clsx from 'clsx'; + +import { EmptyState } from 'app/atoms/EmptyState'; +import { IconButton } from 'app/atoms/IconButton'; +import { ScrollView } from 'app/atoms/PageModal/scroll-view'; +import { useShortcutAccountSelectModalIsOpened } from 'app/hooks/use-account-select-shortcut'; +import { useAllAccountsReactiveOnAddition } from 'app/hooks/use-all-accounts-reactive'; +import { ReactComponent as SettingsIcon } from 'app/icons/base/settings.svg'; +import { + AccountsGroup as GenericAccountsGroup, + AccountsGroupProps as GenericAccountsGroupProps +} from 'app/templates/AccountsGroup'; +import { NewWalletActionsPopper } from 'app/templates/NewWalletActionsPopper'; +import { SearchBarField } from 'app/templates/SearchField'; +import { StoredAccount } from 'lib/temple/types'; +import { navigate } from 'lib/woozie'; +import { searchAndFilterAccounts, useAccountsGroups, useCurrentAccountId, useVisibleAccounts } from 'temple/front'; +import { useSetAccountId } from 'temple/front/ready'; + +import { AccountCard, AccountCardProps } from '../AccountCard'; +import { CreateHDWalletModal } from '../CreateHDWalletModal'; +import { ImportAccountModal, ImportOptionSlug } from '../ImportAccountModal'; + +import { AccountsModalSelectors } from './selectors'; + +export interface AccountsModalContentProps { + accounts?: StoredAccount[]; + currentAccountId?: string; + opened: boolean; + onRequestClose: EmptyFn; +} + +enum AccountsModalSubmodals { + CreateHDWallet = 'create-hd-wallet', + ImportAccount = 'import-account', + WatchOnly = 'watch-only' +} + +export const AccountsModalContent = memo( + ({ accounts: specifiedAccounts, currentAccountId: specifiedCurrentAccountId, opened, onRequestClose }) => { + const allAccounts = useVisibleAccounts(); + const globalCurrentAccountId = useCurrentAccountId(); + const currentAccountId = specifiedCurrentAccountId ?? globalCurrentAccountId; + const accounts = specifiedAccounts ?? allAccounts; + + const [searchValue, setSearchValue] = useState(''); + const [topEdgeIsVisible, setTopEdgeIsVisible] = useState(true); + const [activeSubmodal, setActiveSubmodal] = useState(undefined); + const [importOptionSlug, setImportOptionSlug] = useState(); + + useAllAccountsReactiveOnAddition(); + useShortcutAccountSelectModalIsOpened(onRequestClose); + + const filteredAccounts = useMemo( + () => (searchValue.length ? searchAndFilterAccounts(accounts, searchValue) : accounts), + [searchValue, accounts] + ); + const filteredGroups = useAccountsGroups(filteredAccounts); + + const [attractSelectedAccount, setAttractSelectedAccount] = useState(true); + + useEffect(() => { + if (searchValue) setAttractSelectedAccount(false); + else if (!opened) setAttractSelectedAccount(true); + }, [opened, searchValue]); + + useEffect(() => { + if (!opened) setSearchValue(''); + }, [opened]); + + const closeSubmodal = useCallback(() => { + setActiveSubmodal(undefined); + setImportOptionSlug(undefined); + }, []); + + const totalClose = useCallback(() => { + closeSubmodal(); + onRequestClose(); + }, [closeSubmodal, onRequestClose]); + + const startWalletCreation = useCallback(() => setActiveSubmodal(AccountsModalSubmodals.CreateHDWallet), []); + + const goToImportModal = useCallback(() => { + setActiveSubmodal(AccountsModalSubmodals.ImportAccount); + setImportOptionSlug(undefined); + }, []); + const goToWatchOnlyModal = useCallback(() => setActiveSubmodal(AccountsModalSubmodals.WatchOnly), []); + const handleSeedPhraseImportOptionSelect = useCallback(() => setImportOptionSlug('mnemonic'), []); + const handlePrivateKeyImportOptionSelect = useCallback(() => setImportOptionSlug('private-key'), []); + + const submodal = useMemo(() => { + switch (activeSubmodal) { + case AccountsModalSubmodals.CreateHDWallet: + return ( + + ); + case AccountsModalSubmodals.ImportAccount: + return ( + + ); + case AccountsModalSubmodals.WatchOnly: + return ( + + ); + default: + return null; + } + }, [ + activeSubmodal, + closeSubmodal, + goToImportModal, + handlePrivateKeyImportOptionSelect, + handleSeedPhraseImportOptionSelect, + importOptionSlug, + totalClose + ]); + + return ( + <> + {submodal} + +
+ + + navigate('settings/accounts-management')} + testID={AccountsModalSelectors.accountsManagementButton} + /> + + +
+ + + {filteredGroups.length === 0 ? ( +
+ +
+ ) : ( + filteredGroups.map(group => ( + + )) + )} +
+ + ); + } +); + +interface AccountsGroupProps extends Omit { + currentAccountId: string; + attractSelectedAccount: boolean; + searchValue: string; + onAccountSelect: EmptyFn; +} + +const AccountsGroup = memo( + ({ title, accounts, currentAccountId, attractSelectedAccount, searchValue, onAccountSelect }) => ( + + {account => ( + + )} + + ) +); + +const AccountOfGroup = memo(({ onClick, isCurrent, account, ...restProps }) => { + const setAccountId = useSetAccountId(); + + const handleClick = useCallback(() => { + if (isCurrent) return; + + setAccountId(account.id); + onClick?.(); + }, [isCurrent, account.id, onClick, setAccountId]); + + return ; +}); diff --git a/src/app/templates/AppHeader/selectors.ts b/src/app/templates/AccountsModalContent/selectors.ts similarity index 100% rename from src/app/templates/AppHeader/selectors.ts rename to src/app/templates/AccountsModalContent/selectors.ts diff --git a/src/app/templates/AppHeader/AccountsModal.tsx b/src/app/templates/AppHeader/AccountsModal.tsx deleted file mode 100644 index 0415473166..0000000000 --- a/src/app/templates/AppHeader/AccountsModal.tsx +++ /dev/null @@ -1,280 +0,0 @@ -import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; - -import clsx from 'clsx'; - -import { Name } from 'app/atoms'; -import { AccLabel } from 'app/atoms/AccLabel'; -import { AccountAvatar } from 'app/atoms/AccountAvatar'; -import { AccountName } from 'app/atoms/AccountName'; -import { EmptyState } from 'app/atoms/EmptyState'; -import { IconButton } from 'app/atoms/IconButton'; -import { PageModal } from 'app/atoms/PageModal'; -import { ScrollView } from 'app/atoms/PageModal/scroll-view'; -import { RadioButton } from 'app/atoms/RadioButton'; -import { TotalEquity } from 'app/atoms/TotalEquity'; -import { useShortcutAccountSelectModalIsOpened } from 'app/hooks/use-account-select-shortcut'; -import { useAllAccountsReactiveOnAddition } from 'app/hooks/use-all-accounts-reactive'; -import { ReactComponent as SettingsIcon } from 'app/icons/base/settings.svg'; -import { NewWalletActionsPopper } from 'app/templates/NewWalletActionsPopper'; -import { SearchBarField } from 'app/templates/SearchField'; -import { t } from 'lib/i18n'; -import { StoredAccount } from 'lib/temple/types'; -import { useScrollIntoViewOnMount } from 'lib/ui/use-scroll-into-view'; -import { navigate } from 'lib/woozie'; -import { searchAndFilterAccounts, useAccountsGroups, useCurrentAccountId, useVisibleAccounts } from 'temple/front'; -import { useSetAccountId } from 'temple/front/ready'; - -import { CreateHDWalletModal } from '../CreateHDWalletModal'; -import { ImportAccountModal, ImportOptionSlug } from '../ImportAccountModal'; - -import { AccountsModalSelectors } from './selectors'; - -interface Props { - opened: boolean; - onRequestClose: EmptyFn; -} - -enum AccountsModalSubmodals { - CreateHDWallet = 'create-hd-wallet', - ImportAccount = 'import-account', - WatchOnly = 'watch-only' -} - -export const AccountsModal = memo(({ opened, onRequestClose }) => { - const allAccounts = useVisibleAccounts(); - const currentAccountId = useCurrentAccountId(); - - const [searchValue, setSearchValue] = useState(''); - const [topEdgeIsVisible, setTopEdgeIsVisible] = useState(true); - const [activeSubmodal, setActiveSubmodal] = useState(undefined); - const [importOptionSlug, setImportOptionSlug] = useState(); - - useAllAccountsReactiveOnAddition(); - useShortcutAccountSelectModalIsOpened(onRequestClose); - - const filteredAccounts = useMemo( - () => (searchValue.length ? searchAndFilterAccounts(allAccounts, searchValue) : allAccounts), - [searchValue, allAccounts] - ); - const filteredGroups = useAccountsGroups(filteredAccounts); - - const [attractSelectedAccount, setAttractSelectedAccount] = useState(true); - - useEffect(() => { - if (searchValue) setAttractSelectedAccount(false); - else if (!opened) setAttractSelectedAccount(true); - }, [opened, searchValue]); - - useEffect(() => { - if (!opened) setSearchValue(''); - }, [opened]); - - const closeSubmodal = useCallback(() => { - setActiveSubmodal(undefined); - setImportOptionSlug(undefined); - }, []); - - const totalClose = useCallback(() => { - closeSubmodal(); - onRequestClose(); - }, [closeSubmodal, onRequestClose]); - - const startWalletCreation = useCallback(() => setActiveSubmodal(AccountsModalSubmodals.CreateHDWallet), []); - - const goToImportModal = useCallback(() => { - setActiveSubmodal(AccountsModalSubmodals.ImportAccount); - setImportOptionSlug(undefined); - }, []); - const goToWatchOnlyModal = useCallback(() => setActiveSubmodal(AccountsModalSubmodals.WatchOnly), []); - const handleSeedPhraseImportOptionSelect = useCallback(() => setImportOptionSlug('mnemonic'), []); - const handlePrivateKeyImportOptionSelect = useCallback(() => setImportOptionSlug('private-key'), []); - - const submodal = useMemo(() => { - switch (activeSubmodal) { - case AccountsModalSubmodals.CreateHDWallet: - return ( - - ); - case AccountsModalSubmodals.ImportAccount: - return ( - - ); - case AccountsModalSubmodals.WatchOnly: - return ( - - ); - default: - return null; - } - }, [ - activeSubmodal, - closeSubmodal, - goToImportModal, - handlePrivateKeyImportOptionSelect, - handleSeedPhraseImportOptionSelect, - importOptionSlug, - totalClose - ]); - - return ( - <> - {submodal} - - -
- - - navigate('settings/accounts-management')} - testID={AccountsModalSelectors.accountsManagementButton} - /> - - -
- - - {filteredGroups.length === 0 ? ( -
- -
- ) : ( - filteredGroups.map(group => ( - - )) - )} -
-
- - ); -}); - -interface AccountsGroupProps { - title: string; - accounts: StoredAccount[]; - currentAccountId: string; - searchValue: string; - attractSelectedAccount: boolean; - onAccountSelect: EmptyFn; -} - -const AccountsGroup = memo( - ({ title, accounts, currentAccountId, searchValue, attractSelectedAccount, onAccountSelect }) => { - // - return ( -
- {title} - -
- {accounts.map(account => ( - - ))} -
-
- ); - } -); - -interface AccountOfGroupProps { - account: StoredAccount; - isCurrent: boolean; - searchValue: string; - attractSelf: boolean; - onSelect: EmptyFn; -} - -const AccountOfGroup = memo(({ account, isCurrent, searchValue, attractSelf, onSelect }) => { - const setAccountId = useSetAccountId(); - - const onClick = useCallback(() => { - if (isCurrent) return; - - setAccountId(account.id); - onSelect(); - }, [isCurrent, account.id, onSelect, setAccountId]); - - const elemRef = useScrollIntoViewOnMount(isCurrent && attractSelf); - - return ( -
-
- - - - -
- - -
- -
-
-
Total Balance:
- -
- -
-
- - -
-
- ); -}); diff --git a/src/app/templates/AppHeader/index.tsx b/src/app/templates/AppHeader/index.tsx index fe0b5bd7ed..a8ea10f5ab 100644 --- a/src/app/templates/AppHeader/index.tsx +++ b/src/app/templates/AppHeader/index.tsx @@ -8,12 +8,12 @@ import { AccountName } from 'app/atoms/AccountName'; import { Button } from 'app/atoms/Button'; import { useSearchParamsBoolean } from 'app/hooks/use-search-params-boolean'; import { ReactComponent as BurgerIcon } from 'app/icons/base/menu.svg'; +import { HomeSelectors } from 'app/pages/Home/selectors'; import Popper from 'lib/ui/Popper'; import { useAccount } from 'temple/front'; -import { HomeSelectors } from '../../pages/Home/selectors'; +import { AccountsModal } from '../AccountsModal'; -import { AccountsModal } from './AccountsModal'; import MenuDropdown from './MenuDropdown'; export const AppHeader = memo(() => { diff --git a/src/app/templates/Balance.tsx b/src/app/templates/Balance.tsx index 7edd8523b0..3718c56e9e 100644 --- a/src/app/templates/Balance.tsx +++ b/src/app/templates/Balance.tsx @@ -3,66 +3,53 @@ import React, { FC, cloneElement, ReactElement } from 'react'; import BigNumber from 'bignumber.js'; import CSSTransition from 'react-transition-group/CSSTransition'; -import { EVM_TOKEN_SLUG } from 'lib/assets/defaults'; +import { EVM_TOKEN_SLUG, TEZ_TOKEN_SLUG } from 'lib/assets/defaults'; import { useTezosAssetBalance } from 'lib/balances'; import { useEvmAssetBalance } from 'lib/balances/hooks'; -import { EvmNetworkEssentials, TezosNetworkEssentials } from 'temple/networks'; +import { NetworkEssentials } from 'temple/networks'; +import { TempleChainKind } from 'temple/types'; -interface TezosBalanceProps { - network: TezosNetworkEssentials; - address: string; +export interface BalanceProps { + network: NetworkEssentials; + address: T extends TempleChainKind.EVM ? HexString : string; children: (b: BigNumber) => ReactElement; assetSlug?: string; } -export const TezosBalance: FC = ({ network, address, children, assetSlug = 'tez' }) => { - const { value: balance } = useTezosAssetBalance(assetSlug, address, network); - const exists = balance !== undefined; - const childNode = children(balance == null ? new BigNumber(0) : balance); - - return ( - - {cloneElement(childNode, { - className: childNode.props.className - })} - - ); +const BalanceHOC = ( + useBalance: ( + slug: string, + address: BalanceProps['address'], + network: NetworkEssentials + ) => { value: BigNumber | undefined }, + defaultAssetSlug: string +) => { + const Component: FC> = ({ network, address, children, assetSlug = defaultAssetSlug }) => { + const { value: balance } = useBalance(assetSlug, address, network); + const exists = balance !== undefined; + + const childNode = children(balance == null ? new BigNumber(0) : balance); + + return ( + + {cloneElement(childNode, { + className: childNode.props.className + })} + + ); + }; + + return Component; }; -interface EvmBalanceProps { - network: EvmNetworkEssentials; - address: HexString; - children: (b: BigNumber) => ReactElement; - assetSlug?: string; -} +export const TezosBalance = BalanceHOC(useTezosAssetBalance, TEZ_TOKEN_SLUG); -export const EvmBalance: FC = ({ network, address, children, assetSlug = EVM_TOKEN_SLUG }) => { - const { value: balance } = useEvmAssetBalance(assetSlug, address, network); - const exists = balance !== undefined; - - const childNode = children(balance == null ? new BigNumber(0) : balance); - - return ( - - {cloneElement(childNode, { - className: childNode.props.className - })} - - ); -}; +export const EvmBalance = BalanceHOC(useEvmAssetBalance, EVM_TOKEN_SLUG); diff --git a/src/app/templates/ConnectBanner.tsx b/src/app/templates/ConnectBanner.tsx deleted file mode 100644 index 75ca4f74d8..0000000000 --- a/src/app/templates/ConnectBanner.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React, { FC } from 'react'; - -import classNames from 'clsx'; - -import DAppLogo from 'app/atoms/DAppLogo'; -import { Logo } from 'app/atoms/Logo'; -import Name from 'app/atoms/Name'; -import { ReactComponent as LayersIcon } from 'app/icons/layers.svg'; -import { ReactComponent as OkIcon } from 'app/icons/ok.svg'; -import { DappMetadata } from 'lib/temple/types'; - -interface ConnectBannerProps { - type: 'connect' | 'confirm_operations'; - origin: string; - appMeta: DappMetadata; - className?: string; -} - -const ConnectBanner: FC = ({ type, origin, appMeta, className }) => { - const Icon = type === 'connect' ? OkIcon : LayersIcon; - - return ( -
-
- - - - {appMeta.name} - -
- -
-
-
- -
-
-
- -
- - - Temple -
-
- ); -}; - -export default ConnectBanner; diff --git a/src/app/templates/DAppConnection/index.tsx b/src/app/templates/DAppConnection/index.tsx index 2fc3ee4ca5..52c63421b7 100644 --- a/src/app/templates/DAppConnection/index.tsx +++ b/src/app/templates/DAppConnection/index.tsx @@ -2,23 +2,28 @@ import React, { memo } from 'react'; import { IconBase } from 'app/atoms'; import DAppLogo from 'app/atoms/DAppLogo'; -import { TezosNetworkLogo } from 'app/atoms/NetworkLogo'; +import { EvmNetworkLogo, TezosNetworkLogo } from 'app/atoms/NetworkLogo'; import { StyledButton } from 'app/atoms/StyledButton'; import { ReactComponent as ChevronRightSvg } from 'app/icons/base/chevron_right.svg'; +import { isTezosDAppSession } from 'app/storage/dapps'; import { useTypedSWR } from 'lib/swr'; import { TempleTezosChainId } from 'lib/temple/types'; import { Link } from 'lib/woozie'; -import { useAllTezosChains } from 'temple/front'; +import { useAllEvmChains, useAllTezosChains } from 'temple/front'; import { loadTezosChainId } from 'temple/tezos'; +import { TempleChainKind } from 'temple/types'; import { useDAppsConnections } from './use-connections'; export const DAppConnection = memo(() => { const { activeDApp, disconnectOne } = useDAppsConnections(); + const evmChains = useAllEvmChains(); const tezosChains = useAllTezosChains(); - const { data: chainId } = useTypedSWR(['dapp-connection', 'tezos-chain-id'], () => { + const { data: tezosChainId } = useTypedSWR(['dapp-connection', 'tezos-chain-id'], () => { + if (!isTezosDAppSession(dapp)) return null; + if (dapp.network === 'mainnet') return TempleTezosChainId.Mainnet; if (dapp.network === 'ghostnet') return TempleTezosChainId.Ghostnet; @@ -33,16 +38,24 @@ export const DAppConnection = memo(() => { const [origin, dapp] = activeDApp; - const network = chainId ? tezosChains[chainId] : null; + const network = isTezosDAppSession(dapp) + ? tezosChainId + ? tezosChains[tezosChainId] + : null + : evmChains[dapp.chainId] ?? null; return (
- + {network && (
- + {network.kind === TempleChainKind.Tezos ? ( + + ) : ( + + )}
)}
diff --git a/src/app/templates/DAppConnection/use-connections.ts b/src/app/templates/DAppConnection/use-connections.ts index 4685b3b60c..2699bb4760 100644 --- a/src/app/templates/DAppConnection/use-connections.ts +++ b/src/app/templates/DAppConnection/use-connections.ts @@ -1,25 +1,33 @@ import { useCallback, useMemo } from 'react'; -import { useStoredTezosDappsSessions } from 'app/storage/dapps/use-value.hook'; +import { DAppSession, DAppsSessionsRecord } from 'app/storage/dapps'; +import { useStoredEvmDappsSessions, useStoredTezosDappsSessions } from 'app/storage/dapps/use-value.hook'; import { useTempleClient } from 'lib/temple/front'; import { throttleAsyncCalls } from 'lib/utils/functions'; -import { useAccountAddressForTezos } from 'temple/front'; +import { useAccountAddressForEvm, useAccountAddressForTezos } from 'temple/front'; +import { TempleChainKind } from 'temple/types'; import { useActiveTabUrlOrigin } from './use-active-tab'; +function getDApps(sessions: DAppsSessionsRecord | nullish, address?: string) { + const entries = Object.entries(sessions || {}); + + return address ? entries.filter(([, ds]) => ds.pkh === address) : entries; +} + export function useDAppsConnections() { - const { removeDAppSession } = useTempleClient(); + const { removeDAppSession, switchDAppEvmChain } = useTempleClient(); const tezAddress = useAccountAddressForTezos(); - const [dappsSessions] = useStoredTezosDappsSessions(); + const evmAddress = useAccountAddressForEvm(); + const [tezosDappsSessions] = useStoredTezosDappsSessions(); + const [evmDappsSessions] = useStoredEvmDappsSessions(); const dapps = useMemo(() => { - if (!dappsSessions) return []; - - const entries = Object.entries(dappsSessions); + const tezosDApps: [string, DAppSession][] = getDApps(tezosDappsSessions, tezAddress); - return tezAddress ? entries.filter(([, ds]) => ds.pkh === tezAddress) : entries; - }, [dappsSessions, tezAddress]); + return tezosDApps.concat(getDApps(evmDappsSessions, evmAddress)); + }, [evmAddress, evmDappsSessions, tezAddress, tezosDappsSessions]); const activeTabOrigin = useActiveTabUrlOrigin(); @@ -35,5 +43,5 @@ export function useDAppsConnections() { const disconnectOne = useCallback((origin: string) => disconnectDApps([origin]), [disconnectDApps]); - return { dapps, activeDApp, disconnectDApps, disconnectOne }; + return { dapps, activeDApp, disconnectDApps, disconnectOne, switchDAppEvmChain }; } diff --git a/src/app/templates/EvmOperationView.tsx b/src/app/templates/EvmOperationView.tsx new file mode 100644 index 0000000000..5b7f568522 --- /dev/null +++ b/src/app/templates/EvmOperationView.tsx @@ -0,0 +1,14 @@ +import React, { FC } from 'react'; + +import { TempleEvmDAppSignPayload } from 'lib/temple/types'; + +import { NewRawPayloadView } from './NewRawPayloadView'; + +// TODO: add properties like in `TezosOperationViewProps` when adding other types of EVM dApp actions +interface EvmOperationViewProps { + payload: TempleEvmDAppSignPayload; +} + +export const EvmOperationView: FC = ({ payload }) => ( + +); diff --git a/src/app/templates/ImportAccountModal/forms/mnemonic.tsx b/src/app/templates/ImportAccountModal/forms/mnemonic.tsx index 831dd4f171..b7c67d7417 100644 --- a/src/app/templates/ImportAccountModal/forms/mnemonic.tsx +++ b/src/app/templates/ImportAccountModal/forms/mnemonic.tsx @@ -8,7 +8,7 @@ import { ActionsButtonsBox } from 'app/atoms/PageModal/actions-buttons-box'; import { ScrollView } from 'app/atoms/PageModal/scroll-view'; import { StyledButton } from 'app/atoms/StyledButton'; import { formatMnemonic } from 'app/defaults'; -import { AccountsModalSelectors } from 'app/templates/AppHeader/selectors'; +import { AccountsModalSelectors } from 'app/templates/AccountsModalContent/selectors'; import { isSeedPhraseFilled, SeedPhraseInput } from 'app/templates/SeedPhraseInput'; import { useFormAnalytics } from 'lib/analytics'; import { DEFAULT_EVM_DERIVATION_PATH, DEFAULT_SEED_PHRASE_WORDS_AMOUNT } from 'lib/constants'; @@ -126,7 +126,6 @@ export const MnemonicForm = memo(({ onSuccess }) => { ( title={t(option?.titleI18nKey ?? 'importWallet')} opened onRequestClose={onRequestClose} - shouldShowBackButton={shouldShowBackButton} - onGoBack={onGoBack} + titleLeft={shouldShowBackButton ? : undefined} > {option ? ( diff --git a/src/app/templates/ImportSeedForm/index.tsx b/src/app/templates/ImportSeedForm/index.tsx index a65e963f82..f2e0c8fbe4 100644 --- a/src/app/templates/ImportSeedForm/index.tsx +++ b/src/app/templates/ImportSeedForm/index.tsx @@ -54,7 +54,6 @@ export const ImportSeedForm = memo(({ next }) => { disabled={Boolean(seedError) && wasSubmitted} type="submit" size="L" - className="w-full" color="primary" testID={ImportSeedFormSelectors.nextButton} > diff --git a/src/app/templates/ManualBackupModal/index.tsx b/src/app/templates/ManualBackupModal/index.tsx index b6928b5695..d5da4a5c43 100644 --- a/src/app/templates/ManualBackupModal/index.tsx +++ b/src/app/templates/ManualBackupModal/index.tsx @@ -1,6 +1,6 @@ import React, { memo } from 'react'; -import { PageModal } from 'app/atoms/PageModal'; +import { BackButton, PageModal } from 'app/atoms/PageModal'; import { t } from 'lib/i18n'; import { useBooleanState } from 'lib/ui/hooks'; @@ -27,8 +27,11 @@ export const ManualBackupModal = memo( )} animated={animated} opened - shouldShowBackButton={(isNewMnemonic && shouldVerifySeedPhrase) || Boolean(onStartGoBack)} - onGoBack={shouldVerifySeedPhrase ? goToManualBackup : onStartGoBack ?? onCancel} + titleLeft={ + (isNewMnemonic && shouldVerifySeedPhrase) || Boolean(onStartGoBack) ? ( + + ) : null + } onRequestClose={onCancel} > {shouldVerifySeedPhrase ? ( diff --git a/src/app/templates/ManualBackupModal/mnemonic-view.tsx b/src/app/templates/ManualBackupModal/mnemonic-view.tsx index 5527696d99..39943eadb9 100644 --- a/src/app/templates/ManualBackupModal/mnemonic-view.tsx +++ b/src/app/templates/ManualBackupModal/mnemonic-view.tsx @@ -51,8 +51,8 @@ export const MnemonicView = memo(({ mnemonic, isNewMnemonic, /> - - {isNewMnemonic && ( + {isNewMnemonic && ( + (({ mnemonic, isNewMnemonic, > - )} - + + )} ); }); diff --git a/src/app/templates/NetworkBanner.tsx b/src/app/templates/NetworkBanner.tsx index 88326260d0..d28e334148 100644 --- a/src/app/templates/NetworkBanner.tsx +++ b/src/app/templates/NetworkBanner.tsx @@ -4,57 +4,86 @@ import classNames from 'clsx'; import Name from 'app/atoms/Name'; import { T } from 'lib/i18n'; -import { useTezosChainByChainId } from 'temple/front'; +import { OneOfChains, useTezosChainByChainId } from 'temple/front'; +import { ChainOfKind, useEvmChainByChainId } from 'temple/front/chains'; import { getNetworkTitle } from 'temple/front/networks'; -import { TezosNetworkEssentials } from 'temple/networks'; +import { NetworkEssentials, isTezosNetworkEssentials } from 'temple/networks'; +import { TempleChainKind } from 'temple/types'; interface Props { - network: TezosNetworkEssentials; + network: NetworkEssentials; narrow?: boolean; } -const NetworkBanner = memo(({ network, narrow = false }) => { - const knownChain = useTezosChainByChainId(network.chainId); - - return ( -
-

- - - - - {knownChain ? ( -
-
- - {getNetworkTitle(knownChain)} -
- ) : ( -
-
- - - - - - - {network.rpcBaseURL} - -
- )} -

-
- ); -}); +const NetworkBanner = memo(({ network, narrow = false }) => + isTezosNetworkEssentials(network) ? ( + + ) : ( + + ) +); export default NetworkBanner; + +const NetworkBannerView = memo(({ knownChain, narrow = false, rpcBaseURL }) => ( +
+

+ + + + + {knownChain ? ( +
+
+ + {getNetworkTitle(knownChain)} +
+ ) : ( +
+
+ + + + + + + {rpcBaseURL} + +
+ )} +

+
+)); + +interface ChainNetworkBannerProps { + network: NetworkEssentials; + narrow: boolean; +} + +interface NetworkBannerViewProps { + rpcBaseURL: string; + knownChain: OneOfChains | nullish; + narrow: boolean; +} + +const ChainNetworkBannerHOC = ( + useChainByChainId: (chainId: NetworkEssentials['chainId']) => ChainOfKind | nullish +) => + memo>(({ network, narrow = false }) => { + const knownChain = useChainByChainId(network.chainId); + + return ; + }); + +const TezosNetworkBanner = ChainNetworkBannerHOC(useTezosChainByChainId); +const EvmNetworkBanner = ChainNetworkBannerHOC(useEvmChainByChainId); diff --git a/src/app/templates/NewRawPayloadView.tsx b/src/app/templates/NewRawPayloadView.tsx new file mode 100644 index 0000000000..6bd31fd9d4 --- /dev/null +++ b/src/app/templates/NewRawPayloadView.tsx @@ -0,0 +1,60 @@ +import React, { memo, useCallback, useMemo } from 'react'; + +import clsx from 'clsx'; +import ReactJson from 'react-json-view'; + +import { TextButton } from 'app/atoms/TextButton'; +import { ReactComponent as CopyIcon } from 'app/icons/base/copy.svg'; +import { toastSuccess } from 'app/toaster'; +import { T, t } from 'lib/i18n'; +import { TempleEvmDAppSignPayload } from 'lib/temple/types'; +import useCopyToClipboard from 'lib/ui/useCopyToClipboard'; + +interface NewRawPayloadViewProps { + payload: TempleEvmDAppSignPayload['payload']; +} + +export const NewRawPayloadView = memo(({ payload }) => { + const { fieldRef, copy } = useCopyToClipboard(); + + const text = useMemo(() => (typeof payload === 'string' ? payload : JSON.stringify(payload, null, 2)), [payload]); + + const handleCopyPress = useCallback(() => { + copy(); + toastSuccess(t('copiedHash')); + }, [copy]); + + return ( +
+
+ + + + + + + + +