From 6ff233f11239d56db3657ce6f9e41f7d7b51a387 Mon Sep 17 00:00:00 2001 From: siandreev Date: Sat, 14 Dec 2024 17:13:24 +0100 Subject: [PATCH] feat: select ton connect account for connection --- apps/desktop/src/app/components/DeepLink.tsx | 13 +- .../src/components/Notifications.tsx | 15 +- apps/tablet/src/app/components/DeepLink.tsx | 13 +- .../components/UrlTonConnectSubscription.tsx | 13 +- .../src/service/tonConnect/connectService.ts | 14 +- .../src/components/connect/ScanButton.tsx | 13 +- .../connect/TonConnectNotification.tsx | 128 ++++++++++++++++-- .../src/components/connect/connectHook.ts | 67 ++++----- packages/uikit/src/state/tonConnect.ts | 43 +++--- 9 files changed, 242 insertions(+), 77 deletions(-) diff --git a/apps/desktop/src/app/components/DeepLink.tsx b/apps/desktop/src/app/components/DeepLink.tsx index 6da1f9be0..e0d3a8b89 100644 --- a/apps/desktop/src/app/components/DeepLink.tsx +++ b/apps/desktop/src/app/components/DeepLink.tsx @@ -8,6 +8,8 @@ import { import { useEffect, useState } from 'react'; import { sendBackground } from '../../libs/backgroudService'; import { TonConnectMessage } from '../../libs/message'; +import { Account } from '@tonkeeper/core/dist/entries/account'; +import { WalletId } from '@tonkeeper/core/dist/entries/wallet'; export const DeepLinkSubscription = () => { const [params, setParams] = useState(null); @@ -16,11 +18,18 @@ export const DeepLinkSubscription = () => { const { mutateAsync: responseConnectionAsync, reset: responseReset } = useResponseConnectionMutation(); - const handlerClose = async (replyItems?: ConnectItemReply[], manifest?: DAppManifest) => { + const handlerClose = async ( + result: { + replyItems: ConnectItemReply[]; + manifest: DAppManifest; + account: Account; + walletId: WalletId; + } | null + ) => { if (!params) return; responseReset(); try { - await responseConnectionAsync({ params, replyItems, manifest }); + await responseConnectionAsync({ params, result }); } finally { setParams(null); sendBackground({ king: 'reconnect' } as TonConnectMessage); diff --git a/apps/extension/src/components/Notifications.tsx b/apps/extension/src/components/Notifications.tsx index fbf99c97c..ce5e7289d 100644 --- a/apps/extension/src/components/Notifications.tsx +++ b/apps/extension/src/components/Notifications.tsx @@ -1,4 +1,4 @@ -import { ConnectItemReply } from '@tonkeeper/core/dist/entries/tonConnect'; +import { ConnectItemReply, DAppManifest } from "@tonkeeper/core/dist/entries/tonConnect"; import { delay } from '@tonkeeper/core/dist/utils/common'; import { TonConnectNotification } from '@tonkeeper/uikit/dist/components/connect/TonConnectNotification'; import { TonTransactionNotification } from '@tonkeeper/uikit/dist/components/connect/TonTransactionNotification'; @@ -6,6 +6,8 @@ import { useNotificationAnalytics } from '@tonkeeper/uikit/dist/hooks/amplitude' import { useCallback, useEffect, useState } from 'react'; import { askBackground, sendBackground } from '../event'; import { NotificationData } from '../libs/event'; +import { Account } from "@tonkeeper/core/dist/entries/account"; +import { WalletId } from "@tonkeeper/core/dist/entries/wallet"; export const Notifications = () => { const [data, setData] = useState(undefined); @@ -42,10 +44,15 @@ export const Notifications = () => { { + handleClose={(result: { + replyItems: ConnectItemReply[]; + manifest: DAppManifest; + account: Account; + walletId: WalletId; + } | null) => { if (!data) return; - if (payload) { - sendBackground.message('approveRequest', { id: data.id, payload }); + if (result) { + sendBackground.message('approveRequest', { id: data.id, payload: result }); } else { sendBackground.message('rejectRequest', data.id); } diff --git a/apps/tablet/src/app/components/DeepLink.tsx b/apps/tablet/src/app/components/DeepLink.tsx index ce9779a5a..f50a23cb9 100644 --- a/apps/tablet/src/app/components/DeepLink.tsx +++ b/apps/tablet/src/app/components/DeepLink.tsx @@ -7,6 +7,8 @@ import { } from '@tonkeeper/uikit/dist/components/connect/connectHook'; import { useEffect, useState } from 'react'; import { subscribeToTonOrTonConnectUrlOpened, tonConnectSSE } from "../../libs/tonConnect"; +import { Account } from "@tonkeeper/core/dist/entries/account"; +import { WalletId } from "@tonkeeper/core/dist/entries/wallet"; export const DeepLinkSubscription = () => { const [params, setParams] = useState(null); @@ -15,11 +17,18 @@ export const DeepLinkSubscription = () => { const { mutateAsync: responseConnectionAsync, reset: responseReset } = useResponseConnectionMutation(); - const handlerClose = async (replyItems?: ConnectItemReply[], manifest?: DAppManifest) => { + const handlerClose = async ( + result: { + replyItems: ConnectItemReply[]; + manifest: DAppManifest; + account: Account; + walletId: WalletId; + } | null + ) => { if (!params) return; responseReset(); try { - await responseConnectionAsync({ params, replyItems, manifest }); + await responseConnectionAsync({ params, result }); } finally { setParams(null); await tonConnectSSE.reconnect(); diff --git a/apps/web/src/components/UrlTonConnectSubscription.tsx b/apps/web/src/components/UrlTonConnectSubscription.tsx index 838207ad2..0d667cbc4 100644 --- a/apps/web/src/components/UrlTonConnectSubscription.tsx +++ b/apps/web/src/components/UrlTonConnectSubscription.tsx @@ -8,6 +8,8 @@ import { import { useEffect, useState } from 'react'; import { useLocation, useNavigate } from "react-router-dom"; import { AppRoute } from "@tonkeeper/uikit/dist/libs/routes"; +import { Account } from "@tonkeeper/core/dist/entries/account"; +import { WalletId } from "@tonkeeper/core/dist/entries/wallet"; const TON_CONNECT_TRIGGER_PATH = '/ton-connect'; @@ -19,11 +21,18 @@ export const UrlTonConnectSubscription = () => { useResponseConnectionMutation(); - const handlerClose = async (replyItems?: ConnectItemReply[], manifest?: DAppManifest) => { + const handlerClose = async ( + result: { + replyItems: ConnectItemReply[]; + manifest: DAppManifest; + account: Account; + walletId: WalletId; + } | null + ) => { if (!params) return; responseReset(); try { - await responseConnectionAsync({ params, replyItems, manifest }); + await responseConnectionAsync({ params, result }); } finally { setParams(null); } diff --git a/packages/core/src/service/tonConnect/connectService.ts b/packages/core/src/service/tonConnect/connectService.ts index efa587648..b64c49db6 100644 --- a/packages/core/src/service/tonConnect/connectService.ts +++ b/packages/core/src/service/tonConnect/connectService.ts @@ -20,10 +20,9 @@ import { TonConnectAccount, TonProofItemReplySuccess } from '../../entries/tonConnect'; -import { isStandardTonWallet, TonContract } from '../../entries/wallet'; +import { isStandardTonWallet, TonContract, WalletId } from '../../entries/wallet'; import { TonWalletStandard, WalletVersion } from '../../entries/wallet'; import { accountsStorage } from '../accountsStorage'; -import { getDevSettings } from '../devStorage'; import { walletContractFromState } from '../wallet/contractService'; import { AccountConnection, @@ -403,8 +402,17 @@ export const saveWalletTonConnect = async (options: { replyItems: ConnectItemReply[]; appVersion: string; webViewUrl?: string; + walletId?: WalletId; }): Promise => { - const wallet = options.account.activeTonWallet; + const wallet = + options.walletId !== undefined + ? options.account.getTonWallet(options.walletId) + : options.account.activeTonWallet; + + if (!wallet) { + throw new Error('Missing wallet'); + } + await saveAccountConnection({ storage: options.storage, wallet, diff --git a/packages/uikit/src/components/connect/ScanButton.tsx b/packages/uikit/src/components/connect/ScanButton.tsx index 07d39fce1..5a71fd698 100644 --- a/packages/uikit/src/components/connect/ScanButton.tsx +++ b/packages/uikit/src/components/connect/ScanButton.tsx @@ -7,6 +7,8 @@ import { useAppSdk } from '../../hooks/appSdk'; import { ScanIcon } from '../Icon'; import { TonConnectNotification } from './TonConnectNotification'; import { useResponseConnectionMutation, useGetConnectInfo } from './connectHook'; +import { Account } from '@tonkeeper/core/dist/entries/account'; +import { WalletId } from '@tonkeeper/core/dist/entries/wallet'; const ScanBlock = styled.div` position: absolute; @@ -32,11 +34,18 @@ export const ScanButton = () => { [setParams, mutateAsync] ); - const handlerClose = async (replyItems?: ConnectItemReply[], manifest?: DAppManifest) => { + const handlerClose = async ( + result: { + replyItems: ConnectItemReply[]; + manifest: DAppManifest; + account: Account; + walletId: WalletId; + } | null + ) => { if (!params) return; responseReset(); try { - await responseConnectionAsync({ params, replyItems, manifest }); + await responseConnectionAsync({ params, result }); } finally { setParams(null); } diff --git a/packages/uikit/src/components/connect/TonConnectNotification.tsx b/packages/uikit/src/components/connect/TonConnectNotification.tsx index 8d54d6fa6..90aca9726 100644 --- a/packages/uikit/src/components/connect/TonConnectNotification.tsx +++ b/packages/uikit/src/components/connect/TonConnectNotification.tsx @@ -5,21 +5,24 @@ import { DAppManifest } from '@tonkeeper/core/dist/entries/tonConnect'; import { getManifest } from '@tonkeeper/core/dist/service/tonConnect/connectService'; -import React, { FC, useCallback, useEffect, useState } from 'react'; +import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; import { useAppSdk } from '../../hooks/appSdk'; import { useTranslation } from '../../hooks/translation'; import { TxConfirmationCustomError } from '../../libs/errors/TxConfirmationCustomError'; import { QueryKey } from '../../libs/queryKey'; -import { useIsActiveWalletLedger } from '../../state/ledger'; import { useConnectTonConnectAppMutation } from '../../state/tonConnect'; -import { useIsActiveWalletWatchOnly } from '../../state/wallet'; -import { CheckmarkCircleIcon, ExclamationMarkCircleIcon } from '../Icon'; +import { useAccountsState, useActiveAccount } from '../../state/wallet'; +import { CheckmarkCircleIcon, ExclamationMarkCircleIcon, SwitchIcon } from '../Icon'; import { Notification, NotificationBlock } from '../Notification'; import { Body2, Body3, H2, Label2 } from '../Text'; import { AccountAndWalletInfo } from '../account/AccountAndWalletInfo'; import { Button } from '../fields/Button'; import { ResultButton } from '../transfer/common'; +import { SelectDropDown, SelectDropDownHost, SelectField } from '../fields/Select'; +import { DropDownContent, DropDownItem, DropDownItemsDivider } from '../DropDown'; +import { Account } from '@tonkeeper/core/dist/entries/account'; +import { WalletId } from '@tonkeeper/core/dist/entries/wallet'; const Title = styled(H2)` text-align: center; @@ -75,10 +78,22 @@ const ConnectContent: FC<{ origin?: string; params: ConnectRequest; manifest: DAppManifest; - handleClose: (result?: ConnectItemReply[], manifest?: DAppManifest) => void; + handleClose: ( + result: { + replyItems: ConnectItemReply[]; + manifest: DAppManifest; + account: Account; + walletId: WalletId; + } | null + ) => void; }> = ({ params, manifest, origin, handleClose }) => { - const activeIsLedger = useIsActiveWalletLedger(); - const isReadOnly = useIsActiveWalletWatchOnly(); + const activeAccount = useActiveAccount(); + const [selectedAccountAndWallet, setSelectedAccountAndWallet] = useState<{ + account: Account; + walletId: WalletId; + }>({ account: activeAccount, walletId: activeAccount.activeTonWallet.id }); + + const isReadOnly = selectedAccountAndWallet.account.type === 'watch-only'; const sdk = useAppSdk(); const [done, setDone] = useState(false); @@ -97,9 +112,22 @@ const ConnectContent: FC<{ const onSubmit: React.FormEventHandler = async e => { e.preventDefault(); try { - const result = await mutateAsync({ request: params, manifest, webViewUrl: origin }); + const replyItems = await mutateAsync({ + request: params, + manifest, + webViewUrl: origin, + ...selectedAccountAndWallet + }); setDone(true); - setTimeout(() => handleClose(result, manifest), 300); + setTimeout( + () => + handleClose({ + replyItems, + manifest, + ...selectedAccountAndWallet + }), + 300 + ); } catch (err) { setDone(true); setError(err as Error); @@ -114,7 +142,8 @@ const ConnectContent: FC<{ } const tonProofRequested = params.items.some(item => item.name === 'ton_proof'); - const cantConnectLedger = activeIsLedger && tonProofRequested; + const cantConnectLedger = + selectedAccountAndWallet.account.type === 'ledger' && tonProofRequested; return ( @@ -127,10 +156,14 @@ const ConnectContent: FC<{ {t('ton_login_title_web').replace('%{name}', shortUrl)} {t('ton_login_caption').replace('%{name}', getDomain(manifest.name))}{' '} - + + <> {done && !error && ( @@ -170,6 +203,68 @@ const ConnectContent: FC<{ ); }; +const SelectAccountDropDown: FC<{ + className?: string; + selectedAccountAndWallet: { account: Account; walletId: WalletId }; + onSelect: (accountAndWallet: { account: Account; walletId: WalletId }) => void; +}> = ({ selectedAccountAndWallet, className, onSelect }) => { + const accounts = useAccountsState(); + const accountsAndWallets = useMemo(() => { + return accounts.flatMap(account => + account.allTonWallets.map(w => ({ + account, + walletId: w.id + })) + ); + }, [accounts]); + + return ( + ( + + {accountsAndWallets.map(accountAndWallet => ( + <> + { + onClose(); + onSelect?.(accountAndWallet); + }} + > + + + + + ))} + + )} + > + + + + + + + + ); +}; + const useManifest = (params: ConnectRequest | null) => { const sdk = useAppSdk(); const { t } = useTranslation(); @@ -203,7 +298,14 @@ const useManifest = (params: ConnectRequest | null) => { export const TonConnectNotification: FC<{ origin?: string; params: ConnectRequest | null; - handleClose: (result?: ConnectItemReply[], manifest?: DAppManifest) => void; + handleClose: ( + result: { + replyItems: ConnectItemReply[]; + manifest: DAppManifest; + account: Account; + walletId: WalletId; + } | null + ) => void; }> = ({ params, origin, handleClose }) => { const { data: manifest } = useManifest(params); @@ -220,7 +322,7 @@ export const TonConnectNotification: FC<{ }, [origin, params, manifest, handleClose]); return ( - handleClose()}> + handleClose(null)}> {Content} ); diff --git a/packages/uikit/src/components/connect/connectHook.ts b/packages/uikit/src/components/connect/connectHook.ts index 055806497..666c071af 100644 --- a/packages/uikit/src/components/connect/connectHook.ts +++ b/packages/uikit/src/components/connect/connectHook.ts @@ -17,9 +17,10 @@ import { sendEventToBridge } from '@tonkeeper/core/dist/service/tonConnect/httpB import { useAppSdk } from '../../hooks/appSdk'; import { useTranslation } from '../../hooks/translation'; import { QueryKey } from '../../libs/queryKey'; -import { useActiveAccountQuery } from '../../state/wallet'; import { BLOCKCHAIN_NAME } from '@tonkeeper/core/dist/entries/crypto'; import { useToast } from '../../hooks/useNotification'; +import { Account } from '@tonkeeper/core/dist/entries/account'; +import { WalletId } from '@tonkeeper/core/dist/entries/wallet'; export const useGetConnectInfo = () => { const sdk = useAppSdk(); @@ -70,46 +71,48 @@ export const useGetConnectInfo = () => { export interface AppConnectionProps { params: TonConnectParams; - replyItems?: ConnectItemReply[]; - manifest?: DAppManifest; + result: { + replyItems: ConnectItemReply[]; + manifest: DAppManifest; + account: Account; + walletId: WalletId; + } | null; } export const useResponseConnectionMutation = () => { const sdk = useAppSdk(); - const { data } = useActiveAccountQuery(); const client = useQueryClient(); - return useMutation( - async ({ params, replyItems, manifest }) => { - if (replyItems && manifest && data) { - const response = await saveWalletTonConnect({ - storage: sdk.storage, - account: data, - manifest, - params, - replyItems, - appVersion: sdk.version - }); - - await sendEventToBridge({ - response, - sessionKeyPair: params.sessionKeyPair, - clientSessionId: params.clientSessionId - }); + return useMutation(async ({ params, result }) => { + if (result) { + const response = await saveWalletTonConnect({ + storage: sdk.storage, + account: result.account, + walletId: result.walletId, + manifest: result.manifest, + params, + replyItems: result.replyItems, + appVersion: sdk.version + }); - await client.invalidateQueries([QueryKey.tonConnectConnection]); - await client.invalidateQueries([QueryKey.tonConnectLastEventId]); - } else { - await sendEventToBridge({ - response: connectRejectResponse(), - sessionKeyPair: params.sessionKeyPair, - clientSessionId: params.clientSessionId - }); - } + await sendEventToBridge({ + response, + sessionKeyPair: params.sessionKeyPair, + clientSessionId: params.clientSessionId + }); - return undefined; + await client.invalidateQueries([QueryKey.tonConnectConnection]); + await client.invalidateQueries([QueryKey.tonConnectLastEventId]); + } else { + await sendEventToBridge({ + response: connectRejectResponse(), + sessionKeyPair: params.sessionKeyPair, + clientSessionId: params.clientSessionId + }); } - ); + + return undefined; + }); }; export interface ResponseSendProps { diff --git a/packages/uikit/src/state/tonConnect.ts b/packages/uikit/src/state/tonConnect.ts index 3ee5bc506..5ba3f378d 100644 --- a/packages/uikit/src/state/tonConnect.ts +++ b/packages/uikit/src/state/tonConnect.ts @@ -23,20 +23,23 @@ import { useTranslation } from '../hooks/translation'; import { subject } from '../libs/atom'; import { QueryKey } from '../libs/queryKey'; import { signTonConnectOver } from './mnemonic'; -import { isStandardTonWallet, TonWalletStandard } from '@tonkeeper/core/dist/entries/wallet'; +import { + isStandardTonWallet, + TonWalletStandard, + WalletId +} from '@tonkeeper/core/dist/entries/wallet'; import { IStorage } from '@tonkeeper/core/dist/Storage'; -import { isAccountTonWalletStandard } from '@tonkeeper/core/dist/entries/account'; -import { useCheckTouchId } from './password'; import { - useAccountsState, - useAccountsStateQuery, - useActiveAccount, - useActiveApi, - useActiveTonNetwork, - useActiveWallet -} from './wallet'; + Account, + getNetworkByAccount, + isAccountTonWalletStandard +} from '@tonkeeper/core/dist/entries/account'; +import { useCheckTouchId } from './password'; +import { useAccountsState, useAccountsStateQuery, useActiveWallet } from './wallet'; import { TxConfirmationCustomError } from '../libs/errors/TxConfirmationCustomError'; import { getServerTime } from '@tonkeeper/core/dist/service/ton-blockchain/utils'; +import { getContextApiByNetwork } from '@tonkeeper/core/dist/service/walletService'; +import { useAppContext } from '../hooks/appContext'; export const useAppTonConnectConnections = () => { const sdk = useAppSdk(); @@ -89,14 +92,11 @@ export const useActiveWalletTonConnectConnections = () => { }; export const useConnectTonConnectAppMutation = () => { - const account = useActiveAccount(); + const appContext = useAppContext(); const sdk = useAppSdk(); const client = useQueryClient(); - const api = useActiveApi(); const { t } = useTranslation(); const { mutateAsync: checkTouchId } = useCheckTouchId(); - const network = useActiveTonNetwork(); - const activeIsLedger = account.type === 'ledger'; return useMutation< ConnectItemReply[], @@ -105,9 +105,18 @@ export const useConnectTonConnectAppMutation = () => { request: ConnectRequest; manifest: DAppManifest; webViewUrl?: string; + account: Account; + walletId: WalletId; + } + >(async ({ request, manifest, webViewUrl, account, walletId }) => { + const selectedIsLedget = account.type === 'ledger'; + const network = getNetworkByAccount(account); + const api = getContextApiByNetwork(appContext, network); + + const wallet = account.getTonWallet(walletId); + if (!wallet) { + throw new Error('Wallet not found'); } - >(async ({ request, manifest, webViewUrl }) => { - const wallet = account.activeTonWallet; const params = await getTonConnectParams(request); @@ -118,7 +127,7 @@ export const useConnectTonConnectAppMutation = () => { result.push(toTonAddressItemReply(wallet, network)); } if (item.name === 'ton_proof') { - if (!isStandardTonWallet(wallet) || activeIsLedger) { + if (!isStandardTonWallet(wallet) || selectedIsLedget) { throw new TxConfirmationCustomError( "Current wallet doesn't support connection to the service" );