diff --git a/apps/web-swap-widget/package.json b/apps/web-swap-widget/package.json index 838189601..f12fafe59 100644 --- a/apps/web-swap-widget/package.json +++ b/apps/web-swap-widget/package.json @@ -1,6 +1,6 @@ { "name": "@tonkeeper/web-swap-widget", - "version": "3.0.0", + "version": "3.0.5", "author": "Ton APPS UK Limited ", "description": "Web swap widget for Tonkeeper", "dependencies": { diff --git a/apps/web-swap-widget/src/App.tsx b/apps/web-swap-widget/src/App.tsx index 1a32849cc..c338dae1a 100644 --- a/apps/web-swap-widget/src/App.tsx +++ b/apps/web-swap-widget/src/App.tsx @@ -23,7 +23,7 @@ import { useAccountsStateQuery, useActiveAccountQuery } from '@tonkeeper/uikit/d import { GlobalStyle } from '@tonkeeper/uikit/dist/styles/globalStyle'; import { FC, PropsWithChildren, Suspense, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { BrowserAppSdk } from './libs/appSdk'; +import { WidgetAppSdk } from './libs/appSdk'; import { useAnalytics, useAppHeight, useApplyQueryParams, useAppWidth } from './libs/hooks'; import { useGlobalPreferencesQuery } from '@tonkeeper/uikit/dist/state/global-preferences'; import { useGlobalSetup } from '@tonkeeper/uikit/dist/state/globalSetup'; @@ -36,6 +36,7 @@ import { useAccountsStorage } from '@tonkeeper/uikit/dist/hooks/useStorage'; import { AccountTonWatchOnly } from '@tonkeeper/core/dist/entries/account'; import { getTonkeeperInjectionContext } from './libs/tonkeeper-injection-context'; import { Address } from '@ton/core'; +import { defaultLanguage } from './i18n'; const queryClient = new QueryClient({ defaultOptions: { @@ -46,16 +47,44 @@ const queryClient = new QueryClient({ } }); -const sdk = new BrowserAppSdk(); +const sdk = new WidgetAppSdk(); const TARGET_ENV = 'swap-widget-web'; +const queryParams = new URLSearchParams(new URL(window.location.href).search); + +const queryParamLangKey = (supportedLanguages: string[]) => { + let key = queryParams.get('lang'); + + if (!key) { + return undefined; + } + + if (supportedLanguages.includes(key)) { + return key; + } + + if (key.includes('_')) { + key = key.split('_')[0].toLowerCase(); + + return supportedLanguages.includes(key) ? key : undefined; + } +}; + export const App: FC = () => { + const languages = (import.meta.env.VITE_APP_LOCALES ?? defaultLanguage).split(','); + const queryParamsLang = queryParamLangKey(languages); + const { t: tSimple, i18n } = useTranslation(); + useEffect(() => { + if (queryParamsLang && queryParamsLang !== defaultLanguage) { + i18n.reloadResources(queryParamsLang).then(() => i18n.changeLanguage(queryParamsLang)); + } + }, []); + const t = useTWithReplaces(tSimple); const translation = useMemo(() => { - const languages = (import.meta.env.VITE_APP_LOCALES ?? 'en').split(','); const client: I18nContext = { t, i18n: { @@ -225,7 +254,7 @@ const Loader: FC = () => { const Wrapper = styled.div` box-sizing: border-box; - padding: 0 16px 34px; + padding: 0 16px 46px; height: 100%; `; diff --git a/apps/web-swap-widget/src/components/SwapWidgetFooter.tsx b/apps/web-swap-widget/src/components/SwapWidgetFooter.tsx index 0c91e5429..773326253 100644 --- a/apps/web-swap-widget/src/components/SwapWidgetFooter.tsx +++ b/apps/web-swap-widget/src/components/SwapWidgetFooter.tsx @@ -13,6 +13,10 @@ const Row = styled.div` ${Body3Class} `; +const RowWrap = styled(Row)` + flex-wrap: wrap; +`; + const Link = styled.a` display: flex; align-items: center; @@ -42,7 +46,7 @@ export const SwapWidgetFooter = () => {  {t('and')} TON - + {t('legal_privacy')} @@ -50,7 +54,7 @@ export const SwapWidgetFooter = () => { {t('legal_terms')} - + ); }; diff --git a/apps/web-swap-widget/src/components/SwapWidgetPage.tsx b/apps/web-swap-widget/src/components/SwapWidgetPage.tsx index 417366fc5..4bf158b07 100644 --- a/apps/web-swap-widget/src/components/SwapWidgetPage.tsx +++ b/apps/web-swap-widget/src/components/SwapWidgetPage.tsx @@ -1,6 +1,5 @@ import { styled } from 'styled-components'; import { useEncodeSwapToTonConnectParams } from '@tonkeeper/uikit/dist/state/swap/useEncodeSwap'; -import { useState } from 'react'; import { useSelectedSwap, useSwapFromAmount, @@ -18,6 +17,8 @@ import { NonNullableFields } from '@tonkeeper/core/dist/utils/types'; import { SwapWidgetHeader } from './SwapWidgetHeader'; import { getTonkeeperInjectionContext } from '../libs/tonkeeper-injection-context'; import { SwapWidgetFooter } from './SwapWidgetFooter'; +import { SwapWidgetTxSentNotification } from './SwapWidgetTxSent'; +import { useDisclosure } from '@tonkeeper/uikit/dist/hooks/useDisclosure'; const MainFormWrapper = styled.div` height: 100%; @@ -54,30 +55,56 @@ const ChangeIconStyled = styled(IconButton)` export const SwapWidgetPage = () => { const { isLoading, mutateAsync: encode } = useEncodeSwapToTonConnectParams({ - ignoreBattery: true + forceCalculateBattery: true }); - const [hasBeenSent, setHasBeenSent] = useState(false); const [selectedSwap] = useSelectedSwap(); const [fromAsset, setFromAsset] = useSwapFromAsset(); const [toAsset, setToAsset] = useSwapToAsset(); const [_, setFromAmount] = useSwapFromAmount(); + const { isOpen, onClose, onOpen } = useDisclosure(); const onConfirm = async () => { const params = await encode(selectedSwap! as NonNullableFields); const ctx = getTonkeeperInjectionContext()!; - ctx.sendTransaction({ - source: ctx.address, - // legacy tonkeeper api, timestamp in ms - valid_until: params.valid_until * 1000, - messages: params.messages.map(m => ({ - address: m.address, - amount: m.amount.toString(), - payload: m.payload - })) - }).finally(() => setHasBeenSent(false)); - setHasBeenSent(true); + try { + const result = await ctx.sendTransaction({ + source: ctx.address, + /** + * legacy tonkeeper api, timestamp in ms + */ + valid_until: params.valid_until * 1000, + messages: params.messages.map(m => ({ + address: m.address, + amount: m.amount.toString(), + payload: m.payload + })), + messagesVariants: params.messagesVariants + ? Object.fromEntries( + Object.entries(params.messagesVariants).map(([k, v]) => [ + k, + v.map(m => ({ + address: m.address, + amount: m.amount.toString(), + payload: m.payload + })) + ]) + ) + : undefined + }); + + /** + old tonkeeper android versions return empty result instead of throwing + */ + if (!result) { + throw new Error('Operation failed'); + } + + onOpen(); + } catch (e) { + console.error(e); + } }; const onChangeFields = () => { @@ -97,10 +124,11 @@ export const SwapWidgetPage = () => { - + + ); }; diff --git a/apps/web-swap-widget/src/components/SwapWidgetTxSent.tsx b/apps/web-swap-widget/src/components/SwapWidgetTxSent.tsx new file mode 100644 index 000000000..c788c5a11 --- /dev/null +++ b/apps/web-swap-widget/src/components/SwapWidgetTxSent.tsx @@ -0,0 +1,78 @@ +import { Notification } from '@tonkeeper/uikit/dist/components/Notification'; +import { FC } from 'react'; +import styled, { useTheme } from 'styled-components'; +import { Body2, Button, Label2 } from '@tonkeeper/uikit'; +import { useTranslation } from 'react-i18next'; + +const NotificationStyled = styled(Notification)` + .dialog-header { + margin-bottom: 0; + } +`; + +export const SwapWidgetTxSentNotification: FC<{ isOpen: boolean; onClose: () => void }> = ({ + isOpen, + onClose +}) => { + return ( + + {() => } + + ); +}; + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +`; + +const CheckmarkCircleIcon = () => { + const theme = useTheme(); + + return ( + + + + + ); +}; + +const Body2Secondary = styled(Body2)` + color: ${p => p.theme.textSecondary}; + margin-bottom: 24px; + margin-top: 4px; +`; + +const SwapWidgetTxSentNotificationContent: FC<{ onClose: () => void }> = ({ onClose }) => { + const { t } = useTranslation(); + return ( + + + {t('swap_transaction_sent_title')} + {t('swap_transaction_sent_description')} + + + ); +}; diff --git a/apps/web-swap-widget/src/i18n.ts b/apps/web-swap-widget/src/i18n.ts index 58fede7d7..ca00d1a78 100644 --- a/apps/web-swap-widget/src/i18n.ts +++ b/apps/web-swap-widget/src/i18n.ts @@ -4,20 +4,21 @@ import LanguageDetector from 'i18next-browser-languagedetector'; import Backend from 'i18next-http-backend'; import { initReactI18next } from 'react-i18next'; -i18n - .use(Backend) - .use(LanguageDetector) - .use(initReactI18next) // passes i18n down to react-i18next - .init({ - resources, - debug: false, - lng: 'en', // language to use, more information here: https://www.i18next.com/overview/configuration-options#languages-namespaces-resources - // you can use the i18n.changeLanguage function to change the language manually: https://www.i18next.com/overview/api#changelanguage - // if you're using a language detector, do not define the lng option - fallbackLng: 'en', - interpolation: { - escapeValue: false, // react already safes from xss - }, - }); +export const defaultLanguage = 'en'; + +i18n.use(Backend) + .use(LanguageDetector) + .use(initReactI18next) // passes i18n down to react-i18next + .init({ + resources, + debug: false, + lng: defaultLanguage, // language to use, more information here: https://www.i18next.com/overview/configuration-options#languages-namespaces-resources + // you can use the i18n.changeLanguage function to change the language manually: https://www.i18next.com/overview/api#changelanguage + // if you're using a language detector, do not define the lng option + fallbackLng: defaultLanguage, + interpolation: { + escapeValue: false // react already safes from xss + } + }); export default i18n; diff --git a/apps/web-swap-widget/src/index.tsx b/apps/web-swap-widget/src/index.tsx index 41b41115a..6d635141a 100644 --- a/apps/web-swap-widget/src/index.tsx +++ b/apps/web-swap-widget/src/index.tsx @@ -1,7 +1,11 @@ import ReactDOM from 'react-dom/client'; import { App } from './App'; import './i18n'; +import { WidgetAppSdk } from './libs/appSdk'; -const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); +const rootElement = document.getElementById('root') as HTMLElement; +rootElement.setAttribute('data-app-version', WidgetAppSdk.version); + +const root = ReactDOM.createRoot(rootElement); root.render(); diff --git a/apps/web-swap-widget/src/libs/appSdk.ts b/apps/web-swap-widget/src/libs/appSdk.ts index 5ddd73019..70c27287a 100644 --- a/apps/web-swap-widget/src/libs/appSdk.ts +++ b/apps/web-swap-widget/src/libs/appSdk.ts @@ -2,7 +2,7 @@ import { BaseApp } from '@tonkeeper/core/dist/AppSdk'; import copyToClipboard from 'copy-to-clipboard'; import packageJson from '../../package.json'; import { disableScroll, enableScroll, getScrollbarWidth } from './scroll'; -import { BrowserStorage } from './storage'; +import { SwapWidgetStorage } from './storage'; function iOS() { return ( @@ -14,9 +14,11 @@ function iOS() { ); } -export class BrowserAppSdk extends BaseApp { +export class WidgetAppSdk extends BaseApp { + static version = packageJson.version ?? 'Unknown'; + constructor() { - super(new BrowserStorage()); + super(new SwapWidgetStorage()); } copyToClipboard = (value: string, notification?: string) => { @@ -42,7 +44,7 @@ export class BrowserAppSdk extends BaseApp { isStandalone = () => iOS() && ((window.navigator as unknown as { standalone: boolean }).standalone as boolean); - version = packageJson.version ?? 'Unknown'; + version = WidgetAppSdk.version; targetEnv = 'web' as const; } diff --git a/apps/web-swap-widget/src/libs/storage.ts b/apps/web-swap-widget/src/libs/storage.ts index 1ee17aba9..4297080a5 100644 --- a/apps/web-swap-widget/src/libs/storage.ts +++ b/apps/web-swap-widget/src/libs/storage.ts @@ -1,6 +1,46 @@ +// eslint-disable-next-line max-classes-per-file import { IStorage } from '@tonkeeper/core/dist/Storage'; +import { AppKey } from '@tonkeeper/core/dist/Keys'; -export class BrowserStorage implements IStorage { +export class SwapWidgetStorage implements IStorage { + private permanentStorage = new BrowserPermanentStorage(); + + private temporaryStorage = new BrowserTemporaryStorage(); + + private storageByKey = (key: string) => { + if (key === AppKey.SWAP_OPTIONS) { + return this.permanentStorage; + } + + return this.temporaryStorage; + }; + + get = async (key: string) => { + return this.storageByKey(key).get(key); + }; + + set = async (key: string, payload: R) => { + return this.storageByKey(key).set(key, payload); + }; + + setBatch = async >(values: V) => { + Object.entries(values).forEach(([key, payload]) => { + this.set(key, payload); + }); + return values; + }; + + delete = async (key: string) => { + return this.storageByKey(key).delete(key); + }; + + clear = async () => { + await this.temporaryStorage.clear(); + await this.permanentStorage.clear(); + }; +} + +class BrowserPermanentStorage implements IStorage { prefix = 'tonkeeper-swap-widget'; get = async (key: string) => { @@ -34,3 +74,38 @@ export class BrowserStorage implements IStorage { localStorage.clear(); }; } + +class BrowserTemporaryStorage implements IStorage { + private storage = new Map(); + + get = async (key: string) => { + const value = this.storage.get(key); + if (value == null) { + return null; + } + + return value as R; + }; + + set = async (key: string, payload: R) => { + this.storage.set(key, payload); + return payload; + }; + + setBatch = async >(values: V) => { + Object.entries(values).forEach(([key, payload]) => { + this.storage.set(key, payload); + }); + return values; + }; + + delete = async (key: string) => { + const payload = this.storage.get(key); + this.storage.delete(key); + return payload as R | null; + }; + + clear = async () => { + this.storage.clear(); + }; +} diff --git a/apps/web-swap-widget/src/libs/tonkeeper-injection-context.ts b/apps/web-swap-widget/src/libs/tonkeeper-injection-context.ts index ac3ed10a6..c6c7d4eb8 100644 --- a/apps/web-swap-widget/src/libs/tonkeeper-injection-context.ts +++ b/apps/web-swap-widget/src/libs/tonkeeper-injection-context.ts @@ -1,4 +1,8 @@ import { getWindow } from '@tonkeeper/core/dist/service/telegramOauth'; +import { + TON_CONNECT_MSG_VARIANTS_ID, + TonConnectTransactionPayloadMessage +} from '@tonkeeper/core/dist/entries/tonConnect'; type UserFriendlyAddress = string; type TimestampMS = number; @@ -14,6 +18,9 @@ export type TonkeeperInjection = { amount: string; payload?: string; }[]; + messagesVariants?: Partial< + Record + >; }) => Promise; }; diff --git a/packages/core/src/tonkeeperApi/tonendpoint.ts b/packages/core/src/tonkeeperApi/tonendpoint.ts index 9b8517201..be8590db6 100644 --- a/packages/core/src/tonkeeperApi/tonendpoint.ts +++ b/packages/core/src/tonkeeperApi/tonendpoint.ts @@ -5,7 +5,7 @@ import { DAppTrack } from '../service/urlService'; import { FetchAPI } from '../tonApiV2'; export interface BootParams { - platform: 'ios' | 'android' | 'web' | 'desktop' | 'tablet'; + platform: 'ios' | 'android' | 'web' | 'desktop' | 'tablet' | 'swap-widget-web'; lang: 'en' | 'ru' | string; build: string; // "2.8.0" network: Network; diff --git a/packages/locales/src/tonkeeper-web/en.json b/packages/locales/src/tonkeeper-web/en.json index 8b12a35ae..f8b966fa9 100644 --- a/packages/locales/src/tonkeeper-web/en.json +++ b/packages/locales/src/tonkeeper-web/en.json @@ -351,6 +351,9 @@ "swap_tokens": "Tokens", "swap_tokens_not_found": "Tokens not found", "swap_trade_is_not_available": "Trade is not available", + "swap_transaction_sent_close_button": "OK", + "swap_transaction_sent_description": "Your transaction has been sent to the network and will be processed within a few minutes.", + "swap_transaction_sent_title": "Transaction Sent", "swap_tx_info": "Transaction Information", "swap_unknown_price_impact": "Unknown price impact", "swap_unknown_token_description": "This token isn't included in the active token list. Make sure you are aware of the risks associated with imported tokens.", diff --git a/packages/locales/src/tonkeeper-web/ru-RU.json b/packages/locales/src/tonkeeper-web/ru-RU.json index f4f21613a..c98481eff 100644 --- a/packages/locales/src/tonkeeper-web/ru-RU.json +++ b/packages/locales/src/tonkeeper-web/ru-RU.json @@ -340,6 +340,9 @@ "swap_tokens": "Токены", "swap_tokens_not_found": "Токены не найдены", "swap_trade_is_not_available": "Обмен недоступен", + "swap_transaction_sent_close_button": "OK", + "swap_transaction_sent_description": "Ваша транзакция отправлена в сеть и будет обработана в течение нескольких минут.", + "swap_transaction_sent_title": "Транзакция отправлена", "swap_tx_info": "Подробнее", "swap_unknown_price_impact": "Влияние на цену неизвестно", "swap_unknown_token_description": "Этот токен не включен в список известных токенов. Убедитесь, что вы осознаете риски, связанные с импортом неизвестных токенов.", diff --git a/packages/uikit/src/components/Notification.tsx b/packages/uikit/src/components/Notification.tsx index b7dae793c..e57e62a14 100644 --- a/packages/uikit/src/components/Notification.tsx +++ b/packages/uikit/src/components/Notification.tsx @@ -1,8 +1,10 @@ import React, { + Children, createContext, FC, forwardRef, PropsWithChildren, + ReactElement, ReactNode, useCallback, useContext, @@ -25,6 +27,7 @@ import ReactPortal from './ReactPortal'; import { H2, H3, Label2 } from './Text'; import { IconButtonTransparentBackground } from './fields/IconButton'; import { AnimateHeightChange } from './shared/AnimateHeightChange'; +import { useAppPlatform } from '../hooks/appContext'; const NotificationContainer = styled(Container)<{ scrollbarWidth: number }>` background: transparent; @@ -168,7 +171,7 @@ const Splash = styled.div` } `; -const Content = styled.div<{ standalone: boolean }>` +const Content = styled.div<{ standalone: boolean; $isInWidget: boolean }>` width: 100%; background-color: ${props => props.theme.backgroundPage}; border-top-right-radius: ${props => props.theme.cornerMedium}; @@ -183,6 +186,12 @@ const Content = styled.div<{ standalone: boolean }>` padding-bottom: 2rem; `} + ${props => + props.$isInWidget && + css` + padding-bottom: 46px; + `} + ${p => p.theme.displayType === 'full-width' && css` @@ -397,76 +406,13 @@ export const NotificationBackButton: FC<{ onBack: () => void }> = ({ onBack }) = ); }; -export const NotificationScrollContext = React.createContext(null); - const NotificationOverlay: FC void; entered: boolean }>> = - React.memo(({ children, handleClose, entered }) => { + React.memo(({ children, entered }) => { const scrollRef = useRef(null); - const isFullWidthMode = useIsFullWidthMode(); - - useEffect(() => { - if (isFullWidthMode) { - return; - } - const element = scrollRef.current; - - if (!element) return; - - let lastY = 0; - let startY = 0; - let maxScrollTop = 0; - let startScroll = 0; - - const handlerTouchStart = function (event: TouchEvent) { - lastY = startY = event.touches[0].clientY; - const style = window.getComputedStyle(element); - const outerHeight = ['height', 'padding-top', 'padding-bottom'] - .map(key => parseInt(style.getPropertyValue(key))) - .reduce((prev, cur) => prev + cur); - - maxScrollTop = element.scrollHeight - outerHeight; - startScroll = element.scrollTop; - }; - - const handlerTouchMoveElement = function (event: TouchEvent) { - const top = event.touches[0].clientY; - - const direction = lastY - top < 0 ? 'down' : 'up'; - if (event.cancelable) { - if (startScroll >= maxScrollTop && direction === 'up') { - event.preventDefault(); - } - } - lastY = top; - }; - - const handlerTouchMoveWindow = function (event: TouchEvent) { - if (startY === 0) return; - const top = event.touches[0].clientY; - if (startScroll <= 0 && startY - top < -180) { - window.addEventListener('touchend', handleClose); - window.addEventListener('touchcancel', handleClose); - } - }; - - element.addEventListener('touchstart', handlerTouchStart); - element.addEventListener('touchmove', handlerTouchMoveElement); - window.addEventListener('touchmove', handlerTouchMoveWindow); - - return () => { - element.removeEventListener('touchstart', handlerTouchStart); - element.removeEventListener('touchmove', handlerTouchMoveElement); - window.removeEventListener('touchmove', handlerTouchMoveWindow); - window.removeEventListener('touchend', handleClose); - window.removeEventListener('touchcancel', handleClose); - }; - }, [scrollRef, handleClose, isFullWidthMode]); return ( - - {children} - + {children} ); }); @@ -583,6 +529,8 @@ export const Notification: FC<{ const containerRef = useClickOutside(onClickOutside, nodeRef.current); const [onBack, setOnBack] = useState<(() => void) | undefined>(); + const isInWidget = useAppPlatform() === 'swap-widget-web'; + return ( const isFullWidth = useIsFullWidthMode(); if (!isFullWidth) { - return <>{children}; + return ( + <> + {Children.map(children, child => + React.isValidElement(child) + ? React.cloneElement<{ className?: string }>( + child as ReactElement<{ className?: string }>, + { + className: `${child.props.className || ''} ${className}`.trim() + } + ) + : child + )} + + ); } return ( diff --git a/packages/uikit/src/components/shared/Accordion.tsx b/packages/uikit/src/components/shared/Accordion.tsx index d9d1d75ec..f452f99ff 100644 --- a/packages/uikit/src/components/shared/Accordion.tsx +++ b/packages/uikit/src/components/shared/Accordion.tsx @@ -44,9 +44,9 @@ export const Accordion: FC { clearTimeout(timeoutRef.current); if (isOpened) { - setIsAnimationCompleted(false); - } else { timeoutRef.current = setTimeout(() => setIsAnimationCompleted(true), 200); + } else { + setIsAnimationCompleted(false); } }, [isOpened]); diff --git a/packages/uikit/src/components/shared/ExternalLink.tsx b/packages/uikit/src/components/shared/ExternalLink.tsx new file mode 100644 index 000000000..463efa86f --- /dev/null +++ b/packages/uikit/src/components/shared/ExternalLink.tsx @@ -0,0 +1,53 @@ +import { FC, MouseEventHandler, PropsWithChildren } from 'react'; +import { useAppPlatform } from '../../hooks/appContext'; +import { useAppSdk } from '../../hooks/appSdk'; +import styled from 'styled-components'; + +const AStyled = styled.a` + text-decoration: unset; + cursor: pointer; +`; + +const ButtonStyled = styled.button` + border: none; + outline: none; + background: transparent; + cursor: pointer; +`; + +export const ExternalLink: FC< + PropsWithChildren<{ + className?: string; + href: string; + onClick?: MouseEventHandler; + }> +> = ({ className, href, onClick, children }) => { + const platform = useAppPlatform(); + const sdk = useAppSdk(); + + if (platform === 'web' || platform === 'swap-widget-web') { + return ( + onClick?.(e)} + > + {children} + + ); + } + + return ( + { + onClick?.(e); + sdk.openPage(href); + }} + className={className} + > + {children} + + ); +}; diff --git a/packages/uikit/src/components/swap/SwapToField.tsx b/packages/uikit/src/components/swap/SwapToField.tsx index 45be007c0..8e26b1595 100644 --- a/packages/uikit/src/components/swap/SwapToField.tsx +++ b/packages/uikit/src/components/swap/SwapToField.tsx @@ -16,6 +16,10 @@ const FiledContainerStyled = styled.div` border-radius: ${p => p.theme.displayType === 'full-width' ? p.theme.corner2xSmall : p.theme.cornerSmall}; padding: 6px 12px; + + &:empty { + display: none; + } `; const FiledHeader = styled.div` diff --git a/packages/uikit/src/components/swap/SwapTransactionInfo.tsx b/packages/uikit/src/components/swap/SwapTransactionInfo.tsx index a8fb256b2..0ce2bf2bd 100644 --- a/packages/uikit/src/components/swap/SwapTransactionInfo.tsx +++ b/packages/uikit/src/components/swap/SwapTransactionInfo.tsx @@ -1,5 +1,5 @@ import { css, styled } from 'styled-components'; -import { Body2Class, Body3 } from '../Text'; +import { Body2Class, Body3, Body3Class } from "../Text"; import { IconButton } from '../fields/IconButton'; import { useState } from 'react'; import { ChevronDownIcon, InfoCircleIcon } from '../Icon'; @@ -33,32 +33,6 @@ const TxInfoHeader = styled.div` } `; -const AccordionContent = styled.div` - transform: translateY(-100%); - visibility: hidden; - transition: transform 0.2s ease-in-out, visibility 0.2s ease-in-out; -`; - -const AccordionAnimation = styled.div<{ isOpened: boolean; animationCompleted: boolean }>` - display: grid; - grid-template-rows: ${p => (p.isOpened ? '1fr' : '0fr')}; - overflow: ${p => (p.animationCompleted && p.isOpened ? 'visible' : 'hidden')}; - transition: grid-template-rows 0.2s ease-in-out; - - ${AccordionContent} { - ${p => - p.isOpened && - css` - transform: translateY(0); - visibility: visible; - `} - } -`; - -const AccordionBody = styled.div` - min-height: 0; -`; - const AccordionButton = styled(IconButton)<{ isOpened: boolean }>` transform: ${p => (p.isOpened ? 'rotate(180deg)' : 'rotate(0deg)')}; transition: transform 0.2s ease-in-out; @@ -89,7 +63,7 @@ const Tooltip = styled.div<{ placement: 'top' | 'bottom' }>` background-color: ${p => p.theme.backgroundContentTint}; padding: 8px 12px; ${BorderSmallResponsive}; - ${Body2Class}; + ${Body3Class}; ${p => p.placement === 'top' diff --git a/packages/uikit/src/components/swap/tokens-list/SwapTokensList.tsx b/packages/uikit/src/components/swap/tokens-list/SwapTokensList.tsx index b76b383c5..ce9c59aec 100644 --- a/packages/uikit/src/components/swap/tokens-list/SwapTokensList.tsx +++ b/packages/uikit/src/components/swap/tokens-list/SwapTokensList.tsx @@ -1,5 +1,5 @@ import { styled } from 'styled-components'; -import React, { FC, Fragment, useEffect, useRef, useState } from 'react'; +import React, { FC, Fragment, MouseEventHandler, useEffect, useRef, useState } from 'react'; import { Body2, Body3, Label2 } from '../../Text'; import { useAddUserCustomSwapAsset, @@ -7,13 +7,13 @@ import { WalletSwapAsset } from '../../../state/swap/useSwapAssets'; import { formatFiatCurrency } from '../../../hooks/balance'; -import { useAppContext } from '../../../hooks/appContext'; +import { useAppContext, useAppPlatform } from '../../../hooks/appContext'; import { isTon, TonAsset } from '@tonkeeper/core/dist/entries/crypto/asset/ton-asset'; import { LinkOutIcon, SpinnerIcon } from '../../Icon'; import { ConfirmImportNotification } from './ConfirmImportNotification'; -import { useAppSdk } from '../../../hooks/appSdk'; import { throttle } from '@tonkeeper/core/dist/utils/common'; import { useTranslation } from '../../../hooks/translation'; +import { ExternalLink } from '../../shared/ExternalLink'; const SwapTokensListWrapper = styled.div` overflow-y: auto; @@ -177,8 +177,7 @@ const TokenInfoLine = styled.div` } `; -const LinkOutIconWrapper = styled.div` - cursor: pointer; +const LinkOutIconWrapper = styled(ExternalLink)` &:hover { > svg { color: ${p => p.theme.iconSecondary}; @@ -204,22 +203,19 @@ const TokenListItem: FC<{ swapAsset: WalletSwapAsset; onClick: () => void }> = ( }) => { const isZeroBalance = swapAsset.assetAmount.relativeAmount.isZero(); const { fiat } = useAppContext(); - const sdk = useAppSdk(); + const platform = useAppPlatform(); + + let explorerUrl; + if (isTon(swapAsset.assetAmount.asset.address)) { + explorerUrl = 'https://tonviewer.com/price'; + } else { + explorerUrl = `https://tonviewer.com/${swapAsset.assetAmount.asset.address.toString({ + urlSafe: true + })}`; + } - const onOpenExplorer = (e: React.MouseEvent) => { - e.preventDefault(); + const onClickExplorer: MouseEventHandler = e => { e.stopPropagation(); - - let explorerUrl; - if (isTon(swapAsset.assetAmount.asset.address)) { - explorerUrl = 'https://tonviewer.com/price'; - } else { - explorerUrl = `https://tonviewer.com/${swapAsset.assetAmount.asset.address.toString({ - urlSafe: true - })}`; - } - - sdk.openPage(explorerUrl); }; return ( @@ -228,9 +224,13 @@ const TokenListItem: FC<{ swapAsset: WalletSwapAsset; onClick: () => void }> = ( {swapAsset.assetAmount.asset.symbol} - - - + {platform === 'swap-widget-web' ? ( +
+ ) : ( + + + + )} {swapAsset.assetAmount.stringRelativeAmount} diff --git a/packages/uikit/src/components/swap/tokens-list/SwapTokensListNotification.tsx b/packages/uikit/src/components/swap/tokens-list/SwapTokensListNotification.tsx index ea9f12fc7..7fc66e70c 100644 --- a/packages/uikit/src/components/swap/tokens-list/SwapTokensListNotification.tsx +++ b/packages/uikit/src/components/swap/tokens-list/SwapTokensListNotification.tsx @@ -65,7 +65,7 @@ const SwapTokensListContentWrapper = styled.div` height: 580px; ` : css` - height: calc(var(--app-height) - 6rem); + height: calc(var(--app-height) - 8rem); `} `; diff --git a/packages/uikit/src/hooks/appContext.ts b/packages/uikit/src/hooks/appContext.ts index 69dab1c08..361727d21 100644 --- a/packages/uikit/src/hooks/appContext.ts +++ b/packages/uikit/src/hooks/appContext.ts @@ -67,5 +67,5 @@ export const AppSelectionContext = React.createContext(null) export const useAppPlatform = () => { const { tonendpoint } = useAppContext(); - return tonendpoint.params.platform; + return tonendpoint.targetEnv; }; diff --git a/packages/uikit/src/libs/common.ts b/packages/uikit/src/libs/common.ts index 4b6378a09..a888c794e 100644 --- a/packages/uikit/src/libs/common.ts +++ b/packages/uikit/src/libs/common.ts @@ -62,3 +62,14 @@ export const getCountryName = (language: string, country: string) => { return country; } }; + +export const toErrorMessage = (e: unknown) => { + if (e && typeof e === 'object' && 'message' in e && typeof e.message === 'string') { + return e.message; + } else if (e && typeof e === 'string') { + return e; + } + + console.error(e); + return 'Unknown error'; +}; diff --git a/packages/uikit/src/state/swap/useEncodeSwap.ts b/packages/uikit/src/state/swap/useEncodeSwap.ts index 1c3bf69b3..12897aad1 100644 --- a/packages/uikit/src/state/swap/useEncodeSwap.ts +++ b/packages/uikit/src/state/swap/useEncodeSwap.ts @@ -43,7 +43,7 @@ export function useEncodeSwap() { }); } -export function useEncodeSwapToTonConnectParams(options: { ignoreBattery?: boolean } = {}) { +export function useEncodeSwapToTonConnectParams(options: { forceCalculateBattery?: boolean } = {}) { const { mutateAsync: encode } = useEncodeSwap(); const { data: batteryBalance } = useBatteryBalance(); const { excessAccount: batteryExcess } = useBatteryServiceConfig(); @@ -53,10 +53,13 @@ export function useEncodeSwapToTonConnectParams(options: { ignoreBattery?: boole async swap => { const resultsPromises = [encode(swap)]; - const batterySwapsEnabled = - (activeWalletConfig ? activeWalletConfig.batterySettings.enabledForSwaps : true) && - !options.ignoreBattery; - if (batteryBalance?.batteryUnitsBalance.gt(0) && batterySwapsEnabled) { + const batterySwapsEnabled = activeWalletConfig + ? activeWalletConfig.batterySettings.enabledForSwaps + : true; + if ( + options.forceCalculateBattery || + (batteryBalance?.batteryUnitsBalance.gt(0) && batterySwapsEnabled) + ) { resultsPromises.push( encode({ ...swap, excessAddress: Address.parse(batteryExcess).toRawString() }) );