diff --git a/package-lock.json b/package-lock.json index 4d0e4e3f..98f05b2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,13 +10,13 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { - "@cityofzion/blockchain-service": "1.8.0", + "@cityofzion/blockchain-service": "1.10.0", "@cityofzion/bs-asteroid-sdk": "0.9.0", - "@cityofzion/bs-electron": "0.1.19", - "@cityofzion/bs-ethereum": "2.7.0", - "@cityofzion/bs-neo-legacy": "1.6.0", - "@cityofzion/bs-neo3": "1.8.2", - "@cityofzion/bs-swap": "0.1.1", + "@cityofzion/bs-electron": "0.1.21", + "@cityofzion/bs-ethereum": "2.7.2", + "@cityofzion/bs-neo-legacy": "1.6.2", + "@cityofzion/bs-neo3": "1.8.3", + "@cityofzion/bs-swap": "0.2.1", "@cityofzion/neon-core": "5.5.1", "@cityofzion/neon-js": "5.5.1", "@cityofzion/wallet-connect-sdk-wallet-core": "4.4.0", @@ -568,9 +568,9 @@ } }, "node_modules/@cityofzion/blockchain-service": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@cityofzion/blockchain-service/-/blockchain-service-1.8.0.tgz", - "integrity": "sha512-g4FeVLGMxKirbI16q6KpAQ3gsZJG6uzGEOkTEU7Xqahic+wDtp9EOYMt47327g8WiNFr9uw6L6BRLgTFglz0TQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@cityofzion/blockchain-service/-/blockchain-service-1.10.0.tgz", + "integrity": "sha512-VaHlwHFSawK3TilN0wgv8tEOvBLuaKxFl7fNIGacnnpj4GwWyc+0xzv0ZwwzwgDgwneyM153roKVkcuzH/k+fg==", "license": "MIT", "dependencies": { "@ledgerhq/hw-transport": "~6.30.5", @@ -604,12 +604,12 @@ } }, "node_modules/@cityofzion/bs-electron": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@cityofzion/bs-electron/-/bs-electron-0.1.19.tgz", - "integrity": "sha512-+jNjCiSCgUstShrwyan8nqaipeZDtKMEn3Qz6Eq7a3Ebxgq2bsyMGqcG+ntvrsjU0Voa/AonaLCNrdL9f9lyWw==", + "version": "0.1.21", + "resolved": "https://registry.npmjs.org/@cityofzion/bs-electron/-/bs-electron-0.1.21.tgz", + "integrity": "sha512-Um+zCAvxPcZrQ8bm41YaWBX8K32wc9TBHDChOIZLlOfGj5yV+d7QB2v99iST47TkmjGLGTSTFLFQUewYdNQZlg==", "license": "MIT", "dependencies": { - "@cityofzion/blockchain-service": "1.8.0", + "@cityofzion/blockchain-service": "1.10.0", "lodash.clonedeep": "~4.5.0" }, "peerDependencies": { @@ -618,12 +618,12 @@ } }, "node_modules/@cityofzion/bs-ethereum": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@cityofzion/bs-ethereum/-/bs-ethereum-2.7.0.tgz", - "integrity": "sha512-tl63b6IvlsBLtd/1KTmoP+XjFLbWWfy9rzeDRYcq+jAoOvjIPEZ1LyAAr8AD2pMax24ZED5YXhTBGEM7UXSdVg==", + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@cityofzion/bs-ethereum/-/bs-ethereum-2.7.2.tgz", + "integrity": "sha512-5kQJ4GWaPZ57FW72Zw5/l5LtqKxUjddANvydNfl65HyZtdHrT0hM5s5XAbCwDBjvjT5XCt+47tQzlhuTShF4EQ==", "license": "MIT", "dependencies": { - "@cityofzion/blockchain-service": "1.8.0", + "@cityofzion/blockchain-service": "1.10.0", "@ethersproject/abstract-signer": "~5.7.0", "@ethersproject/bignumber": "5.7.0", "@ethersproject/bytes": "5.7.0", @@ -757,12 +757,12 @@ } }, "node_modules/@cityofzion/bs-neo-legacy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@cityofzion/bs-neo-legacy/-/bs-neo-legacy-1.6.0.tgz", - "integrity": "sha512-oJs9c6BUekhtTDxLcUo/4f3oCvRA8L5snxKfL0fq9dJDjvm731rdSPI6abq0d2FCfsuve/s47D25SLwcppUDvg==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@cityofzion/bs-neo-legacy/-/bs-neo-legacy-1.6.2.tgz", + "integrity": "sha512-GnwxdSqvJ5NoyPbL9QdVx1kUqQpivSByOjoRIEeMxR60hJ07f78mOThbLey80fQyfR3TZiMzpk8BQr4YRIFZ1Q==", "license": "MIT", "dependencies": { - "@cityofzion/blockchain-service": "1.8.0", + "@cityofzion/blockchain-service": "1.10.0", "@cityofzion/bs-asteroid-sdk": "0.9.0", "@cityofzion/dora-ts": "0.0.11", "@cityofzion/neon-js": "4.8.3" @@ -850,11 +850,12 @@ } }, "node_modules/@cityofzion/bs-neo3": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/@cityofzion/bs-neo3/-/bs-neo3-1.8.2.tgz", - "integrity": "sha512-Q5I7ogjoo6YEyniC43EWaOCT91gHX0vZx4cTQ+UnDBvOr5oiZThBSK7nkFT428CW6hZR20YCuMQdWvFkfnpvoA==", + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@cityofzion/bs-neo3/-/bs-neo3-1.8.3.tgz", + "integrity": "sha512-8fHDxIv4wTdB7m7HKqu5xVb4Rw8rxV+5AYMcS6SsMyZTdMQGnqxW2QfvZ17wLuCdbScu4HJIIPKurDUujkl5xQ==", + "license": "MIT", "dependencies": { - "@cityofzion/blockchain-service": "1.9.0", + "@cityofzion/blockchain-service": "1.10.0", "@cityofzion/bs-asteroid-sdk": "0.9.0", "@cityofzion/dora-ts": "0.0.11", "@cityofzion/neon-core": "5.5.1", @@ -868,17 +869,10 @@ "query-string": "7.1.3" } }, - "node_modules/@cityofzion/bs-neo3/node_modules/@cityofzion/blockchain-service": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@cityofzion/blockchain-service/-/blockchain-service-1.9.0.tgz", - "integrity": "sha512-lolAD/TzXyIZxifYI/mFBvZSeBIblWfotfdpKjpfarVCZFQmNnOY/ebeX+5o+Twq+W1H+YebjxtYMt+zlvCUiw==", - "dependencies": { - "@ledgerhq/hw-transport": "~6.30.5", - "axios": "1.5.1" - } - }, "node_modules/@cityofzion/bs-neo3/node_modules/@ledgerhq/hw-transport": { "version": "6.30.6", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport/-/hw-transport-6.30.6.tgz", + "integrity": "sha512-fT0Z4IywiuJuZrZE/+W0blkV5UCotDPFTYKLkKCLzYzuE6javva7D/ajRaIeR+hZ4kTmKF4EqnsmDCXwElez+w==", "license": "Apache-2.0", "dependencies": { "@ledgerhq/devices": "^8.3.0", @@ -889,6 +883,8 @@ }, "node_modules/@cityofzion/bs-neo3/node_modules/axios": { "version": "1.5.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz", + "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.0", @@ -904,47 +900,17 @@ } }, "node_modules/@cityofzion/bs-swap": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@cityofzion/bs-swap/-/bs-swap-0.1.1.tgz", - "integrity": "sha512-kIJcOMQDTicj7FJWL5n6o5E1L12zP/QBouTWu2XNOGjNtWEGIBpckqzYChzsZwrBjpuZD4NhGeNgXaMj3ZVlMg==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@cityofzion/bs-swap/-/bs-swap-0.2.1.tgz", + "integrity": "sha512-LB3pHjYHyb8Gruhux8zi5AfKaYrATIDL6bPKu72Z9QS6Zs5NaOnRDN5UzugT6rrutWJloEelI5Vw8zUMZ1s3WA==", "license": "MIT", "dependencies": { - "@cityofzion/blockchain-service": "1.8.0", - "@cityofzion/bs-neo3": "1.8.0", + "@cityofzion/blockchain-service": "1.10.0", + "@cityofzion/bs-neo3": "1.8.3", "axios": "1.5.1", "lodash": "~4.17.21" } }, - "node_modules/@cityofzion/bs-swap/node_modules/@cityofzion/bs-neo3": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@cityofzion/bs-neo3/-/bs-neo3-1.8.0.tgz", - "integrity": "sha512-YW++gN2qvl6ZhWf82JFkCHazNsVLJmskS3rIP38rFjgZqVNDrVI7jCMvMeT9k+Qwh9pAKBZHGjAYNrpk8cNqqw==", - "dependencies": { - "@cityofzion/blockchain-service": "1.8.0", - "@cityofzion/bs-asteroid-sdk": "0.9.0", - "@cityofzion/dora-ts": "0.0.11", - "@cityofzion/neon-core": "5.5.1", - "@cityofzion/neon-dappkit": "0.4.1", - "@cityofzion/neon-js": "5.5.1", - "@ledgerhq/hw-transport": "~6.30.5", - "axios": "1.5.1", - "bignumber.js": "^9.1.2", - "isomorphic-ws": "^5.0.0", - "lodash.clonedeep": "^4.5.0", - "query-string": "7.1.3" - } - }, - "node_modules/@cityofzion/bs-swap/node_modules/@ledgerhq/hw-transport": { - "version": "6.30.6", - "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport/-/hw-transport-6.30.6.tgz", - "integrity": "sha512-fT0Z4IywiuJuZrZE/+W0blkV5UCotDPFTYKLkKCLzYzuE6javva7D/ajRaIeR+hZ4kTmKF4EqnsmDCXwElez+w==", - "dependencies": { - "@ledgerhq/devices": "^8.3.0", - "@ledgerhq/errors": "^6.16.4", - "@ledgerhq/logs": "^6.12.0", - "events": "^3.3.0" - } - }, "node_modules/@cityofzion/bs-swap/node_modules/axios": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz", @@ -956,14 +922,6 @@ "proxy-from-env": "^1.1.0" } }, - "node_modules/@cityofzion/bs-swap/node_modules/isomorphic-ws": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", - "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", - "peerDependencies": { - "ws": "*" - } - }, "node_modules/@cityofzion/dora-ts": { "version": "0.0.11", "dependencies": { diff --git a/package.json b/package.json index ba569f24..86d27cbe 100644 --- a/package.json +++ b/package.json @@ -29,13 +29,13 @@ "playwright:report": "npx playwright show-report" }, "dependencies": { - "@cityofzion/blockchain-service": "1.8.0", + "@cityofzion/blockchain-service": "1.10.0", "@cityofzion/bs-asteroid-sdk": "0.9.0", - "@cityofzion/bs-electron": "0.1.19", - "@cityofzion/bs-ethereum": "2.7.0", - "@cityofzion/bs-neo-legacy": "1.6.0", - "@cityofzion/bs-neo3": "1.8.2", - "@cityofzion/bs-swap": "0.1.1", + "@cityofzion/bs-electron": "0.1.21", + "@cityofzion/bs-ethereum": "2.7.2", + "@cityofzion/bs-neo-legacy": "1.6.2", + "@cityofzion/bs-neo3": "1.8.3", + "@cityofzion/bs-swap": "0.2.1", "@cityofzion/neon-core": "5.5.1", "@cityofzion/neon-js": "5.5.1", "@cityofzion/wallet-connect-sdk-wallet-core": "4.4.0", diff --git a/src/renderer/src/assets/images/simple-swap-logo.png b/src/renderer/src/assets/images/simple-swap-logo.png deleted file mode 100644 index 66292840..00000000 Binary files a/src/renderer/src/assets/images/simple-swap-logo.png and /dev/null differ diff --git a/src/renderer/src/components/Details.tsx b/src/renderer/src/components/Details.tsx new file mode 100644 index 00000000..9b408fcb --- /dev/null +++ b/src/renderer/src/components/Details.tsx @@ -0,0 +1,90 @@ +import { cloneElement, ComponentProps } from 'react' +import { MdOutlineContentCopy } from 'react-icons/md' +import { StyleHelper } from '@renderer/helpers/StyleHelper' +import { UtilsHelper } from '@renderer/helpers/UtilsHelper' + +import { IconButton } from './IconButton' +import { Separator } from './Separator' + +type TRootProps = ComponentProps<'div'> +const Root = ({ className, children, ...props }: TRootProps) => { + return ( +
+ {children} +
+ ) +} + +type THeaderProps = { + label: string + icon?: JSX.Element +} & ComponentProps<'div'> +const Header = ({ label, icon, children, ...props }: THeaderProps) => { + return ( +
+
+ {icon && cloneElement(icon, { className: StyleHelper.mergeStyles('text-blue w-6 h-6', icon.props.className) })} + + {label} + + {children} +
+ + +
+ ) +} +type TBodyProps = ComponentProps<'div'> +const Body = ({ className, children, ...props }: TBodyProps) => { + return ( +
+ {children} +
+ ) +} + +type TPanelProps = { label: string } & ComponentProps<'div'> +const Panel = ({ className, children, label, ...props }: TPanelProps) => { + return ( +
+
{label}
+ + {children} +
+ ) +} + +type TItemProps = { label: string; copyable?: boolean } & ComponentProps<'div'> +const Item = ({ label, children, copyable, className, ...props }: TItemProps) => { + const handleCopy = () => { + if (typeof children !== 'string') return + UtilsHelper.copyToClipboard(children) + } + return ( +
+
+ {label} + +
+ {typeof children === 'string' ? {children} : children} + + {copyable && ( + } + size="sm" + onClick={handleCopy} + compacted + /> + )} +
+
+ + +
+ ) +} + +export const Details = { Root, Header, Body, Panel, Item } diff --git a/src/renderer/src/components/GreyTokenSelect/GreyTokenSelectItem.tsx b/src/renderer/src/components/GreyTokenSelect/GreyTokenSelectItem.tsx index d7947d5e..1df72ce3 100644 --- a/src/renderer/src/components/GreyTokenSelect/GreyTokenSelectItem.tsx +++ b/src/renderer/src/components/GreyTokenSelect/GreyTokenSelectItem.tsx @@ -1,5 +1,6 @@ import { Fragment, useEffect, useState } from 'react' import defaultTokenLogo from '@renderer/assets/images/default-token-logo.png' +import { NumberHelper } from '@renderer/helpers/NumberHelper' import { TGreyTokenSelectToken } from '.' @@ -30,6 +31,10 @@ export const GreyTokenSelectItem = ({ token }: TProps) => { {token.symbol} {token.network && {` | ${token.network}`}} + + {token.amount && ( + {NumberHelper.formatString(token.amount, 6)} + )} ) } diff --git a/src/renderer/src/components/GreyTokenSelect/index.tsx b/src/renderer/src/components/GreyTokenSelect/index.tsx index 275ca8e0..79a75895 100644 --- a/src/renderer/src/components/GreyTokenSelect/index.tsx +++ b/src/renderer/src/components/GreyTokenSelect/index.tsx @@ -61,7 +61,9 @@ export const GreyTokenSelect = ({ if (balance) { filtered = filtered.map(token => { const tokenHash = UtilsHelper.normalizeHash(token.hash!) - const tokenBalance = balance.tokensBalances.find(tokenBalance => tokenBalance.token.hash === tokenHash) + const tokenBalance = balance.tokensBalances.find( + tokenBalance => UtilsHelper.normalizeHash(tokenBalance.token.hash) === tokenHash + ) return { ...token, @@ -144,7 +146,7 @@ export const GreyTokenSelect = ({ position: 'relative', }} > - {rowVirtualizer.getVirtualItems().map(virtualItem => { + {rowVirtualizer.getVirtualItems().map((virtualItem, _, array) => { const row = filteredTokensByText[virtualItem.index] return ( @@ -162,7 +164,7 @@ export const GreyTokenSelect = ({ - + {virtualItem.index + 1 !== array.length && } ) })} diff --git a/src/renderer/src/components/Modal/SideModal.tsx b/src/renderer/src/components/Modal/SideModal.tsx index 40aca88a..0408266f 100644 --- a/src/renderer/src/components/Modal/SideModal.tsx +++ b/src/renderer/src/components/Modal/SideModal.tsx @@ -10,8 +10,9 @@ import { ModalContainer } from './ModalContainer' const widthBySizes: Partial> = { md: '25.875rem', sm: '20.625rem', - xl: '62.5rem', - lg: '45rem', + lg: '32rem', + xl: '45rem', + '1xl': '62.5rem', } export const SideModal = () => { diff --git a/src/renderer/src/components/Stepper.tsx b/src/renderer/src/components/Stepper.tsx index 802ca47f..5f48d89e 100644 --- a/src/renderer/src/components/Stepper.tsx +++ b/src/renderer/src/components/Stepper.tsx @@ -1,12 +1,23 @@ import { Fragment } from 'react' import { StyleHelper } from '@renderer/helpers/StyleHelper' +export type TStepperCurrentState = 'success' | 'error' + type TProps = { steps: string[] currentStep?: number + currentState?: TStepperCurrentState + theme?: 'neon' | 'default' } & React.ComponentProps<'div'> -export const Stepper = ({ steps, currentStep = 1, className, ...props }: TProps) => { +export const Stepper = ({ + steps, + currentStep = 1, + className, + theme = 'default', + currentState = 'success', + ...props +}: TProps) => { return (
{steps.map((step, index) => { @@ -19,9 +30,12 @@ export const Stepper = ({ steps, currentStep = 1, className, ...props }: TProps) className={StyleHelper.mergeStyles( 'w-6 h-6 rounded-full flex items-center justify-center text-sm font-bold transition-colors', { - 'bg-blue text-asphalt': fixedIndex < currentStep, - 'bg-white text-asphalt': fixedIndex === currentStep, - 'bg-gray-900 text-gray-300': fixedIndex > currentStep, + 'bg-blue text-asphalt': fixedIndex < currentStep && theme === 'default', + 'bg-gray-900 text-gray-300': fixedIndex > currentStep && theme === 'default', + 'bg-neon text-asphalt': fixedIndex < currentStep && theme === 'neon', + 'bg-gray-300 text-asphalt': fixedIndex > currentStep && theme === 'neon', + 'bg-white text-asphalt': fixedIndex === currentStep && currentState === 'success', + 'bg-pink text-asphalt': fixedIndex === currentStep && currentState === 'error', } )} > @@ -32,8 +46,10 @@ export const Stepper = ({ steps, currentStep = 1, className, ...props }: TProps) className={StyleHelper.mergeStyles( 'text-center w-20 top-8 left-1/2 -translate-x-1/2 text-xs transition-colors absolute', { - 'text-blue': fixedIndex < currentStep, - 'text-white': fixedIndex === currentStep, + 'text-blue': fixedIndex < currentStep && theme === 'default', + 'text-neon': fixedIndex < currentStep && theme === 'neon', + 'text-white': fixedIndex === currentStep && currentState === 'success', + 'text-pink': fixedIndex === currentStep && currentState === 'error', 'text-gray-300': fixedIndex > currentStep, } )} @@ -45,8 +61,10 @@ export const Stepper = ({ steps, currentStep = 1, className, ...props }: TProps) {fixedIndex < steps.length && (
= currentStep, + 'border-blue': fixedIndex < currentStep && theme === 'default', + 'border-gray-900': fixedIndex >= currentStep && theme === 'default', + 'border-neon': fixedIndex < currentStep && theme === 'neon', + 'border-gray-300': fixedIndex >= currentStep && theme === 'neon', })} /> )} diff --git a/src/renderer/src/constants/swap.ts b/src/renderer/src/constants/swap.ts index 584a53b0..a017f227 100644 --- a/src/renderer/src/constants/swap.ts +++ b/src/renderer/src/constants/swap.ts @@ -16,3 +16,5 @@ export const SWAP_NETWORK_BY_BLOCKCHAIN_AND_NETWORK_ID: { '47763': [''], }, } + +export const SWAP_DISCORD_LINK = 'https://discord.gg/zW26BZC5ku' diff --git a/src/renderer/src/locales/en/modals.json b/src/renderer/src/locales/en/modals.json index d8afdcf4..ad2ecd86 100644 --- a/src/renderer/src/locales/en/modals.json +++ b/src/renderer/src/locales/en/modals.json @@ -525,5 +525,32 @@ "notConnectedMessage": "No hardware wallet found! Please check your connection and search again", "connectedMessage": "Detected your hardware wallet! Opening your wallet...", "searchAgainButtonLabel": "Search again" + }, + "swapDetails": { + "title": "Your swap", + "swapErrorMessage": " Oops! We’ve encountered an error - swap failed", + "transferErrorMessage": " Oops! We’ve encountered an error - transfer failed", + "subtitle": "Your swap details", + "refundedSubtitle": "You’ve been refunded", + "detailsHeaderLabel": "Details", + "detailsHeaderDescription": " We aim to complete swaps in under 7 minutes", + "statusPanelLabel": "Status", + "statusPanelSteps": [ + "Confirm", + "Exchange", + "Complete" + ], + "routingPanelLabel": "Routing", + "routingPanelTransactionFromLabel": "Transaction from", + "routingPanelTransactionToLabel": "Transaction to", + "routingPanelTransactionToLabelPending": "Pending...", + "routingPanelTransactionFeeLabel": "Transaction fee", + "sentPanelLabel": "You sent", + "sentPanelTokenLabel": "Token", + "sentPanelAddressLabel": "Sending address", + "receivePanelLabel": "You’ll receive", + "receivePanelTokenLabel": "Token", + "receivePanelAddressLabel": "Receiving address", + "helpButtonLabel": "Need help? Chat with us" } -} +} \ No newline at end of file diff --git a/src/renderer/src/locales/en/pages.json b/src/renderer/src/locales/en/pages.json index 224bf9ab..113160fc 100644 --- a/src/renderer/src/locales/en/pages.json +++ b/src/renderer/src/locales/en/pages.json @@ -397,7 +397,6 @@ }, "form": { "title": "What tokens do you want to swap?", - "simpleSwapImgAlt": "Simple swap", "swapFromTitle": "Swap this...", "swapToTitle": "For this...", "balanceLabel": "Balance", diff --git a/src/renderer/src/routes/modals/SwapDetails/SwapDetailsModalTokenDetails.tsx b/src/renderer/src/routes/modals/SwapDetails/SwapDetailsModalTokenDetails.tsx new file mode 100644 index 00000000..495448bb --- /dev/null +++ b/src/renderer/src/routes/modals/SwapDetails/SwapDetailsModalTokenDetails.tsx @@ -0,0 +1,25 @@ +import { useTranslation } from 'react-i18next' +import { BlockchainIcon } from '@renderer/components/BlockchainIcon' +import { TBlockchainServiceKey } from '@shared/@types/blockchain' + +type TProps = { + blockchain?: TBlockchainServiceKey + symbol: string + amount: string +} + +export const SwapDetailsModalTokenDetails = ({ amount, blockchain, symbol }: TProps) => { + const { t: commonT } = useTranslation('common') + + return ( +
+ {blockchain && } + + {symbol} + {blockchain && | {commonT(`blockchain.${blockchain}`)}} + + + {amount} +
+ ) +} diff --git a/src/renderer/src/routes/modals/SwapDetails/index.tsx b/src/renderer/src/routes/modals/SwapDetails/index.tsx new file mode 100644 index 00000000..0e212962 --- /dev/null +++ b/src/renderer/src/routes/modals/SwapDetails/index.tsx @@ -0,0 +1,214 @@ +import { useEffect, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { MdLaunch, MdRefresh } from 'react-icons/md' +import { TbCircleX, TbDiscountCheck, TbReceipt, TbReplace } from 'react-icons/tb' +import { SimpleSwapServiceHelper } from '@cityofzion/bs-swap' +import { BlockchainIcon } from '@renderer/components/BlockchainIcon' +import { Button } from '@renderer/components/Button' +import { Details } from '@renderer/components/Details' +import { Separator } from '@renderer/components/Separator' +import { Stepper, TStepperCurrentState } from '@renderer/components/Stepper' +import { SWAP_DISCORD_LINK } from '@renderer/constants/swap' +import { StringHelper } from '@renderer/helpers/StringHelper' +import { useModalState } from '@renderer/hooks/useModalRouter' +import { useAppDispatch } from '@renderer/hooks/useRedux' +import { SideModalLayout } from '@renderer/layouts/SideModal' +import { bsAggregator } from '@renderer/libs/blockchainService' +import { authReducerActions } from '@renderer/store/reducers/AuthReducer' +import { TSwapRecord } from '@shared/@types/store' +import { match, P } from 'ts-pattern' + +import { SwapDetailsModalTokenDetails } from './SwapDetailsModalTokenDetails' + +type TState = { + swapRecord: TSwapRecord +} + +const swapServiceHelper = new SimpleSwapServiceHelper(import.meta.env.VITE_SIMPLE_SWAP_API_KEY ?? '') + +const stepsByStatus: Record = { + confirming: 2, + exchanging: 3, + finished: 4, + failed: 2, + refunded: 2, +} + +export const SwapDetailsModal = () => { + const modalState = useModalState() + const dispatch = useAppDispatch() + const { t } = useTranslation('modals', { keyPrefix: 'swapDetails' }) + + const [swapRecord, setSwapRecord] = useState(modalState.swapRecord) + + const timeoutRef = useRef() + + const service = bsAggregator.blockchainServicesByName[swapRecord.account.blockchain] + + const handleHelpClick = () => { + window.open(SWAP_DISCORD_LINK, '_blank') + } + + useEffect(() => { + const getStatus = async () => { + if (!swapRecord.swapId) return + + try { + const response = await swapServiceHelper.getStatus(swapRecord.swapId) + + const updatedSwapRecord: TSwapRecord = { ...swapRecord, swapStatus: 'failed', txTo: response.txTo } + setSwapRecord(updatedSwapRecord) + dispatch(authReducerActions.persistSwapRecord(updatedSwapRecord)) + + if (response && response.status === 'finished') { + clearTimeout(timeoutRef.current) + return + } + } catch { + // Empty block + } + + timeoutRef.current = setTimeout(getStatus, 2500) + } + + if (swapRecord.swapStatus !== 'finished' && swapRecord.swapId) timeoutRef.current = setTimeout(getStatus, 0) + + return () => { + clearTimeout(timeoutRef.current) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + return ( + } + contentClassName="flex flex-col items-center overflow-auto" + > +
+ {swapRecord.swapStatus === 'failed' || swapRecord.swapStatus === 'refunded' ? ( + + ) : ( + + )} +
+ +
+ {match({ swapStatus: swapRecord.swapStatus, txFrom: swapRecord.txFrom }) + .with({ swapStatus: 'refunded', txFrom: P.nullish }, () => ( +

{t('transferErrorMessage')}

+ )) + .with({ swapStatus: P.union('refunded', 'failed'), txFrom: P.string }, () => ( +

{t('swapErrorMessage')}

+ )) + .run()} + +

+ {match({ swapStatus: swapRecord.swapStatus }) + .with({ swapStatus: 'refunded' }, () => t('refundedSubtitle')) + .otherwise(() => t('subtitle'))} +

+
+ + + + + }> + {t('detailsHeaderDescription')} + + + + + () + .with({ swapStatus: P.union('failed', 'refunded') }, () => 'error') + .with({ txFrom: P.nullish }, () => 'error') + .otherwise(() => 'success')} + theme="neon" + /> + + + + {swapRecord.txFrom && ( + +
+ + {StringHelper.truncateStringMiddle(swapRecord.txFrom, 50)} +
+
+ )} + + {swapRecord.txFrom && ( + + {match({ txTo: swapRecord.txTo }) + .with({ txTo: P.string }, ({ txTo }) => ( +
+ {swapRecord.tokenTo.blockchain && } + {StringHelper.truncateStringMiddle(txTo, 50)} +
+ )) + .otherwise(() => ( +
+ {t('routingPanelTransactionToLabelPending')} + +
+ ))} +
+ )} + + {swapRecord.fee && ( + + + + )} +
+
+
+ + + + + + + + + {swapRecord.account.address} + + + + + + + + {swapRecord.addressTo} + + + + +
- -
- {t('explanation.useTitle')} - {t('form.simpleSwapImgAlt')} -
diff --git a/src/renderer/src/routes/pages/Swap/SwapSuccessModalContent.tsx b/src/renderer/src/routes/pages/Swap/SwapSuccessModalContent.tsx deleted file mode 100644 index 9bf631ca..00000000 --- a/src/renderer/src/routes/pages/Swap/SwapSuccessModalContent.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import { Fragment, ReactNode, useEffect, useRef, useState } from 'react' -import { useTranslation } from 'react-i18next' -import { TbReceipt } from 'react-icons/tb' -import { SwapService, SwapServiceStatusResponse } from '@cityofzion/blockchain-service' -import { Loader } from '@renderer/components/Loader' -import { Separator } from '@renderer/components/Separator' -import { bsAggregator } from '@renderer/libs/blockchainService' -import { TBlockchainServiceKey } from '@shared/@types/blockchain' -import { TSwapRecord } from '@shared/@types/store' - -type TProps = { - swapService: SwapService - swapRecord: TSwapRecord -} - -type TItemProps = { - label: string - value: ReactNode -} -const Item = ({ label, value }: TItemProps) => { - return ( -
- {label} - - {value} -
- ) -} - -export const SwapSuccessModalContent = ({ swapRecord, swapService }: TProps) => { - const { t } = useTranslation('pages', { keyPrefix: 'swap.form.sendSuccess' }) - const [statusResponse, setStatusResponse] = useState({ - status: swapRecord.swapStatus, - transactionHashes: swapRecord.transactionHashes, - }) - - const timeoutRef = useRef() - - useEffect(() => { - const getStatus = async () => { - let response: SwapServiceStatusResponse | undefined - try { - response = await swapService.getStatus() - setStatusResponse(response) - } catch { - // Empty block - } - - if (response && response.status === 'finished') { - clearTimeout(timeoutRef.current) - return - } - - timeoutRef.current = setTimeout(getStatus, 2500) - } - - timeoutRef.current = setTimeout(getStatus, 0) - - return () => { - clearTimeout(timeoutRef.current) - } - }, [swapService]) - - return ( -
-
-
-
- - {t('detailsTitle')} -
- - - -
- - {statusResponse.status} - {statusResponse.status !== 'finished' && statusResponse.status !== 'failed' && ( - - )} -
- } - /> - - - - - - - - - {swapRecord.amountFrom} - {swapRecord.tokenFrom.symbol} -
- } - /> - - - - - {swapRecord.amountTo} - {swapRecord.tokenTo.symbol} -
- } - /> - - - - {swapRecord.fee && ( - - {swapRecord.fee} - - {bsAggregator.blockchainServicesByName[swapRecord.account.blockchain].feeToken.symbol} - - - } - /> - )} - - - - {statusResponse.transactionHashes.map((hash, index, array) => { - const label = - index === 0 - ? t('transactionFromLabel') - : index + 1 === array.length - ? t('transactionToLabel') - : t('transactionAux') - - return ( - - - - {index + 1 !== array.length && } - - ) - })} -
- - - - ) -} diff --git a/src/renderer/src/store/reducers/AuthReducer.ts b/src/renderer/src/store/reducers/AuthReducer.ts index 1362e71f..1adafef3 100644 --- a/src/renderer/src/store/reducers/AuthReducer.ts +++ b/src/renderer/src/store/reducers/AuthReducer.ts @@ -231,8 +231,19 @@ const addPendingTransaction = createAsyncThunk< } }) -const addSwapRecord: CaseReducer> = (state, action) => { - state.data.swapRecords = [...state.data.swapRecords, action.payload] +const persistSwapRecord: CaseReducer> = (state, action) => { + const swapRecord = action.payload + + const swapRecordIndex = state.data.swapRecords.findIndex( + it => it.swapId === swapRecord.swapId && it.swapProvider === swapRecord.swapProvider + ) + + if (swapRecordIndex === -1) { + state.data.swapRecords = [...state.data.swapRecords, swapRecord] + return + } + + state.data.swapRecords[swapRecordIndex] = swapRecord } const AuthReducer = createSlice({ @@ -245,7 +256,7 @@ const AuthReducer = createSlice({ deleteAccount, setCurrentLoginSession, resetTemporaryApplicationData, - addSwapRecord, + persistSwapRecord, }, extraReducers: builder => { builder.addCase(PURGE, () => initialState) diff --git a/src/shared/@types/i18next-resources.d.ts b/src/shared/@types/i18next-resources.d.ts index c4e98cab..19a9c6d3 100644 --- a/src/shared/@types/i18next-resources.d.ts +++ b/src/shared/@types/i18next-resources.d.ts @@ -777,6 +777,29 @@ interface Resources { connectedMessage: 'Detected your hardware wallet! Opening your wallet...' searchAgainButtonLabel: 'Search again' } + swapDetails: { + title: 'Your swap' + swapErrorMessage: ' Oops! We’ve encountered an error - swap failed' + transferErrorMessage: ' Oops! We’ve encountered an error - transfer failed' + subtitle: 'Your swap details' + refundedSubtitle: 'You’ve been refunded' + detailsHeaderLabel: 'Details' + detailsHeaderDescription: ' We aim to complete swaps in under 7 minutes' + statusPanelLabel: 'Status' + statusPanelSteps: ['Confirm', 'Exchange', 'Complete'] + routingPanelLabel: 'Routing' + routingPanelTransactionFromLabel: 'Transaction from' + routingPanelTransactionToLabel: 'Transaction to' + routingPanelTransactionToLabelPending: 'Pending...' + routingPanelTransactionFeeLabel: 'Transaction fee' + sentPanelLabel: 'You sent' + sentPanelTokenLabel: 'Token' + sentPanelAddressLabel: 'Sending address' + receivePanelLabel: 'You’ll receive' + receivePanelTokenLabel: 'Token' + receivePanelAddressLabel: 'Receiving address' + helpButtonLabel: 'Need help? Chat with us' + } } pages: { welcome: { @@ -1167,7 +1190,6 @@ interface Resources { } form: { title: 'What tokens do you want to swap?' - simpleSwapImgAlt: 'Simple swap' swapFromTitle: 'Swap this...' swapToTitle: 'For this...' balanceLabel: 'Balance' diff --git a/src/shared/@types/modal.ts b/src/shared/@types/modal.ts index 09ec97f3..8c4abf78 100644 --- a/src/shared/@types/modal.ts +++ b/src/shared/@types/modal.ts @@ -1,5 +1,5 @@ export type TRouteType = 'side' | 'center' -export type TRouterSize = 'sm' | 'md' | 'lg' | 'xl' +export type TRouterSize = 'sm' | 'md' | 'lg' | 'xl' | '1xl' export type TRoute = { element: JSX.Element diff --git a/src/shared/@types/store.ts b/src/shared/@types/store.ts index 4f6e676c..58e77391 100644 --- a/src/shared/@types/store.ts +++ b/src/shared/@types/store.ts @@ -91,15 +91,15 @@ export interface IContactState { export type TSwapRecord = { account: IAccountState - transactionHashes: string[] + txFrom?: string + txTo?: string swapProvider: 'simpleswap' - swapId: string + swapId?: string swapStatus: SwapServiceStatusResponse['status'] - tokenFrom: SwapServiceToken - tokenTo: SwapServiceToken + tokenFrom: SwapServiceToken + tokenTo: SwapServiceToken amountFrom: string amountTo: string addressTo: string - numberOfTransactions: number fee?: string }