diff --git a/package-lock.json b/package-lock.json index 85888de83..9a2368a75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@testing-library/jest-dom": "^5.16.2", "@testing-library/react": "^12.1.4", "@testing-library/user-event": "^13.5.0", + "@tonconnect/sdk": "^2.0.7", "@types/jest": "^27.4.1", "@types/node": "^16.11.26", "@types/react": "^17.0.41", @@ -46,7 +47,6 @@ "react-ga4": "^1.4.1", "react-i18next": "^11.18.3", "react-number-format": "^4.9.1", - "react-qr-code": "^2.0.7", "react-qrcode-logo": "^2.8.0", "react-redux": "^8.0.2", "react-router-dom": "^6.2.2", @@ -8541,6 +8541,26 @@ "@testing-library/dom": ">=7.21.4" } }, + "node_modules/@tonconnect/protocol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@tonconnect/protocol/-/protocol-2.0.1.tgz", + "integrity": "sha512-jkSj6EKjIlHnJxrtxdlO7KqVJe41yrIgqamGZiqziKH6iwx0m9YyKvuIREd6CmWY2jbsev3BvBWqPp9KH6HrRw==", + "dependencies": { + "tweetnacl-util": "^0.15.1" + } + }, + "node_modules/@tonconnect/sdk": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@tonconnect/sdk/-/sdk-2.0.7.tgz", + "integrity": "sha512-MDbA5RhkVbSQQYXsLuVXLATROSQgDJSIx9Y2HIPohfU44PH6vaJ1cBrv9nogIoAQUM2tNuBXC0w+RcFT+eRTCg==", + "dependencies": { + "@tonconnect/protocol": "^2.0.1", + "deepmerge": "^4.2.2", + "eventsource": "^2.0.2", + "node-fetch": "^2.6.7", + "tweetnacl": "^1.0.3" + } + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -13937,6 +13957,14 @@ "node": ">=0.8.x" } }, + "node_modules/eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/exec-async": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/exec-async/-/exec-async-2.2.0.tgz", @@ -24479,11 +24507,6 @@ "teleport": ">=0.2.0" } }, - "node_modules/qr.js": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/qr.js/-/qr.js-0.0.0.tgz", - "integrity": "sha1-ys6GOG9ZoNuAUPqQ2baw6IoeNk8=" - }, "node_modules/qrcode-generator": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/qrcode-generator/-/qrcode-generator-1.4.4.tgz", @@ -25229,24 +25252,6 @@ "react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" } }, - "node_modules/react-qr-code": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/react-qr-code/-/react-qr-code-2.0.7.tgz", - "integrity": "sha512-NpknL80p7dWbLdHfBSIxQIqLCu3+kBlyzYD692rO0UnVwfCSziIxc1xn/p3JhPEv1RV1lRD8j0w+hR3L7tawTQ==", - "dependencies": { - "prop-types": "^15.7.2", - "qr.js": "0.0.0" - }, - "peerDependencies": { - "react": "^16.x || ^17.x || ^18.x", - "react-native-svg": "*" - }, - "peerDependenciesMeta": { - "react-native-svg": { - "optional": true - } - } - }, "node_modules/react-qrcode-logo": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/react-qrcode-logo/-/react-qrcode-logo-2.8.0.tgz", @@ -28473,6 +28478,11 @@ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" }, + "node_modules/tweetnacl-util": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz", + "integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -36182,6 +36192,26 @@ "@babel/runtime": "^7.12.5" } }, + "@tonconnect/protocol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@tonconnect/protocol/-/protocol-2.0.1.tgz", + "integrity": "sha512-jkSj6EKjIlHnJxrtxdlO7KqVJe41yrIgqamGZiqziKH6iwx0m9YyKvuIREd6CmWY2jbsev3BvBWqPp9KH6HrRw==", + "requires": { + "tweetnacl-util": "^0.15.1" + } + }, + "@tonconnect/sdk": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@tonconnect/sdk/-/sdk-2.0.7.tgz", + "integrity": "sha512-MDbA5RhkVbSQQYXsLuVXLATROSQgDJSIx9Y2HIPohfU44PH6vaJ1cBrv9nogIoAQUM2tNuBXC0w+RcFT+eRTCg==", + "requires": { + "@tonconnect/protocol": "^2.0.1", + "deepmerge": "^4.2.2", + "eventsource": "^2.0.2", + "node-fetch": "^2.6.7", + "tweetnacl": "^1.0.3" + } + }, "@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -40332,6 +40362,11 @@ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" }, + "eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==" + }, "exec-async": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/exec-async/-/exec-async-2.2.0.tgz", @@ -48197,11 +48232,6 @@ "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" }, - "qr.js": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/qr.js/-/qr.js-0.0.0.tgz", - "integrity": "sha1-ys6GOG9ZoNuAUPqQ2baw6IoeNk8=" - }, "qrcode-generator": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/qrcode-generator/-/qrcode-generator-1.4.4.tgz", @@ -48760,15 +48790,6 @@ "prop-types": "^15.7.2" } }, - "react-qr-code": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/react-qr-code/-/react-qr-code-2.0.7.tgz", - "integrity": "sha512-NpknL80p7dWbLdHfBSIxQIqLCu3+kBlyzYD692rO0UnVwfCSziIxc1xn/p3JhPEv1RV1lRD8j0w+hR3L7tawTQ==", - "requires": { - "prop-types": "^15.7.2", - "qr.js": "0.0.0" - } - }, "react-qrcode-logo": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/react-qrcode-logo/-/react-qrcode-logo-2.8.0.tgz", @@ -51309,6 +51330,11 @@ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" }, + "tweetnacl-util": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz", + "integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==" + }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index 2836db6ce..99fd264c9 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@testing-library/jest-dom": "^5.16.2", "@testing-library/react": "^12.1.4", "@testing-library/user-event": "^13.5.0", + "@tonconnect/sdk": "^2.0.7", "@types/jest": "^27.4.1", "@types/node": "^16.11.26", "@types/react": "^17.0.41", @@ -41,7 +42,6 @@ "react-ga4": "^1.4.1", "react-i18next": "^11.18.3", "react-number-format": "^4.9.1", - "react-qr-code": "^2.0.7", "react-qrcode-logo": "^2.8.0", "react-redux": "^8.0.2", "react-router-dom": "^6.2.2", diff --git a/public/tonswap-manifest.json b/public/tonswap-manifest.json new file mode 100644 index 000000000..6e5c28dfd --- /dev/null +++ b/public/tonswap-manifest.json @@ -0,0 +1,5 @@ +{ + "name": "The first decentralized AMM on The Open Network", + "url": "https://tonswap.org", + "iconUrl": "https://tonswap.org/logo192.png" +} \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 6cd908c66..4b99f3328 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,18 +1,23 @@ -import { Box } from "@mui/material"; +import { Box, Typography } from '@mui/material' import AppRoutes from "router/Router"; import { Navbar } from "components"; import { LAYOUT_MAX_WIDTH } from "consts"; import { styled } from "@mui/system"; import SelectWallet from "components/SelectWallet"; -import { useWalletActions } from "store/wallet/hooks"; +import { useSelectedAdapter, useWalletActions, useWalletStore } from 'store/wallet/hooks' import { AppGrid } from "styles/styles"; -import useEffectOnce from "hooks/useEffectOnce"; import { useWebAppResize } from "store/application/hooks"; import './services/i18next/i18n'; import { useEffect } from 'react' -import { getHttpEndpoint } from '@orbs-network/ton-access' import { TonClient } from 'ton' import { setClienT } from 'services/api' +import { useDispatch, useSelector } from 'react-redux' +import { fetchTonConnectWallets } from 'store/wallet/actions' +import { RootState } from 'store/store' +import FullPageLoader from 'components/FullPageLoader' +import { useTokenOperationsStore } from 'store/token-operations/hooks' +import { isMobile } from 'react-device-detect' +import { useTranslation } from 'react-i18next' const StyledAppContainer = styled(Box)({ display: "flex", @@ -32,13 +37,16 @@ const StyledRoutesContainer = styled(AppGrid)({ }); const App = () => { - const { restoreSession } = useWalletActions(); + const { restoreSession, restoreAdapter } = useWalletActions(); + const { adapterId } = useWalletStore(); + const wallets = useSelector((state: RootState) => state.wallet.allWallets) + const walletsLength = wallets?.length + const dispatch = useDispatch() + const { txPending } = useTokenOperationsStore(); + const adapter = useSelectedAdapter() + const {t} = useTranslation() useWebAppResize(); - useEffectOnce(() => { - restoreSession(); - }); - useEffect(() => { (async () => { const _client = new TonClient({ @@ -46,10 +54,19 @@ const App = () => { }); setClienT(_client) })(); + dispatch(fetchTonConnectWallets()) + restoreSession(); }, []) + useEffect(() => { + adapterId && walletsLength && restoreAdapter(adapterId) + }, [walletsLength, adapterId]) + return ( <> + + {t('pending-transaction', {adapter: adapter?.name || ''})} + @@ -61,4 +78,4 @@ const App = () => { ); }; -export default App; +export default App; \ No newline at end of file diff --git a/src/components/FullPageLoader.tsx b/src/components/FullPageLoader.tsx index aee9036b4..04b1b9e45 100644 --- a/src/components/FullPageLoader.tsx +++ b/src/components/FullPageLoader.tsx @@ -1,12 +1,8 @@ -import Backdrop from "@mui/material/Backdrop"; -import CircularProgress from "@mui/material/CircularProgress"; -import { styled, Typography } from "@mui/material"; -import { Box } from "@mui/system"; -import { ReactNode } from "react"; -import { isMobile } from "react-device-detect"; -import { Adapters } from "services/wallets/types"; -import { useWalletStore } from "store/wallet/hooks"; -import { useTranslation } from "react-i18next"; +import Backdrop from '@mui/material/Backdrop' +import CircularProgress from '@mui/material/CircularProgress' +import { styled } from '@mui/material' +import { Box } from '@mui/system' +import { ReactNode } from 'react' interface Props { open: boolean; @@ -21,9 +17,6 @@ const StyledContainer = styled(Box)({ }); function FullPageLoader({ open, children }: Props) { - const { adapterId } = useWalletStore(); - const { t } = useTranslation(); - const showReminderInLoader = !isMobile && adapterId === Adapters.TON_HUB; return ( {children} - {showReminderInLoader && ( - - {t('check-tonhub')} - - )} ); diff --git a/src/components/Navbar/LogoWithText.tsx b/src/components/Navbar/LogoWithText.tsx index 8d351e4b0..9712f1e7d 100644 --- a/src/components/Navbar/LogoWithText.tsx +++ b/src/components/Navbar/LogoWithText.tsx @@ -8,6 +8,7 @@ import { ROUTES } from "router/routes"; import useNavigateWithParams from "hooks/useNavigateWithParams"; import { useApplicationStore } from "store/application/hooks"; import { OperationType } from "store/application/reducer"; +import { APP_VERSION } from 'consts' const StyledText = styled(Typography)(({ theme }) => ({ fontSize: 18, @@ -17,6 +18,8 @@ const StyledText = styled(Typography)(({ theme }) => ({ }, })); +const showVersionPlug = () => (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1" || window.location.hostname.includes('netlify.app') || window.location.hostname.includes('ngrok.io')) ? APP_VERSION : '' + const LogoWithText = () => { const classes = useStyles(); const { selectedToken } = useTokenOperationsStore(); @@ -45,7 +48,8 @@ const LogoWithText = () => { > - TonSwap + TonSwap{' '} + {showVersionPlug()} ); diff --git a/src/components/Navbar/Menu/WalletAddress.tsx b/src/components/Navbar/Menu/WalletAddress.tsx index dba5907cf..6541761ef 100644 --- a/src/components/Navbar/Menu/WalletAddress.tsx +++ b/src/components/Navbar/Menu/WalletAddress.tsx @@ -16,6 +16,7 @@ import { useTranslation } from "react-i18next"; import { isMobile } from "react-device-detect"; import SelectLanguage from "./SelectLanguage"; import { isTelegramWebApp } from "utils"; +import { disconnectTC } from 'services/wallets/adapters/TonConnectAdapter' const StyledIconButton = styled("button")({ cursor: "pointer", @@ -51,6 +52,7 @@ const WalletAddress = observer(() => { const onDisconnect = () => { resetWallet(); + disconnectTC(); setShowDisconnect(false); gaAnalytics.disconnect(); }; diff --git a/src/components/Popup.tsx b/src/components/Popup.tsx index 609828a4a..b7129d306 100644 --- a/src/components/Popup.tsx +++ b/src/components/Popup.tsx @@ -34,7 +34,6 @@ export function Popup({ minWidth }: Props) { const expanded = useIsExpandedView(); - return isMobile ? ( - + {typeof onClose !== 'undefined' && - + } {children} @@ -82,9 +81,9 @@ export function Popup({ }} > - + {typeof onClose !== 'undefined' && - + } {children} diff --git a/src/components/SelectWallet/AdaptersList.tsx b/src/components/SelectWallet/AdaptersList.tsx index a2a467af9..ddfd009ee 100644 --- a/src/components/SelectWallet/AdaptersList.tsx +++ b/src/components/SelectWallet/AdaptersList.tsx @@ -1,84 +1,71 @@ -import { styled } from "@mui/styles"; -import { - ListItem, - List, - ListItemButton, - Box, - Typography, - Fade, -} from "@mui/material"; -import Title from "./Title"; -import { Theme } from "@mui/material/styles"; -import { Adapter, Adapters } from "services/wallets/types"; -import CircularProgress from "@mui/material/CircularProgress"; -import gaAnalytics from "services/analytics/ga/ga"; -import { useTranslation } from "react-i18next"; - -const StyledListItem = styled(ListItem)( - ({ disabled }: { disabled?: boolean }) => ({ - background: "white", - width: "100%", - }) -); +import { styled } from '@mui/styles' +import { Box, Fade, List, ListItem, ListItemButton, Typography } from '@mui/material' +import Title from './Title' +import { Theme } from '@mui/material/styles' +import { Adapter, Adapters } from 'services/wallets/types' +import CircularProgress from '@mui/material/CircularProgress' +import gaAnalytics from 'services/analytics/ga/ga' +import { useTranslation } from 'react-i18next' +import { isMobile } from 'react-device-detect' const StyledList = styled(List)({ - width: "100%", - gap: "5px", - display: "flex", - flexDirection: "column", -}); + width: '100%', + gap: '5px', + display: 'flex', + flexDirection: 'column', +}) const StyledListItemButton = styled(ListItemButton)({ paddingLeft: 10, -}); +}) const StyledContainer = styled(Box)(({ theme }: { theme: Theme }) => ({ - width: "100%", + width: '100%', position: 'relative', - "& .MuiCircularProgress-root": { + '& .MuiCircularProgress-root': { position: 'absolute', left: '40%', top: '50%', - transform: 'translate(-50%, -50%)' + transform: 'translate(-50%, -50%)', }, -})); +})) const StyledConnectModalTitle = styled(Box)({ - paddingLeft: "10px", -}); + paddingLeft: '10px', +}) const StyledListItemRight = styled(Box)(({ theme }: { theme: Theme }) => ({ - "& h5": { + '& h5': { color: theme.palette.secondary.main, - fontSize: "18px", - fontWeight: "500", - marginBottom: "5px", + fontSize: '18px', + fontWeight: '500', + marginBottom: '5px', }, - "& p": { + '& p': { color: theme.palette.secondary.main, - fontSize: "14px", - opacity: "0.7", + fontSize: '14px', + opacity: '0.7', }, -})); +})) const StyledIcon = styled(Box)({ - width: "40px", - height: "40px", - marginRight: "24px", - display: "flex", - alignItems: "center", - justifyContent: "center", - "& img": { - width: "100%", - height: "100%", - objectFit: "cover", + width: '40px', + height: '40px', + marginRight: '24px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + '& img': { + width: '100%', + height: '100%', + objectFit: 'cover', }, - "& .MuiCircularProgress-root": { + '& .MuiCircularProgress-root': { zoom: 0.85, }, -}); +}) interface Props { - select: (adapter: Adapters) => void; + select: (adapter: Adapters, supportsTonConnect?: boolean) => void; open: boolean; onClose: () => void; adapters: Adapter[]; @@ -94,17 +81,37 @@ function AdaptersList({ adapters, adapterLoading, isLoading, - title = "Select Wallet" + title = 'Select Wallet', }: Props) { const { t } = useTranslation() + // const adaptersToShow = isMobile ? adapters.filter((adapter) => adapter.mobileCompatible || adapter.name.toLowerCase() === Adapters.TONSAFE) : adapters + const adaptersToShow = isMobile ? adapters.filter((adapter) => adapter.mobileCompatible) : adapters - const onSelect = (adapter: Adapters) => { - select(adapter) + const onSelect = (adapter: Adapters, supportsTonConnect?: boolean) => { + select(adapter, supportsTonConnect) gaAnalytics.selectWallet(adapter) } if (!open) { - return null; + return null + } + + const onAdapterSelect = (type: Adapters, tonConnect?: boolean) => { + //@ts-ignore + if (type === Adapters.OPENMASK && !window.ton.isOpenMask) { + window.open('https://www.openmask.app/', '_blank') + return + } + //@ts-ignore + if(type === Adapters.MYTONWALLET && !window.myTonWallet.isMyTonWallet) { + window.open('https://mytonwallet.io/', '_blank') + return + } + // if (type === Adapters.TONSAFE && isMobile) { + if (type === Adapters.TONSAFE) { + return + } + onSelect(type, tonConnect) } return ( @@ -116,36 +123,45 @@ function AdaptersList({ - {adapters.map((adapter) => { - const { type, icon, name, description, disabled } = adapter; + {adaptersToShow.map((adapter) => { + const { type, icon, name, description, disabled, tonConnect } = adapter + {/* disabled={adapter.name.toLowerCase() === Adapters.TONSAFE && isMobile} */} return ( - { } : () => onSelect(type)} + onClick={() => onAdapterSelect(type, tonConnect)} > - + - - {name} {disabled ? t('coming-soon') : ""} + + {title === 'Tap to connect' && 'Connect'} {name} {' '} + {/* {disabled || adapter.name.toLowerCase() === Adapters.TONSAFE && isMobile ? t('coming-soon') : ''}*/} + {disabled ? t('coming-soon') : ''} - {description} + {title !== 'Tap to connect' && {description}} - - ); + + ) })} {isLoading && } - ); + ) } -export default AdaptersList; +export default AdaptersList diff --git a/src/components/SelectWallet/DesktopFlow/QR.tsx b/src/components/SelectWallet/DesktopFlow/QR.tsx index 6ab8da8fb..13208999b 100644 --- a/src/components/SelectWallet/DesktopFlow/QR.tsx +++ b/src/components/SelectWallet/DesktopFlow/QR.tsx @@ -1,12 +1,10 @@ import {useTheme } from '@mui/styles'; import { Box } from "@mui/material"; -import QRCode from "react-qr-code"; import { QRCode as QrCodeLogo } from "react-qrcode-logo"; import Fade from "@mui/material/Fade"; import CircularProgress from "@mui/material/CircularProgress"; import Title from "../Title"; import { styled } from '@mui/system'; -import { Adapter, Adapters } from 'services/wallets/types'; const StyledContainer = styled(Box)({ display: "flex", @@ -27,34 +25,26 @@ const StyledQrBox = styled(Box)({ }); interface Props { - onClose: () => void; link?: string; open: boolean; - adapter: Adapters + title?: string + image?: string } -function QR({ onClose, link, open, adapter }: Props) { +function QR({ link, open, title, image }: Props) { const theme = useTheme() if (!open) { return null; } - let title = ''; - let qrCode = ; - if (adapter == Adapters.TON_KEEPER) { - title = 'Connect Ton Keeper'; - qrCode = < QrCodeLogo size={250} logoImage={"https://tonkeeper.com/assets/logo.svg"} value={link} /> - } else { - title = 'Connect Tonhub'; - } return ( - + <Title text={`Connect ${title}`} /> <StyledQrBox> {link ? ( <Fade in={true}> <Box> - {qrCode} + <QrCodeLogo size={250} logoImage={image} logoWidth={50} logoHeight={50} value={link} removeQrCodeBehindLogo /> </Box> </Fade> ) : ( diff --git a/src/components/SelectWallet/DesktopFlow/index.tsx b/src/components/SelectWallet/DesktopFlow/index.tsx index db1d9eca8..7c565d33d 100644 --- a/src/components/SelectWallet/DesktopFlow/index.tsx +++ b/src/components/SelectWallet/DesktopFlow/index.tsx @@ -1,11 +1,10 @@ -import { styled } from '@mui/styles'; -import { Box } from "@mui/material"; -import QR from "./QR"; -import AdaptersList from "../AdaptersList"; -import { useState } from "react"; -import { Adapters } from "services/wallets/types"; -import { adapters } from "services/wallets/config"; -import { useWalletActions, useWalletStore } from 'store/wallet/hooks'; +import { styled } from '@mui/styles' +import { Box } from '@mui/material' +import QR from './QR' +import AdaptersList from '../AdaptersList' +import { useSelectedAdapter, useWalletSelect } from 'store/wallet/hooks' +import { useSelector } from 'react-redux' +import { RootState } from 'store/store' const StyledContainer = styled(Box)({ display: "flex", @@ -21,38 +20,20 @@ interface Props { } const DesktopFlow = ({ closeModal }: Props) => { - const { resetWallet, createWalletSession } = useWalletActions(); - const [ adapter, setAdapter ] = useState<Adapters>(Adapters.TON_HUB); - const { sessionLink } = useWalletStore() - const [showQr, setShowQr] = useState(false); - - const onSelect = (adapter: Adapters) => { - resetWallet(); - setAdapter(adapter); - createWalletSession(adapter); - if (adapter === Adapters.TON_HUB) { - setShowQr(true); - } - if (adapter === Adapters.TON_KEEPER) { - setShowQr(true); - } - }; - - const cancel = () => { - setShowQr(false); - resetWallet(); - }; + const {selectWallet, session} = useWalletSelect() + const adapter = useSelectedAdapter() + const wallets = useSelector((state: RootState) => state.wallet.allWallets) return ( <StyledContainer> <AdaptersList - adapters={adapters} + adapters={wallets || []} onClose={closeModal} - open={!showQr} - select={onSelect} + open={!session} + select={selectWallet} /> - - <QR open={showQr} adapter={adapter} link={sessionLink} onClose={cancel} /> + + {adapter && session && <QR open={!!session} link={session} title={adapter.name} image={adapter.icon} />} </StyledContainer> ); } diff --git a/src/components/SelectWallet/MobileFlow/index.tsx b/src/components/SelectWallet/MobileFlow/index.tsx index 845f66cd9..2659f9ac1 100644 --- a/src/components/SelectWallet/MobileFlow/index.tsx +++ b/src/components/SelectWallet/MobileFlow/index.tsx @@ -1,11 +1,10 @@ import { styled } from "@mui/styles"; -import { Box, Link } from "@mui/material"; +import { Box } from "@mui/material"; import { observer } from "mobx-react-lite"; -import { useEffect, useMemo, useRef, useState } from "react"; -import { adapters } from "services/wallets/config"; -import { Adapters } from "services/wallets/types"; import AdaptersList from "../AdaptersList"; -import { useWalletActions, useWalletStore } from "store/wallet/hooks"; +import { useSelectedAdapter, useWalletSelect } from 'store/wallet/hooks' +import { useSelector } from 'react-redux' +import { RootState } from 'store/store' const StyledContainer = styled(Box)({ display: "flex", @@ -21,51 +20,27 @@ interface Props { } const MobileFlow = observer(({ closeModal }: Props) => { - - const { sessionLink } = useWalletStore(); - const { createWalletSession } = useWalletActions(); - const [selectedAdapter, setSelectedAdapter] = useState< - Adapters | undefined - >(); - - const filteredAdapters = useMemo( - () => adapters.filter((m) => m.mobileCompatible), - [] - ); - - const onSelect = async (adapter: Adapters) => { - - await createWalletSession(adapter); - setSelectedAdapter(adapters.find((m) => m.type === adapter)?.type); - }; + const {selectWallet, session} = useWalletSelect() + const selectedAdapter = useSelectedAdapter() + const wallets = useSelector((state: RootState) => state.wallet.allWallets) const openDeepLink = () => { - if (sessionLink) { - window.location.href = sessionLink; + if (session) { + window.location.href = session; } } - const createSessions = async () => { - // const tonhub = await createWalletSession(Adapters.TON_HUB) - // const tonkeeper = await createWalletSession(Adapters.TON_KEEPER) - } - - useEffect(() => { - - // createSessions() - }, []); - if (sessionLink && selectedAdapter) { + if (session) { - const currentAdapter = filteredAdapters.filter((a) => a.type == selectedAdapter ) return (<StyledContainer style={{ width: "100%", display:"flex" }}> <AdaptersList - adapterLoading={selectedAdapter} - adapters={currentAdapter} + adapterLoading={selectedAdapter ? selectedAdapter.type : undefined} + adapters={selectedAdapter ? [selectedAdapter] : []} onClose={closeModal} open={true} select={openDeepLink} isLoading={false} - title={"Click to connect"} + title={"Tap to connect"} /> </StyledContainer>) } @@ -73,11 +48,11 @@ const MobileFlow = observer(({ closeModal }: Props) => { return ( <StyledContainer style={{ width: "100%" }}> <AdaptersList - adapterLoading={selectedAdapter} - adapters={filteredAdapters} + adapterLoading={selectedAdapter ? selectedAdapter.type : undefined} + adapters={wallets || []} onClose={closeModal} open={true} - select={onSelect} + select={selectWallet} isLoading={false} /> </StyledContainer> diff --git a/src/consts/index.ts b/src/consts/index.ts index 1f9152fe7..214758c92 100644 --- a/src/consts/index.ts +++ b/src/consts/index.ts @@ -1,10 +1,9 @@ import { getParamsFromUrl } from "utils"; -const LOCAL_STORAGE_ADDRESS = "ton_address"; const LAYOUT_MAX_WIDTH = "1200px"; const TELEGRAM_WEBAPP_PARAM = "tg"; -const DESTINATION_PATH = "destination_path"; const APP_NAME = "TonSwap"; +const APP_VERSION = '1.0.15' const TEST_MODE = "test-mode"; const TON_WALLET_EXTENSION_URL = "https://chrome.google.com/webstore/detail/ton-wallet/nphplpgoakhhjchkkhmiggakijnkhfnd"; @@ -22,15 +21,14 @@ const colors = [ const TOKENS_IN_LOCAL_STORAGE = "user_tokens"; export { - LOCAL_STORAGE_ADDRESS, LAYOUT_MAX_WIDTH, TELEGRAM_WEBAPP_PARAM, TON_WALLET_EXTENSION_URL, - DESTINATION_PATH, APP_NAME, TEST_MODE, TOKENS_IN_LOCAL_STORAGE, colors, + APP_VERSION, }; export const COMING_SOON = "(coming soon)"; @@ -40,10 +38,6 @@ export const TELEGRAM = "https://t.me/mint_xyz"; export const SUPPORT = "https://t.me/Mint_xyz_chat"; -export const ZERO_ADDRESS = "EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c"; - -export const BETA_TEXT = "TonSwap is in Beta, use at your own risk"; - export const isDebug = () => getParamsFromUrl("beta") || localStorage["debug"]; export const DECIMALS_LIMIT = 9; diff --git a/src/hooks/useSubmitTransaction.ts b/src/hooks/useSubmitTransaction.ts new file mode 100644 index 000000000..c8feab4e9 --- /dev/null +++ b/src/hooks/useSubmitTransaction.ts @@ -0,0 +1,53 @@ +import { useSelectedAdapter, useWalletStore } from 'store/wallet/hooks' +import { client, waitForSeqno } from 'services/api' +import { Address, Cell } from 'ton' +import { walletService } from 'services/wallets/WalletService' +import { isMobile } from 'react-device-detect' +import { requestTonConnectTransaction } from 'services/wallets/adapters/TonConnectAdapter' +import { useTokenOperationsActions } from 'store/token-operations/hooks' +import { onSetTransactionStatusManually } from 'store/token-operations/actions' + +export const useSubmitTransaction = () => { + const selectedAdapter = useSelectedAdapter() + const { + getTokensBalance, + sendTransaction, + sendTonConnectTransaction, + setTransactionStatus, + } = useTokenOperationsActions() + const { address, adapterId, session } = useWalletStore() + + const submitTransaction = async (getTxRequest: () => any, sendAnalyticsEvent: () => (undefined | void), getBalances: () => Promise<any>) => { + setTransactionStatus(true) + const txRequest = await getTxRequest() + + const waiter = await waitForSeqno( + client.openWalletFromAddress({ + source: Address.parse(address!!), + }), + ) + if (!selectedAdapter?.tonConnect) { + const tx = async () => { + let deepLinkUrl = await walletService.requestTransaction(adapterId!!, session, txRequest) + if (typeof deepLinkUrl === 'string' && isMobile) { + window.location.href = deepLinkUrl + } + } + sendTransaction(tx) + await waiter() + } else { + if (isMobile) { + onSetTransactionStatusManually(true) + window.location.href = `https://app.tonkeeper.com` + } + sendTonConnectTransaction(async () => await requestTonConnectTransaction(txRequest)) + await waiter() + onSetTransactionStatusManually(false) + } + + sendAnalyticsEvent() + getTokensBalance(getBalances) + } + + return submitTransaction +} \ No newline at end of file diff --git a/src/screens/components/TokenOperations/SrcToken.tsx b/src/screens/components/TokenOperations/SrcToken.tsx index 0f47a60ca..b409a0648 100644 --- a/src/screens/components/TokenOperations/SrcToken.tsx +++ b/src/screens/components/TokenOperations/SrcToken.tsx @@ -7,10 +7,10 @@ import { } from "store/token-operations/hooks"; import { InInput } from "store/token-operations/reducer"; import { useWalletStore } from "store/wallet/hooks"; -import { fromNano, toNano } from "ton"; import { useDebouncedCallback } from "use-debounce"; import { fromDecimals, toDecimals } from "utils"; import { calculateTokens } from "./util"; + interface Props { token: PoolInfo; destTokenName: string; @@ -35,7 +35,8 @@ const SrcToken = ({ totalBalances, srcLoading, srcAvailableAmountLoading, - inInput + inInput, + txSuccess } = useTokenOperationsStore(); const { address } = useWalletStore(); @@ -107,8 +108,12 @@ const SrcToken = ({ } }, []); - - + useEffect(() => { + if(txSuccess) { + updateSrcTokenAmount(''); + updateDestTokenAmount(''); + } + }, [txSuccess]) return ( <SwapCard diff --git a/src/screens/components/TokenOperations/SuccessModal.tsx b/src/screens/components/TokenOperations/SuccessModal.tsx index 4c33ea570..b570da930 100644 --- a/src/screens/components/TokenOperations/SuccessModal.tsx +++ b/src/screens/components/TokenOperations/SuccessModal.tsx @@ -1,23 +1,26 @@ -import { Typography } from "@mui/material"; -import { Box, styled } from "@mui/system"; -import { Popup } from "components"; -import { ReactNode, useCallback } from "react"; -import { ActionType } from "services/wallets/types"; +import { Typography } from '@mui/material' +import { Box, styled } from '@mui/system' +import { Popup } from 'components' +import { ReactNode, useCallback, useEffect, useState } from 'react' +import { ActionType } from 'services/wallets/types' import { useTokenOperationsActions, useTokenOperationsStore, -} from "store/token-operations/hooks"; -import SuccessIcon from "assets/images/shared/success.svg"; -import BigNumberDisplay from "components/BigNumberDisplay"; -import { useTranslation } from "react-i18next"; +} from 'store/token-operations/hooks' +import SuccessIcon from 'assets/images/shared/success.svg' +import BigNumberDisplay from 'components/BigNumberDisplay' +import { useTranslation } from 'react-i18next' +import CircularProgress from '@mui/material/CircularProgress' + interface Props { actionType: ActionType; } function SuccessModal({ actionType }: Props) { - const { txConfirmation, txSuccess } = useTokenOperationsStore(); - const { closeSuccessModal } = useTokenOperationsActions(); + const { txConfirmation, txSuccess } = useTokenOperationsStore() + const { closeSuccessModal } = useTokenOperationsActions() const { t } = useTranslation() + const [allowActions, setAllowActions] = useState(false) const createComponent = useCallback(() => { switch (actionType) { @@ -37,14 +40,14 @@ function SuccessModal({ actionType }: Props) { </Typography> </Box> </Container> - ); + ) case ActionType.SELL: return ( - <Container title={t('purchase-confirmation')}> + <Container title={t('sell-confirmation')}> <Box className="row"> - <Typography>{t('token-received', { token: txConfirmation.tokenName })}</Typography> + <Typography>{t('token-sold', { token: txConfirmation.tokenName })}</Typography> <Typography> - <BigNumberDisplay value={txConfirmation.srcTokenAmount} />{" "} + <BigNumberDisplay value={txConfirmation.srcTokenAmount} />{' '} </Typography> </Box> <Box className="row"> @@ -54,7 +57,7 @@ function SuccessModal({ actionType }: Props) { </Typography> </Box> </Container> - ); + ) case ActionType.REMOVE_LIQUIDITY: return ( <Container title={t('liquidity-removed')}> @@ -74,7 +77,7 @@ function SuccessModal({ actionType }: Props) { </Typography> </Box> </Container> - ); + ) case ActionType.ADD_LIQUIDITY: return ( <Container title={t('liquidity-added')}> @@ -94,86 +97,122 @@ function SuccessModal({ actionType }: Props) { </Typography> </Box> </Container> - ); + ) default: - return <></>; + return <></> } - }, [actionType, txConfirmation]); + }, [actionType, txConfirmation]) return ( - <Popup open={txSuccess} onClose={closeSuccessModal} maxWidth={400}> - {createComponent()} + <Popup open={txSuccess} onClose={allowActions ? closeSuccessModal : undefined} maxWidth={400}> + <DelayedRender setAllowActions={setAllowActions} waitBeforeShow={13000}> + {createComponent()} + </DelayedRender> </Popup> - ); + ) } -export default SuccessModal; +export default SuccessModal interface ContainerProps { children: ReactNode; title: string; + showIcon?: boolean } -function Container({ children, title }: ContainerProps) { +function Container({ children, title, showIcon = true }: ContainerProps) { return ( <StyledContainer> <StyledHeader> - <img src={SuccessIcon} className="icon" /> + {showIcon && <img src={SuccessIcon} className="icon" />} <Typography>{title}</Typography> </StyledHeader> <StyledChildren>{children}</StyledChildren> </StyledContainer> - ); + ) } const StyledHeader = styled(Box)({ - display: "flex", - alignItems: "center", + display: 'flex', + alignItems: 'center', gap: 22, - flexDirection: "column", - "& p": { + flexDirection: 'column', + '& p': { fontSize: 19, fontWeight: 500, }, - "& .icon": { + '& .icon': { width: 45, height: 45, - objectFit: "contain", + objectFit: 'contain', }, -}); +}) const StyledChildren = styled(Box)({ - display: "flex", - flexDirection: "column", + display: 'flex', + flexDirection: 'column', gap: 13, - width: "100%", -}); + width: '100%', +}) const StyledContainer = styled(Box)({ - display: "flex", - flexDirection: "column", + display: 'flex', + flexDirection: 'column', gap: 19, - alignItems: "center", - width: "100%", + alignItems: 'center', + width: '100%', - "& *": { - color: "black", + '& *': { + color: 'black', }, - - "& .row": { - display: "flex", + '& .row': { + display: 'flex', gap: 40, - background: "#EEEEEE", + background: '#EEEEEE', borderRadius: 12, minHeight: 49, - width: "100%", - alignItems: "center", - justifyContent: "space-between", - padding: "10px 20px", - "& p": { + width: '100%', + alignItems: 'center', + justifyContent: 'space-between', + padding: '10px 20px', + '& p': { fontSize: 14, }, }, -}); +}) + +interface IDelayedRender { + children: ReactNode; + waitBeforeShow?: number; + setAllowActions: any; +} + +const DelayedContainer = styled(Container)({ + minWidth: 260, minHeight: 285, display: 'flex', flexDirection: 'column' +}) + +export const DelayedRender = ({ children, waitBeforeShow = 7000, setAllowActions }: IDelayedRender) => { + const [isShown, setIsShown] = useState(false) + + useEffect(() => { + const timer = setTimeout(() => { + setIsShown(true) + setAllowActions(true) + }, waitBeforeShow) + return () => clearTimeout(timer) + }, [waitBeforeShow]) + + return <Box> + {isShown ? children + : <DelayedContainer showIcon={false} title='Please wait'> + <Typography sx={{width: '100%', marginBottom: 3, textAlign: 'center'}}> + Transaction is being processed + </Typography> + <Box sx={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}> + <CircularProgress /> + </Box> + </DelayedContainer>} + </Box> +} diff --git a/src/screens/components/TokenOperations/TxError.tsx b/src/screens/components/TokenOperations/TxError.tsx index 07c37fa6b..ca59f0e50 100644 --- a/src/screens/components/TokenOperations/TxError.tsx +++ b/src/screens/components/TokenOperations/TxError.tsx @@ -9,6 +9,10 @@ import ErrorIcon from "assets/images/shared/error.png"; import { useEffect, useState } from "react"; import { delay } from "utils"; +const errorCleaner = (error: string) => { + return error.replace('[TON_CONNECT_SDK_ERROR]', '') +} + function TxError() { const { txError } = useTokenOperationsStore(); const { hideTxError } = useTokenOperationsActions(); @@ -30,9 +34,9 @@ function TxError() { return ( <Popup open={open} onClose={onClose} maxWidth={400}> - <StyledContent> + <StyledContent sx={{minWidth: 240}}> <img src={ErrorIcon} className="icon" /> - <Typography>{txError}</Typography> + <Typography>{txError && errorCleaner(txError)}</Typography> </StyledContent> </Popup> ); diff --git a/src/screens/components/TokenOperations/index.tsx b/src/screens/components/TokenOperations/index.tsx index dcb814414..a452a76fe 100644 --- a/src/screens/components/TokenOperations/index.tsx +++ b/src/screens/components/TokenOperations/index.tsx @@ -1,36 +1,33 @@ import { Box, styled } from "@mui/material"; import { useEffect, useState } from "react"; -import { ActionButton, Popup } from "components"; -import { getToken, PoolInfo } from "services/api/addresses"; +import { ActionButton } from "components"; +import { PoolInfo } from "services/api/addresses"; import WarningAmberRoundedIcon from "@mui/icons-material/WarningAmberRounded"; import { useStyles } from "./styles"; import DestToken from "./DestToken"; import SrcToken from "./SrcToken"; -import { walletService } from "services/wallets/WalletService"; import { useTokenOperationsActions, useTokenOperationsStore, } from "store/token-operations/hooks"; -import { useWalletStore } from "store/wallet/hooks"; +import { useWalletStore } from 'store/wallet/hooks' import { useIsExpandedView, useWalletModalToggle, } from "store/application/hooks"; import { StyledTokenOperationActions } from "styles/styles"; import Icon from "./Icon"; -import { ActionCategory, ActionType, Adapters } from "services/wallets/types"; -import { client, GAS_FEE, waitForSeqno } from "services/api"; -import { Address } from "ton"; +import { ActionCategory, ActionType, Adapters } from 'services/wallets/types' +import { GAS_FEE } from "services/api"; import SuccessModal from "./SuccessModal"; import useValidation from "./useValidation"; import TxError from "./TxError"; import useTxAnalytics from "./useTxAnalytics"; import gaAnalytics from "services/analytics/ga/ga"; import { useTranslation } from "react-i18next"; -import TradeInfo from "./TradeInfo"; import TxLoader from "./TxLoader"; import { isMobile } from "react-device-detect"; -import { QRCode } from "react-qrcode-logo"; +import { useSubmitTransaction } from 'hooks/useSubmitTransaction' interface Props { srcToken: PoolInfo; @@ -66,7 +63,6 @@ const TokenOperations = ({ const expanded = useIsExpandedView(); const classes = useStyles({ color: srcToken?.color || "", expanded }); const [showTxLoader, setShowTxLoader] = useState<boolean>(false); - const [keeperTransactionLink, setKeeperTransactionLink] = useState(""); const { txPending, srcTokenAmount } = useTokenOperationsStore(); const toggleModal = useWalletModalToggle(); @@ -81,53 +77,22 @@ const TokenOperations = ({ destToken ); const { - onResetAmounts, getTokensBalance, resetTokensBalance, - sendTransaction, } = useTokenOperationsActions(); const { t } = useTranslation(); + const submitTransaction = useSubmitTransaction() - - + const submitInternalTransaction = () => submitTransaction(getTxRequest, sendAnalyticsEvent, getBalances) const onSubmit = () => { if ( adapterId === Adapters.TON_HUB && isMobile) { setShowTxLoader(true) } else { - submitTransaction() + submitInternalTransaction() } }; - const submitTransaction = async () => { - const tx = async () => { - const txRequest = await getTxRequest(); - const waiter = await waitForSeqno( - client.openWalletFromAddress({ - source: Address.parse(address!!), - }) - ); - let deepLinkUrl = await walletService.requestTransaction(adapterId!!, session, txRequest); - if (typeof deepLinkUrl == "string") { - if (isMobile) { - window.location.href = deepLinkUrl; - } else { - setKeeperTransactionLink(deepLinkUrl); - } - } - await waiter(); - setTimeout(() => { - onSuccess?.() - },7000) - setKeeperTransactionLink(""); - sendAnalyticsEvent() - onResetAmounts(); - getTokensBalance(getBalances); - }; - - sendTransaction(tx); - } - useEffect(() => { if (address && refreshAmountsOnActionChange) { getTokensBalance(getBalances); @@ -145,26 +110,8 @@ const TokenOperations = ({ const closeTransactionLoader = () => { setShowTxLoader(false); - setKeeperTransactionLink(""); - } - function onClose() { - setKeeperTransactionLink("") } - function qrCodeComponent() { - - const el = ( - <Popup open={true} onClose={onClose}> - <StyledContainer> - <p>Please Scan using the QR code using TonKeeper</p> - < QRCode logoOpacity={0.5} ecLevel={"H"} size={250} value={keeperTransactionLink} /> - </StyledContainer> - </Popup> - ); - return keeperTransactionLink && !isMobile ? el : null; - } - - return ( <StyledTokenOperationActions> <TxError /> @@ -172,10 +119,8 @@ const TokenOperations = ({ open={showTxLoader} adapterId={adapterId} close={closeTransactionLoader} - confirm={submitTransaction} + confirm={submitInternalTransaction} /> - {qrCodeComponent()} - <SuccessModal actionType={actionType} /> <Box className={classes.content}> <Box @@ -208,7 +153,7 @@ const TokenOperations = ({ <Box className={classes.button}> {!address ? ( <ActionButton onClick={onConnect}>{t('connect-wallet')}</ActionButton> - ) : insufficientFunds ? ( + ) : insufficientFunds && !address.length ? ( <ActionButton isDisabled={disabled || insufficientFunds} onClick={() => { }} @@ -225,7 +170,7 @@ const TokenOperations = ({ ) : ( <ActionButton isLoading={showTxLoader || txPending} - isDisabled={disabled || insufficientFunds} + isDisabled={disabled || insufficientFunds || !address?.length} onClick={onSubmit} > {submitButtonText} diff --git a/src/services/api/deploy-pool.ts b/src/services/api/deploy-pool.ts index 58000cc90..a4a412f61 100644 --- a/src/services/api/deploy-pool.ts +++ b/src/services/api/deploy-pool.ts @@ -1,6 +1,6 @@ import BN from "bn.js"; import { Address, Cell, contractAddress, toNano, TonClient, beginDict, beginCell, StateInit, Slice } from "ton"; -import { getTokenData, _getJettonBalance } from "."; +import { getTokenData } from "."; import { Sha256 } from "@aws-crypto/sha256-js"; import { walletService } from "services/wallets/WalletService"; import { TransactionRequest } from "services/wallets/types"; diff --git a/src/services/api/index.ts b/src/services/api/index.ts index c7c9e2fef..7223a7fbd 100644 --- a/src/services/api/index.ts +++ b/src/services/api/index.ts @@ -86,7 +86,6 @@ function getOwner() { export async function _getJettonBalance(jettonWallet: Address, minterAddress?: Address) { try { console.log(`_getJettonBalance:: jettonWallet at ${jettonWallet.toFriendly()}`); - let res = await client.callGetMethod(jettonWallet, "get_wallet_data", []); const balance = hexToBn(res.stack[0][1]); const walletOwner = bytesToAddress(res.stack[1][1].bytes); @@ -100,7 +99,6 @@ export async function _getJettonBalance(jettonWallet: Address, minterAddress?: A }; } catch (e) { console.log(e); - return { balance: new BN(0), walletOwner: getOwner(), diff --git a/src/services/i18next/locales/en.json b/src/services/i18next/locales/en.json index 6a44c7e2c..af69ec90b 100644 --- a/src/services/i18next/locales/en.json +++ b/src/services/i18next/locales/en.json @@ -16,10 +16,12 @@ "token-added-pool": "{{token}} added to pool", "token-removed-pool": "{{token}} removed from pool", "ton-paid": "TON Paid", + "token-sold": "{{token}} Sold", "ton-received": "TON Received", "ton-removed-pool": "Ton removed from pool", "ton-added-pool": "Ton added to pool", "purchase-confirmation": "Purchase Confirmation", + "sell-confirmation": "Sale Confirmation", "liquidity-removed": "Liquidity Removed", "liquidity-added": "Liquidity Added", "address-copy": "Address copied", @@ -58,11 +60,12 @@ "pool-info": "Pool info", "liquidity": "Liquidity", "warning": "WARNING", - "impact-warning": "Price Impact warning!", "your-current-lp": "Your current LP position", "share-of-pool": "Share of liquidity pool", "value": "value", "no-liquidity": "No liquidity", + "impact-warning": "Price Impact warning!", + "pending-transaction": "Please check your {{adapter}} wallet for pending transaction", "search-placeholder": "Enter Jetton symbol or address", "searching-for-jetton": "Searching for Jetton", "jetton-not-found": "Jetton not found", diff --git a/src/services/i18next/locales/es.json b/src/services/i18next/locales/es.json index a1e9f7385..e47776b29 100644 --- a/src/services/i18next/locales/es.json +++ b/src/services/i18next/locales/es.json @@ -16,10 +16,12 @@ "token-added-pool": "{{token}} agregado a la piscina", "token-removed-pool": "{{token}} eliminado de la piscina", "ton-paid": "Toncoin pagado", + "token-sold": "{{token}} Vendido", "ton-received": "Toncoin recibido", "ton-removed-pool": "Toncoin eliminado de la piscina", "ton-added-pool": "Toncoin agregado a la piscina", "purchase-confirmation": "Confirmación de compra", + "sell-confirmation": "Confirmación de venta", "liquidity-removed": "Liquidez eliminada", "liquidity-added": "Liquidez agregada", "address-copy": "Dirección copiada", @@ -59,6 +61,7 @@ "liquidity": "Liquidez", "warning": "ADVERTENCIA", "impact-warning": "Advertencia de impacto de precio!", + "pending-transaction": "Verifique su billetera {{adapter}} para transacciones pendientes", "your-current-lp": "Su posición actual de LP", "share-of-pool": "Cuota de fondo de liquidez", "value": "valor", diff --git a/src/services/i18next/locales/pt.json b/src/services/i18next/locales/pt.json index 29e82503b..90bd0025f 100644 --- a/src/services/i18next/locales/pt.json +++ b/src/services/i18next/locales/pt.json @@ -16,10 +16,12 @@ "token-added-pool": "{{token}} adicionado a pool", "token-removed-pool": "{{token}} removido da pool", "ton-paid": "Toncoin Pago", + "token-sold": "{{token}} Vendido", "ton-received": "Toncoin Recebido", "ton-removed-pool": "Toncoin removido da pool", "ton-added-pool": "Toncoin adicionado a pool", "purchase-confirmation": "Confirmação de compra", + "sell-confirmation": "Confirmação de venda", "liquidity-removed": "Liquidez removida", "liquidity-added": "Liquidez adicionada", "address-copy": "Endereço copiado", @@ -59,6 +61,7 @@ "liquidity": "Liquidez", "warning": "AVISO", "impact-warning": "Aviso de impacto de preço!", + "pending-transaction": "Por favor, verifique sua carteira {{adapter}} para transações pendentes", "your-current-lp": "Sua posição atual de LP", "share-of-pool": "Participação no pool de liquidez", "value": "valor", diff --git a/src/services/i18next/locales/ru.json b/src/services/i18next/locales/ru.json index 241a42f81..0f4fc08e9 100644 --- a/src/services/i18next/locales/ru.json +++ b/src/services/i18next/locales/ru.json @@ -16,10 +16,12 @@ "token-added-pool": "{{token}} добавлен в пул", "token-removed-pool": "{{token}} удален из пула", "ton-paid": "TON оплачено", + "token-sold": "{{token}} продано", "ton-received": "TON получено", "ton-removed-pool": "TON удален из пула", "ton-added-pool": "TON добавлен в пул", "purchase-confirmation": "Подтверждение покупки", + "sell-confirmation": "Подтверждение продажи", "liquidity-removed": "Ликвидность удалена", "liquidity-added": "Добавлена ​​ликвидность", "address-copy": "Адрес скопирован", @@ -59,6 +61,7 @@ "liquidity": "Ликвидность", "warning": "ПРЕДУПРЕЖДЕНИЕ", "impact-warning": "Предупреждение о влиянии цены!", + "pending-transaction": "Пожалуйста, проверьте ваш {{adapter}} кошелек на наличие ожидающих транзакции", "your-current-lp": "Ваша текущая позиция в LP", "share-of-pool": "Доля пула ликвидности", "value": "значение", diff --git a/src/services/i18next/locales/zh.json b/src/services/i18next/locales/zh.json index 9034c4098..c69274672 100644 --- a/src/services/i18next/locales/zh.json +++ b/src/services/i18next/locales/zh.json @@ -16,10 +16,12 @@ "token-added-pool": "{{token}}添加進池 ", "token-removed-pool": "{{token}} 從池中移除", "ton-paid": "Toncoin 支付", + "token-sold": "{{token}} 賣", "ton-received": "Toncoin 收到", "ton-removed-pool": "Toncoin 從池中移除", "ton-added-pool": "Toncoin 添加進池", "purchase-confirmation": "購買確認", + "sell-confirmation": "銷售確認", "liquidity-removed": "流動性移除", "liquidity-added": "增加流動性", "address-copy": "複製地址", @@ -59,6 +61,7 @@ "liquidity": "流動性", "warning": "警告", "impact-warning": "價格影響警告!", + "pending-transaction": "請檢查您的 {{adapter}} 錢包是否有待處理的交易", "your-current-lp": "您當前的 LP 職位", "share-of-pool": "流動資金池份額", "value": "價值", diff --git a/src/services/wallets/adapters/TonConnectAdapter.ts b/src/services/wallets/adapters/TonConnectAdapter.ts new file mode 100644 index 000000000..2eba76647 --- /dev/null +++ b/src/services/wallets/adapters/TonConnectAdapter.ts @@ -0,0 +1,119 @@ +import TonConnect, { + IStorage, SendTransactionResponse, + WalletInfo, + WalletInfoInjected, + WalletInfoRemote, +} from '@tonconnect/sdk' +import { Address, Cell, StateInit } from 'ton' + +export const getWallets = () => { + return connector.getWallets() +} + +export interface TonWalletProvider { + connect(): Promise<Wallet>; + requestTransaction(request: TransactionDetails, onSuccess?: () => void): Promise<void>; + disconnect(): Promise<void>; +} + +export type TonkeeperProviderConfig = { + manifestUrl: string; + onSessionLinkReady: (link: string) => void; + storage?: IStorage; +}; + +export function stateInitToBuffer(s: StateInit): Buffer { + const INIT_CELL = new Cell(); + s.writeTo(INIT_CELL); + return INIT_CELL.toBoc(); +} + +export interface Wallet { + address: string; +} + +export interface TransactionDetails { + to: string; + value: string; + stateInit?: StateInit; + message?: Cell; +} + +// TODO revert to the link below after pr is approved +//"https://tonswap.org/tonswap-manifest.json" +export const connector = new TonConnect({ manifestUrl: "https://tonverifier.live/tonconnect-manifest.json"}); + + export async function disconnectTC(): Promise<void> { + localStorage.removeItem('ton-connect-storage_bridge-connection') + localStorage.removeItem('ton-connect-storage_http-bridge-gateway') + + try { + await connector.disconnect(); + } catch (e) { + console.log(e) + } +} + +export const getTonConnectWallets = async () => await connector.getWallets(); + +function isInjected(walletInfo: WalletInfo): walletInfo is WalletInfoInjected { + return "jsBridgeKey" in walletInfo && "injected" in walletInfo && walletInfo.injected; +} + +function isRemote(walletInfo: WalletInfo): walletInfo is WalletInfoRemote { + return "universalLink" in walletInfo && "bridgeUrl" in walletInfo; +} + +export async function connect(walletInfo: any, config: any): Promise<Wallet> { + const getWalletP = new Promise<Wallet>((resolve, reject) => { + connector.onStatusChange((wallet) => { + try { + if (wallet) { + resolve({ + address: Address.parse(wallet.account.address).toFriendly(), + }); + } else { + reject("No wallet received"); + } + } catch (e) { + reject(e); + } + }, reject); +}); + +await connector.restoreConnection(); + +if (!connector.connected) { + if (isInjected(walletInfo)) { + connector.connect({ jsBridgeKey: walletInfo.jsBridgeKey }); + } else if (isRemote(walletInfo)) { + const sessionLink = connector.connect({ + universalLink: walletInfo.universalLink, + bridgeUrl: walletInfo.bridgeUrl, + }); + config.onSessionLinkReady(sessionLink); + } else { + throw new Error("Unknown wallet type"); + } +} +return getWalletP; +} + + +export async function requestTonConnectTransaction( + request: TransactionDetails, +): Promise<SendTransactionResponse> { + await connector.restoreConnection() + + const message = { + address: request.to, + amount: request.value, + //@ts-ignore + payload: request.payload, + } + + return connector.sendTransaction({ + validUntil: Date.now() + 5 * 60 * 1000, + messages: [message], + }); +} \ No newline at end of file diff --git a/src/services/wallets/config.ts b/src/services/wallets/config.ts index 47513d928..8f7b5770a 100644 --- a/src/services/wallets/config.ts +++ b/src/services/wallets/config.ts @@ -1,30 +1,15 @@ import { Adapter, Adapters } from "./types"; import TonhubImg from "assets/images/shared/tonhub.png"; import ChromeExtImg from "assets/images/shared/chrome.svg"; -import TonKeeper from "assets/images/shared/tonkeeper.svg"; -const adapters: Adapter[] = [ - { - name: "Tonkeeper", - type: Adapters.TON_KEEPER, - icon: TonKeeper, - mobileCompatible: true, - description: "A mobile wallet in your pocket", - disabled: false, - }, +let adapters: Adapter[] = [ { name: "Tonhub", type: Adapters.TON_HUB, icon: TonhubImg, mobileCompatible: true, description: "Crypto wallet for Toncoin", - }, - { - name: "Google Chrome Plugin", - type: Adapters.TON_WALLET, - icon: ChromeExtImg, - mobileCompatible: false, - description: "TON Wallet Plugin for Google Chrome", + tonConnect: false, }, ]; diff --git a/src/services/wallets/types.ts b/src/services/wallets/types.ts index 0562b671b..43b39d0fc 100644 --- a/src/services/wallets/types.ts +++ b/src/services/wallets/types.ts @@ -1,4 +1,5 @@ import { StateInit } from "ton"; +import { WalletInfo } from '@tonconnect/sdk' export interface TransactionRequest { /** Destination */ @@ -40,7 +41,10 @@ export interface TonWalletProvider { export enum Adapters { TON_HUB = "tonhub", TON_WALLET = "ton-wallet", - TON_KEEPER = "ton-keeper", + TON_KEEPER = "tonkeeper", + OPENMASK = 'openmask', + MYTONWALLET = 'mytonwallet', + TONSAFE = 'tonsafe' } export interface Adapter { @@ -50,6 +54,9 @@ export interface Adapter { mobileCompatible: boolean; description: string; disabled?: boolean; + tonConnect?: boolean + //Only for TonConnect + walletInfo?: WalletInfo } export enum ActionCategory { diff --git a/src/store/token-operations/actions.ts b/src/store/token-operations/actions.ts index f1812a521..7e616e3c1 100644 --- a/src/store/token-operations/actions.ts +++ b/src/store/token-operations/actions.ts @@ -1,10 +1,6 @@ -import { createAsyncThunk } from "@reduxjs/toolkit"; -import gaAnalytics from "services/analytics/ga/ga"; -import { client, waitForSeqno } from "services/api"; -import { TransactionRequest } from "services/wallets/types"; -import { walletService } from "services/wallets/WalletService"; -import { Address, fromNano } from "ton"; +import { createAsyncThunk, createAction } from '@reduxjs/toolkit' import { fromDecimals } from "utils"; +import { SendTransactionResponse } from '@tonconnect/sdk' export const getAmounts = createAsyncThunk< // Return type of the payload creator @@ -21,6 +17,8 @@ export const getAmounts = createAsyncThunk< }; }); +export const onSetTransactionStatusManually = createAction<boolean>('token-operations/setTransactionStatusManually') + export const onSendTransaction = createAsyncThunk< // Return type of the payload creator any, @@ -28,3 +26,9 @@ export const onSendTransaction = createAsyncThunk< >("token-operations/sendTransaction", async (txMethod) => { await txMethod(); }); + +export const onSendTonConnectTransaction = createAsyncThunk< + any, () => Promise<SendTransactionResponse> + >("token-operations/sendTonConnectTransaction", async (res) => { + await res() +}) \ No newline at end of file diff --git a/src/store/token-operations/hooks.ts b/src/store/token-operations/hooks.ts index d2ad4bb4d..e2886ca5c 100644 --- a/src/store/token-operations/hooks.ts +++ b/src/store/token-operations/hooks.ts @@ -1,4 +1,4 @@ -import { useCallback, useRef } from "react"; +import { useCallback } from "react"; import { useDispatch, useSelector } from "react-redux"; import { ROUTES } from "router/routes"; import { RootState } from "store/store"; @@ -18,9 +18,10 @@ import { toggleAction, } from "./reducer"; -import { getAmounts, onSendTransaction } from "./actions"; +import { getAmounts, onSendTransaction, onSendTonConnectTransaction, onSetTransactionStatusManually } from './actions' import useNavigateWithParams from "hooks/useNavigateWithParams"; import { PoolInfo } from "services/api/addresses"; +import { SendTransactionResponse } from '@tonconnect/sdk' export const useTokenOperationsStore = () => { return useSelector((state: RootState) => state.tokenOperations); @@ -41,6 +42,8 @@ export const useTokenOperationsActions = (): { clearStore: () => void; selectToken: (token?: PoolInfo) => void; sendTransaction: (txMethod: () => Promise<void>) => void; + sendTonConnectTransaction: (res: () => Promise<SendTransactionResponse>) => void; + setTransactionStatus: (status: boolean) => void; hideTxError: () => void; closeSuccessModal: () => void; onInputChange: (inInput :InInput) => void; @@ -161,6 +164,15 @@ export const useTokenOperationsActions = (): { [dispatch] ); + const sendTonConnectTransaction = useCallback( + ( + res: () => Promise<SendTransactionResponse> + ) => { + dispatch(onSendTonConnectTransaction(res)); + }, + [dispatch] + ); + const onInputChange = useCallback( (inInput: InInput) => { @@ -177,6 +189,13 @@ export const useTokenOperationsActions = (): { [dispatch] ); + const setTransactionStatus = useCallback( + (status: boolean) => { + dispatch(onSetTransactionStatusManually(status)) + }, + [dispatch] + ) + return { onResetAmounts, updateSrcTokenAmount, @@ -192,8 +211,10 @@ export const useTokenOperationsActions = (): { clearStore, selectToken, sendTransaction, + sendTonConnectTransaction, hideTxError, closeSuccessModal, - onInputChange + onInputChange, + setTransactionStatus }; }; diff --git a/src/store/token-operations/reducer.ts b/src/store/token-operations/reducer.ts index 49a561d97..1fcb7c962 100644 --- a/src/store/token-operations/reducer.ts +++ b/src/store/token-operations/reducer.ts @@ -1,7 +1,7 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { PoolInfo } from "services/api/addresses"; -import { getAmounts, onSendTransaction } from "./actions"; +import { getAmounts, onSendTonConnectTransaction, onSendTransaction, onSetTransactionStatusManually } from './actions' interface TxConfirmation { destTokenAmount: string; @@ -134,9 +134,28 @@ const WalletOperationSlice = createSlice({ .addCase(onSendTransaction.fulfilled, (state, action) => { state.txPending = false; state.txSuccess = true; - }); + }) + .addCase(onSendTonConnectTransaction.pending, (state, action) => { + state.txPending = true; + state.txConfirmation = { + destTokenAmount: state.destTokenAmount, + srcTokenAmount: state.srcTokenAmount, + tokenName: state.selectedToken?.displayName, + }; + }) + .addCase(onSendTonConnectTransaction.rejected, (state, action) => { + state.txError = action.error.message; + state.txPending = false; + }) + .addCase(onSendTonConnectTransaction.fulfilled, (state, action) => { + state.txPending = false; + state.txSuccess = true; + }) + .addCase(onSetTransactionStatusManually, (state, action) => { + state.txPending = action.payload + }) }, -}); +}) export const { resetState, diff --git a/src/store/wallet/actions.ts b/src/store/wallet/actions.ts index b2d2e4001..a14b1188a 100644 --- a/src/store/wallet/actions.ts +++ b/src/store/wallet/actions.ts @@ -1,29 +1,75 @@ -import { createAction, createAsyncThunk } from "@reduxjs/toolkit"; -import { Adapters, Wallet } from "services/wallets/types"; -import { walletService } from "services/wallets/WalletService"; +import { createAction, createAsyncThunk } from '@reduxjs/toolkit' +import { Adapter, Adapters, Wallet } from 'services/wallets/types' +import { walletService } from 'services/wallets/WalletService' +import { getTonConnectWallets } from 'services/wallets/adapters/TonConnectAdapter' export const onWalletConnect = createAction<{ - wallet: Wallet; - _adapterId?: string; -}>("wallet/onWalletConnect"); -export const resetWallet = createAction("wallet/resetWallet"); -export const setConnecting = createAction<boolean>("wallet/setConnecting"); - -export const setSession = createAction<string | {}>("wallet/setSession"); -export const updateWallet = createAction<any>("wallet/updateWallet"); - -export const awaitWalletReadiness = createAsyncThunk< - // Return type of the payload creator - { wallet: Wallet; adapterId: Adapters }, - { adapterId: Adapters; session: string | {} } ->("wallet/createWalletSession", async ({ adapterId, session }, thunkApi) => { - const wallet = await walletService.awaitReadiness(adapterId, session); - if (!wallet) { - thunkApi.dispatch(resetWallet); - throw new Error(""); - } - return { - wallet, - adapterId, - }; -}); + wallet: Wallet; + _adapterId?: string; +}>('wallet/onWalletConnect') +export const resetWallet = createAction('wallet/resetWallet') +export const setConnecting = createAction<boolean>('wallet/setConnecting') +export const setAdapter = createAction<Adapter>('wallet/setAdapter') + +export const setTonHubSession = createAction<string | {}>('wallet/setTonHubSession') +export const updateWallet = createAction<any>('wallet/updateWallet') + +export const awaitWalletReadiness = createAsyncThunk<{ wallet: Wallet; adapterId: Adapters }, + { adapterId: Adapters; session: string | {} }>('wallet/createWalletSession', async ({ + adapterId, + session, +}, thunkApi) => { + + const wallet = await walletService.awaitReadiness(adapterId, session) + if (!wallet) { + thunkApi.dispatch(resetWallet) + throw new Error('') + } + return { + wallet, + adapterId, + } +}) + +const defineWalletDescription: any = (name: string) => { + if (name === 'OpenMask' || name === 'MyTonWallet') { + return 'TON Wallet Plugin for Google Chrome' + } + return 'A mobile wallet in your pocket' +} + +const defineIsMobileCompatible: any = (name: string) => { + if (name === 'OpenMask') { + return false + } + if (name === 'Tonkeeper') { + return true + } + if (name === 'MyTonWallet') { + return false + } +} + +export const fetchTonConnectWallets = createAsyncThunk<Adapter[]>( + 'wallet/fetchTonConnectWallets', async () => { + const supportedWallets = await getTonConnectWallets() + return supportedWallets.map((w) => { + //@ts-ignore + let _type = w.jsBridgeKey + if(w.name.toLowerCase() === Adapters.TONSAFE) { + _type = Adapters.TONSAFE + } + + return { + name: w.name, + type: _type, + icon: w.imageUrl, + mobileCompatible: defineIsMobileCompatible(w.name), + description: defineWalletDescription(w.name), + tonConnect: true, + walletInfo: w, + disabled: _type === Adapters.TONSAFE + } + }) + }, +) \ No newline at end of file diff --git a/src/store/wallet/hooks.ts b/src/store/wallet/hooks.ts index 1dd0d7e8d..e9c6645b5 100644 --- a/src/store/wallet/hooks.ts +++ b/src/store/wallet/hooks.ts @@ -1,61 +1,128 @@ -import { useCallback } from "react"; -import { useDispatch, useSelector } from "react-redux"; -import { Adapters } from "services/wallets/types"; -import { walletService } from "services/wallets/WalletService"; -import { RootState } from "store/store"; -import { awaitWalletReadiness, resetWallet, setConnecting, setSession, updateWallet } from "./actions"; +import { useCallback, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { Adapter, Adapters } from 'services/wallets/types' +import { walletService } from 'services/wallets/WalletService' +import { RootState } from 'store/store' +import { awaitWalletReadiness, resetWallet, setAdapter, setConnecting, setTonHubSession, updateWallet } from './actions' +import { connect, disconnectTC, Wallet } from 'services/wallets/adapters/TonConnectAdapter' +import { Address } from 'ton' + +const findAdapter = (wallets: any, adapterId: Adapters): Adapter | null => wallets?.find((wallet: Adapter) => wallet.type === adapterId) || null export const useWalletStore = () => { - return useSelector((state: RootState) => state.wallet); -}; + return useSelector((state: RootState) => state.wallet) +} + +export const useSelectedAdapter = () => { + return useSelector((state: RootState) => state.wallet.adapter) +} + +export const useWalletSelect = () => { + const dispatch = useDispatch<any>() + const [localSession, setLocalSession] = useState<null | string>(null) + const { resetWallet } = useWalletActions() + const wallets = useSelector((state: RootState) => state.wallet.allWallets) + + const selectWallet = async (adapterId: Adapters, supportsTonConnect?: boolean) => { + resetWallet() + disconnectTC() + const adapter: Adapter | null = findAdapter(wallets, adapterId) + if (!adapter) { + return + } + dispatch(setAdapter(adapter)) + if (!supportsTonConnect) { + const _session: string | {} = await walletService.createSession(adapterId) + dispatch(setTonHubSession(_session)) + dispatch(awaitWalletReadiness({ adapterId, session: _session })) + const parsedSession = typeof _session === 'string' ? JSON.parse(_session) : _session + const deepLink = parsedSession.link.replace('ton-test://', 'https://test.tonhub.com/').replace('ton://', 'https://tonhub.com/') + setLocalSession(deepLink) + return + } + if (adapterId === Adapters.OPENMASK && (window.ton as any).isOpenMask) { + const accounts = await window.ton?.send('ton_requestAccounts') + dispatch(updateWallet({ wallet: { address: accounts[0] }, adapterId: Adapters.OPENMASK })) + localStorage.setItem('ton-connect-storage_bridge-connection', '{"type":"injected","jsBridgeKey":"openmask"}') + return + } + if (adapterId === Adapters.MYTONWALLET && (window as any).myTonWallet.isMyTonWallet) { + const wallet = await connect(adapter.walletInfo, { + onSessionLinkReady: (val: string) => { + setLocalSession(val) + }, + }) + dispatch(updateWallet({ wallet, adapterId: Adapters.MYTONWALLET })) + return + } + const wallet = await connect(adapter.walletInfo, { + onSessionLinkReady: (val: string) => { + setLocalSession(val) + }, + }) + dispatch(updateWallet({ wallet, adapterId: adapterId })) + } + return { selectWallet, session: localSession } +} export const useWalletActions = (): { - createWalletSession: (adapterId: Adapters) => void; - restoreSession: () => void; - resetWallet: () => void; + restoreSession: () => void; + resetWallet: () => void; + restoreAdapter: (adapterId: string) => void; } => { - const dispatch = useDispatch<any>(); - - const createWalletSession = useCallback( - async (adapterId: Adapters) => { - const session: string | {} = await walletService.createSession(adapterId); - dispatch(setSession(session)); - dispatch(awaitWalletReadiness({ adapterId, session })); - return session; - }, - [dispatch] - ); - - const reset = useCallback(async () => { - dispatch(resetWallet()); - }, [dispatch]); - - const restoreSession = useCallback(() => { - const adapterId = localStorage.getItem("wallet:adapter-id"); - const session = localStorage.getItem("wallet:session"); - - if (!adapterId || !session) { - dispatch(setConnecting(false)); - return; - } - if (adapterId == Adapters.TON_KEEPER) { - const wallet = { - address: JSON.parse(session).address, - publicKey: "", - walletVersion: "4", - }; - dispatch(updateWallet({ wallet, adapterId: Adapters.TON_KEEPER })); - return; - } - - dispatch(setSession(session)); - dispatch( - awaitWalletReadiness({ - adapterId: adapterId as Adapters, - session: JSON.parse(session), - }) - ); - }, [dispatch]); - - return { createWalletSession, restoreSession, resetWallet: reset }; -}; + const dispatch = useDispatch<any>() + const wallets = useSelector((state: RootState) => state.wallet.allWallets) + + const reset = useCallback(async () => { + dispatch(resetWallet()) + }, [dispatch]) + + const restoreAdapter = (adapterId: string) => { + const adapter: Adapter | null = findAdapter(wallets, adapterId as Adapters) + if (!adapter) { + return + } + dispatch(setAdapter(adapter)) + } + + const restoreSession = useCallback(async () => { + const adapterId = localStorage.getItem('wallet:adapter-id') + const session = localStorage.getItem('wallet:session') + + const tcBridgeConnection = JSON.parse(localStorage.getItem('ton-connect-storage_bridge-connection') || '{}') + const tcBridgeGateway = localStorage.getItem('ton-connect-storage_http-bridge-gateway') + + if (adapterId && session) { + dispatch(setTonHubSession(session)) + dispatch( + awaitWalletReadiness({ + adapterId: adapterId as Adapters, + session: JSON.parse(session), + }), + ) + return + } + + if (tcBridgeConnection.jsBridgeKey === Adapters.OPENMASK) { + const accounts = await window.ton?.send('ton_requestAccounts') + dispatch(updateWallet({ wallet: { address: accounts[0] }, adapterId: Adapters.OPENMASK })) + return + } + if (tcBridgeConnection.jsBridgeKey === Adapters.MYTONWALLET) { + //@ts-ignore + const accounts = await window.myTonWallet.send('ton_requestAccounts') + dispatch(updateWallet({ wallet: { address: accounts[0] }, adapterId: Adapters.MYTONWALLET })) + return + } + if (tcBridgeConnection?.session && tcBridgeGateway?.length) { + const wallet: Wallet = { address: Address.parse(tcBridgeConnection.connectEvent.payload.items[0].address).toFriendly() } + const _adapter = tcBridgeConnection.connectEvent.payload.device.appName.toLowerCase() + dispatch(updateWallet({ wallet, adapterId: _adapter })) + return + } + dispatch(setConnecting(false)) + return + }, [dispatch]) + + return { restoreSession, resetWallet: reset, restoreAdapter } +} diff --git a/src/store/wallet/reducer.ts b/src/store/wallet/reducer.ts index fcd82e240..5cba21085 100644 --- a/src/store/wallet/reducer.ts +++ b/src/store/wallet/reducer.ts @@ -1,6 +1,15 @@ import { createReducer } from "@reduxjs/toolkit"; -import { Wallet } from "services/wallets/types"; -import { awaitWalletReadiness, resetWallet, setConnecting, setSession, updateWallet } from "./actions"; +import { Adapter, Adapters, Wallet } from 'services/wallets/types' +import { + awaitWalletReadiness, + fetchTonConnectWallets, + resetWallet, + setConnecting, + setTonHubSession, + setAdapter, + updateWallet, +} from './actions' +import { adapters } from 'services/wallets/config' interface State { address?: string; @@ -10,6 +19,10 @@ interface State { adapterId?: string; sessionLink?: string; connecting: boolean; + tonConnectWallets?: Adapter[] + allWallets?: Adapter[] + mobileWallets?: Adapter[] + adapter?: Adapter } const initialState: State = { @@ -42,8 +55,9 @@ const reducer = createReducer(initialState, (builder) => { state.wallet = payload.wallet; state.adapterId = payload.adapterId; state.address = payload.wallet.address; + state.connecting = false }) - .addCase(setSession, (state, action) => { + .addCase(setTonHubSession, (state, action) => { const { payload } = action; const session = typeof payload === "string" ? JSON.parse(payload) : payload; state.session = session; @@ -57,11 +71,24 @@ const reducer = createReducer(initialState, (builder) => { state.wallet = wallet; state.adapterId = adapterId; state.address = wallet.address; - localStorage.setItem("wallet:adapter-id", adapterId); - localStorage.setItem("wallet:session", JSON.stringify({ ...state.session, address: wallet.address })); + if(!!state.session && adapterId) { + localStorage.setItem("wallet:adapter-id", adapterId); + localStorage.setItem("wallet:session", JSON.stringify({ ...state.session, address: wallet.address })); + } state.connecting = false; - }); + }) + + .addCase(fetchTonConnectWallets.fulfilled, (state, action) => { + state.tonConnectWallets = action.payload + const _allWallets = [...adapters, ...action.payload] + state.allWallets = _allWallets + // state.mobileWallets = _allWallets.filter((wallet) => wallet.mobileCompatible || wallet.name.toLowerCase() === Adapters.TONSAFE) + state.mobileWallets = _allWallets.filter((wallet) => wallet.mobileCompatible) + }) + .addCase(setAdapter, (state, action) => { + state.adapter = action.payload + }) // Or, you can reference the .type field: // if using TypeScript, the action type cannot be inferred that way });