diff --git a/apps/desktop/src/app/App.tsx b/apps/desktop/src/app/App.tsx index 9cb72ae3d..ef6d7039a 100644 --- a/apps/desktop/src/app/App.tsx +++ b/apps/desktop/src/app/App.tsx @@ -14,6 +14,7 @@ import MemoryScroll from '@tonkeeper/uikit/dist/components/MemoryScroll'; import QrScanner from '@tonkeeper/uikit/dist/components/QrScanner'; import { ActivitySkeletonPage, + BrowserSkeletonPage, CoinSkeletonPage, HomeSkeleton, SettingsSkeletonPage @@ -46,6 +47,7 @@ import { UnlockNotification } from '@tonkeeper/uikit/dist/pages/home/UnlockNotif import ImportRouter from '@tonkeeper/uikit/dist/pages/import'; import Initialize, { InitializeContainer } from '@tonkeeper/uikit/dist/pages/import/Initialize'; import Settings from '@tonkeeper/uikit/dist/pages/settings'; +import Browser from '@tonkeeper/uikit/dist/pages/browser'; import { UserThemeProvider } from '@tonkeeper/uikit/dist/providers/UserThemeProvider'; import { useAccountState } from '@tonkeeper/uikit/dist/state/account'; import { useAuthState } from '@tonkeeper/uikit/dist/state/password'; @@ -71,6 +73,7 @@ const queryClient = new QueryClient({ }); const sdk = new DesktopAppSdk(); +const TARGET_ENV = 'desktop'; const langs = 'en,zh_CN,ru,it,tr'; const listOfAuth: AuthState['kind'][] = ['keychain']; @@ -133,7 +136,12 @@ export const Loader: FC = () => { const { data: account } = useAccountState(); const { data: auth } = useAuthState(); - const tonendpoint = useTonendpoint(sdk.version, activeWallet?.network, activeWallet?.lang); + const tonendpoint = useTonendpoint( + TARGET_ENV, + sdk.version, + activeWallet?.network, + activeWallet?.lang + ); const { data: config } = useTonenpointConfig(tonendpoint); const navigate = useNavigate(); @@ -239,6 +247,14 @@ export const Content: FC<{ } /> + }> + + + } + /> import('@tonkeeper/uikit/dist/pages/import')); const Settings = React.lazy(() => import('@tonkeeper/uikit/dist/pages/settings')); +const Browser = React.lazy(() => import('@tonkeeper/uikit/dist/pages/browser')); const Activity = React.lazy(() => import('@tonkeeper/uikit/dist/pages/activity/Activity')); const Home = React.lazy(() => import('@tonkeeper/uikit/dist/pages/home/Home')); const Coin = React.lazy(() => import('@tonkeeper/uikit/dist/pages/coin/Coin')); @@ -81,6 +82,7 @@ const queryClient = new QueryClient({ }); const sdk = new ExtensionAppSdk(); +const TARGET_ENV = 'extension'; connectToBackground(); export const App: FC = () => { @@ -165,6 +167,7 @@ export const Loader: FC = React.memo(() => { const { data: account } = useAccountState(); const { data: auth } = useAuthState(); const tonendpoint = useTonendpoint( + TARGET_ENV, sdk.version, activeWallet?.network, localizationFrom(browser.i18n.getUILanguage()) @@ -282,6 +285,14 @@ export const Content: FC<{ } /> + }> + + + } + /> import('@tonkeeper/uikit/dist/pages/import/Initialize')); const ImportRouter = React.lazy(() => import('@tonkeeper/uikit/dist/pages/import')); +const Browser = React.lazy(() => import('@tonkeeper/uikit/dist/pages/browser')); const Settings = React.lazy(() => import('@tonkeeper/uikit/dist/pages/settings')); const Activity = React.lazy(() => import('@tonkeeper/uikit/dist/pages/activity/Activity')); const Home = React.lazy(() => import('@tonkeeper/uikit/dist/pages/home/Home')); @@ -77,6 +78,8 @@ const queryClient = new QueryClient({ } }); +const TARGET_ENV = 'twa'; + export const App = () => { return ( @@ -211,7 +214,7 @@ export const Loader: FC<{ sdk: IAppSdk }> = ({ sdk }) => { const { data: account } = useAccountState(); const { data: auth } = useAuthState(); - const tonendpoint = useTonendpoint(sdk.version, activeWallet?.network, activeWallet?.lang); + const tonendpoint = useTonendpoint(TARGET_ENV, sdk.version, activeWallet?.network, activeWallet?.lang); const { data: config } = useTonenpointConfig(tonendpoint); const navigate = useNavigate(); @@ -350,6 +353,14 @@ const MainPages: FC<{ showQrScan: boolean }> = ({ showQrScan }) => { } /> + }> + + + } + />
- + diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index f0bae78f2..bcdabf100 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -12,6 +12,7 @@ import { Loading } from '@tonkeeper/uikit/dist/components/Loading'; import MemoryScroll from '@tonkeeper/uikit/dist/components/MemoryScroll'; import { ActivitySkeletonPage, + BrowserSkeletonPage, CoinSkeletonPage, HomeSkeleton, SettingsSkeletonPage @@ -51,6 +52,7 @@ import { useAnalytics, useAppHeight, useAppWidth } from './libs/hooks'; const ImportRouter = React.lazy(() => import('@tonkeeper/uikit/dist/pages/import')); const Settings = React.lazy(() => import('@tonkeeper/uikit/dist/pages/settings')); +const Browser = React.lazy(() => import('@tonkeeper/uikit/dist/pages/browser')); const Activity = React.lazy(() => import('@tonkeeper/uikit/dist/pages/activity/Activity')); const Home = React.lazy(() => import('@tonkeeper/uikit/dist/pages/home/Home')); const Coin = React.lazy(() => import('@tonkeeper/uikit/dist/pages/coin/Coin')); @@ -81,6 +83,7 @@ const queryClient = new QueryClient({ }); const sdk = new BrowserAppSdk(); +const TARGET_ENV = 'web'; export const App: FC = () => { const { t, i18n } = useTranslation(); @@ -170,7 +173,7 @@ export const Loader: FC = () => { const { data: account } = useAccountState(); const { data: auth } = useAuthState(); - const tonendpoint = useTonendpoint(sdk.version, activeWallet?.network, activeWallet?.lang); + const tonendpoint = useTonendpoint(TARGET_ENV, sdk.version, activeWallet?.network, activeWallet?.lang); const { data: config } = useTonenpointConfig(tonendpoint); const navigate = useNavigate(); @@ -276,6 +279,14 @@ export const Content: FC<{ } /> + }> + + + } + /> { @@ -136,7 +137,9 @@ export abstract class BaseApp implements IAppSdk { isStandalone = () => false; confirm = async (text: string) => window.confirm(text); + alert = async (text: string) => window.alert(text); + prompt = async (message: string, defaultValue?: string) => window.prompt(message, defaultValue); requestExtensionPermission = async () => {}; @@ -153,3 +156,5 @@ export class MockAppSdk extends BaseApp { super(new MemoryStorage()); } } + +export type TargetEnv = 'web' | 'extension' | 'desktop' | 'twa'; diff --git a/packages/core/src/tonkeeperApi/tonendpoint.ts b/packages/core/src/tonkeeperApi/tonendpoint.ts index ee8961e31..5305fb3a8 100644 --- a/packages/core/src/tonkeeperApi/tonendpoint.ts +++ b/packages/core/src/tonkeeperApi/tonendpoint.ts @@ -1,6 +1,7 @@ import { intlLocale } from '../entries/language'; import { Network } from '../entries/network'; import { FetchAPI } from '../tonApiV2'; +import { TargetEnv } from '../AppSdk'; interface BootParams { platform: 'ios' | 'android' | 'web'; @@ -39,6 +40,8 @@ export interface TonendpointConfig { transactionExplorer?: string; NFTOnExplorerUrl?: string; + featured_play_interval?: number; + /** * @deprecated use ton api */ @@ -66,16 +69,20 @@ export class Tonendpoint { public basePath: string; + public readonly targetEnv: TargetEnv; + constructor( { lang = 'en', build = '3.0.0', network = Network.MAINNET, platform = 'web', - countryCode - }: Partial, + countryCode, + targetEnv + }: Partial & { targetEnv: TargetEnv }, { fetchApi = defaultFetch, basePath = defaultTonendpoint }: BootOptions ) { + this.targetEnv = targetEnv; this.params = { lang, build, network, platform, countryCode }; this.fetchApi = fetchApi; this.basePath = basePath; @@ -85,15 +92,31 @@ export class Tonendpoint { this.params.countryCode = countryCode; }; - toSearchParams = () => { + toSearchParams = ( + rewriteParams?: Partial, + additionalParams?: Record + ) => { const params = new URLSearchParams({ - lang: intlLocale(this.params.lang), - build: this.params.build, - chainName: this.params.network === Network.TESTNET ? 'testnet' : 'mainnet', - platform: this.params.platform + lang: intlLocale(rewriteParams?.lang ?? this.params.lang), + build: rewriteParams?.build ?? this.params.build, + chainName: + (rewriteParams?.network ?? this.params.network) === Network.TESTNET + ? 'testnet' + : 'mainnet', + platform: rewriteParams?.platform ?? this.params.platform }); - if (this.params.countryCode) { - params.append('countryCode', this.params.countryCode); + const countryCode = rewriteParams?.countryCode ?? this.params.countryCode; + + if (countryCode) { + params.append('countryCode', countryCode); + } + + if (!additionalParams) { + return params.toString(); + } + + for (const key in additionalParams) { + params.append(key, additionalParams[key].toString()); } return params.toString(); }; @@ -109,10 +132,17 @@ export class Tonendpoint { return response.json(); }; - GET = async (path: string): Promise => { - const response = await this.fetchApi(`${this.basePath}${path}?${this.toSearchParams()}`, { - method: 'GET' - }); + GET = async ( + path: string, + rewriteParams?: Partial, + additionalParams?: Record + ): Promise => { + const response = await this.fetchApi( + `${this.basePath}${path}?${this.toSearchParams(rewriteParams, additionalParams)}`, + { + method: 'GET' + } + ); const result: TonendpointResponse = await response.json(); if (!result.success) { @@ -121,6 +151,18 @@ export class Tonendpoint { return result.data; }; + + getFiatMethods = (countryCode?: string | null | undefined): Promise => { + return this.GET('/fiat/methods', { countryCode }); + }; + + getAppsPopular = (countryCode?: string | null | undefined): Promise => { + return this.GET( + '/apps/popular', + { countryCode }, + { track: this.targetEnv === 'extension' ? 'extension' : 'desktop' } + ); + }; } export const getServerConfig = async (tonendpoint: Tonendpoint): Promise => { @@ -168,10 +210,25 @@ export interface TonendpoinFiatMethods { categories: TonendpoinFiatCategory[]; } -export const getFiatMethods = async ( - tonendpoint: Tonendpoint, - countryCode: string | null | undefined -) => { - tonendpoint.setCountryCode(countryCode); - return tonendpoint.GET('/fiat/methods'); -}; +export interface CarouselApp extends PromotedApp { + poster: string; +} + +export interface PromotedApp { + name: string; + description: string; + icon: string; + url: string; + textColor?: string; +} + +export interface PromotionCategory { + id: string; + title: string; + apps: PromotedApp[]; +} + +export interface Recommendations { + categories: PromotionCategory[]; + apps: CarouselApp[]; +} diff --git a/packages/uikit/package.json b/packages/uikit/package.json index a97be750b..31d3721b1 100644 --- a/packages/uikit/package.json +++ b/packages/uikit/package.json @@ -28,7 +28,9 @@ "react-lottie": "^1.2.4", "react-qrcode-logo": "^2.9.0", "react-router-dom": "^6.4.5", + "react-slick": "^0.29.0", "react-transition-group": "^4.4.5", + "slick-carousel": "^1.8.1", "styled-components": "^6.1.1", "ton": "^13.4.1", "ton-core": "^0.49.0", @@ -50,7 +52,9 @@ "@types/react-beautiful-dnd": "^13.1.3", "@types/react-dom": "^18.0.9", "@types/react-lottie": "^1.2.6", + "@types/react-slick": "^0", "@types/react-transition-group": "^4.4.5", + "@types/slick-carousel": "^1", "@types/styled-components": "^5.1.26", "@types/uuid": "^9.0.1", "babel-loader": "^8.3.0", diff --git a/packages/uikit/src/components/Body.tsx b/packages/uikit/src/components/Body.tsx index 82a219fca..2e0caf8a8 100644 --- a/packages/uikit/src/components/Body.tsx +++ b/packages/uikit/src/components/Body.tsx @@ -126,70 +126,71 @@ export const useAppSelection = (elementRef: React.MutableRefObject( - ({ children }, ref) => { - const sdk = useAppSdk(); - const { standalone } = useAppContext(); - - const innerRef = useRef(null); - - const elementRef = ( - ref == null ? innerRef : ref - ) as React.MutableRefObject; - - useLayoutEffect(() => { - const element = elementRef.current; - if (!element) return; - if (!standalone) return; - - let timer: NodeJS.Timeout | undefined; - - const handlerScroll = throttle(() => { - if (element.scrollTop < 10) { - setTop(); - } else { - removeTop(); - } - if (element.scrollTop + element.clientHeight < element.scrollHeight - 10) { - removeBottom(); - } else { - setBottom(); - } - clearTimeout(timer); - if (!document.body.classList.contains('scroll')) { - document.body.classList.add('scroll'); - } - timer = setTimeout(function () { - document.body.classList.remove('scroll'); - }, 300); - }, 50); - - element.addEventListener('scroll', handlerScroll); - sdk.uiEvents.on('loading', handlerScroll); - - handlerScroll(); - - return () => { +export const InnerBody = React.forwardRef< + HTMLDivElement, + PropsWithChildren & { className?: string } +>(({ children, className }, ref) => { + const sdk = useAppSdk(); + const { standalone } = useAppContext(); + + const innerRef = useRef(null); + + const elementRef = ( + ref == null ? innerRef : ref + ) as React.MutableRefObject; + + useLayoutEffect(() => { + const element = elementRef.current; + if (!element) return; + if (!standalone) return; + + let timer: NodeJS.Timeout | undefined; + + const handlerScroll = throttle(() => { + if (element.scrollTop < 10) { setTop(); + } else { + removeTop(); + } + if (element.scrollTop + element.clientHeight < element.scrollHeight - 10) { + removeBottom(); + } else { setBottom(); - clearTimeout(timer); - sdk.uiEvents.off('loading', handlerScroll); - - element.removeEventListener('scroll', handlerScroll); - }; - }, [elementRef]); - - const selection = useAppSelection(elementRef); - const id = standalone ? 'body' : undefined; - - return ( - - - {children} - - - ); - } -); + } + clearTimeout(timer); + if (!document.body.classList.contains('scroll')) { + document.body.classList.add('scroll'); + } + timer = setTimeout(function () { + document.body.classList.remove('scroll'); + }, 300); + }, 50); + + element.addEventListener('scroll', handlerScroll); + sdk.uiEvents.on('loading', handlerScroll); + + handlerScroll(); + + return () => { + setTop(); + setBottom(); + clearTimeout(timer); + sdk.uiEvents.off('loading', handlerScroll); + + element.removeEventListener('scroll', handlerScroll); + }; + }, [elementRef]); + + const selection = useAppSelection(elementRef); + const id = standalone ? 'body' : undefined; + + return ( + + + {children} + + + ); +}); InnerBody.displayName = 'InnerBody'; diff --git a/packages/uikit/src/components/Footer.tsx b/packages/uikit/src/components/Footer.tsx index 3c38a6732..3fdebb3f9 100644 --- a/packages/uikit/src/components/Footer.tsx +++ b/packages/uikit/src/components/Footer.tsx @@ -53,6 +53,36 @@ const ActivityIcon = () => { ); }; +const BrowserIcon = () => { + return ( + + + + + + ); +}; + const SettingsIcon = () => { return ( ` max-width: 548px; box-sizing: border-box; overflow: visible !important; + z-index: 3; background-color: ${props => props.theme.backgroundPage}; @@ -150,6 +181,9 @@ export const Footer: FC<{ standalone?: boolean; sticky?: boolean }> = ({ standal if (location.pathname.includes(AppRoute.settings)) { return AppRoute.settings; } + if (location.pathname.includes(AppRoute.browser)) { + return AppRoute.browser; + } return AppRoute.home; }, [location.pathname]); @@ -177,6 +211,13 @@ export const Footer: FC<{ standalone?: boolean; sticky?: boolean }> = ({ standal {t('activity_screen_title')} +

{t('browser_title')}

+ {isCountryLoading ? ( + + ) : ( + + + {country || '🌎'} + + + )} + + ); +}; diff --git a/packages/uikit/src/components/List.tsx b/packages/uikit/src/components/List.tsx index 3919f7b7c..63e9d6109 100644 --- a/packages/uikit/src/components/List.tsx +++ b/packages/uikit/src/components/List.tsx @@ -1,6 +1,14 @@ -import React, { FC, PropsWithChildren, useContext, useLayoutEffect, useRef, useState } from 'react'; +import React, { + forwardRef, + PropsWithChildren, + useContext, + useLayoutEffect, + useRef, + useState +} from 'react'; import styled, { createGlobalStyle, css } from 'styled-components'; import { AppSelectionContext, useAppContext } from '../hooks/appContext'; +import { mergeRefs } from '../libs/common'; export const ListBlock = styled.div<{ margin?: boolean; @@ -129,14 +137,15 @@ export const GlobalListStyle = createGlobalStyle` } `; -export const ListItem: FC< +export const ListItem = forwardRef< + HTMLDivElement, PropsWithChildren< { hover?: boolean; dropDown?: boolean } & Omit< React.HTMLProps, 'size' | 'children' | 'as' | 'ref' > > -> = ({ children, hover, dropDown, ...props }) => { +>(({ children, hover, dropDown, ...props }, externalRef) => { const selection = useContext(AppSelectionContext); const { ios } = useAppContext(); const [isHover, setHover] = useState(false); @@ -154,7 +163,7 @@ export const ListItem: FC< ); -}; +}); diff --git a/packages/uikit/src/components/Skeleton.tsx b/packages/uikit/src/components/Skeleton.tsx index bcb49324c..062efc1bb 100644 --- a/packages/uikit/src/components/Skeleton.tsx +++ b/packages/uikit/src/components/Skeleton.tsx @@ -1,8 +1,8 @@ import React, { FC, useEffect } from 'react'; -import styled, { css } from 'styled-components'; +import styled from 'styled-components'; import { useAppSdk } from '../hooks/appSdk'; import { InnerBody } from './Body'; -import { ActivityHeader, SettingsHeader } from './Header'; +import {ActivityHeader, BrowserHeader, SettingsHeader} from './Header'; import { ActionsRow } from './home/Actions'; import { BalanceSkeleton } from './home/Balance'; import { CoinInfoSkeleton } from './jettons/Info'; @@ -10,83 +10,9 @@ import { ColumnText } from './Layout'; import { ListBlock, ListItem, ListItemPayload } from './List'; import { SubHeader } from './SubHeader'; import { H3 } from './Text'; - -function randomIntFromInterval(min: number, max: number) { - return Math.floor(Math.random() * (max - min + 1) + min); -} - -function randomWidth() { - return randomIntFromInterval(30, 90) + '%'; -} - -const Base = styled.div` - display: inline-block; - - @keyframes placeHolderShimmer { - 0% { - background-position: -800px 0; - } - 100% { - background-position: 800px 0; - } - } - - animation-duration: 2s; - animation-fill-mode: forwards; - animation-iteration-count: infinite; - animation-name: placeHolderShimmer; - animation-timing-function: linear; - background-color: #f6f7f8; - background: linear-gradient(to right, #4f5a70 8%, #bbbbbb 18%, #4f5a70 33%); - background-size: 800px 104px; - - opacity: 0.1; - - position: relative; -`; -const Block = styled(Base)<{ size?: string; width?: string }>` - border-radius: ${props => props.theme.cornerExtraExtraSmall}; - - ${props => css` - width: ${props.width ?? randomWidth()}; - `} - - ${props => { - switch (props.size) { - case 'large': - return css` - height: 1.5rem; - `; - case 'small': - return css` - height: 0.5rem; - `; - default: - return css` - height: 1rem; - `; - } - }} -`; - -export const SkeletonText: FC<{ size?: 'large' | 'small'; width?: string }> = React.memo( - ({ size, width }) => { - return ; - } -); - -const Image = styled(Base)<{ width?: string }>` - border-radius: ${props => props.theme.cornerFull}; - - ${props => css` - width: ${props.width ?? '44px'}; - height: ${props.width ?? '44px'}; - `} -`; - -export const SkeletonImage: FC<{ width?: string }> = React.memo(({ width }) => { - return ; -}); +import { SkeletonImage, SkeletonText } from './shared/Skeleton'; +import {randomIntFromInterval} from "../libs/common"; +import {RecommendationsPageBodySkeleton} from "./skeletons/BrowserSkeletons"; export const SkeletonSubHeader = React.memo(() => { return } />; @@ -213,6 +139,17 @@ export const SettingsSkeletonPage = React.memo(() => { ); }); +export const BrowserSkeletonPage = React.memo(() => { + return ( + <> + + + + + + ); +}); + export const HistoryBlock = styled.div` margin-top: 3rem; `; diff --git a/packages/uikit/src/components/SubHeader.tsx b/packages/uikit/src/components/SubHeader.tsx index 3aa2e4e33..1e8a5026e 100644 --- a/packages/uikit/src/components/SubHeader.tsx +++ b/packages/uikit/src/components/SubHeader.tsx @@ -7,7 +7,9 @@ import { ChevronLeftIcon } from './Icon'; import { H3 } from './Text'; import { BackButton } from './fields/BackButton'; -const Block = styled.div` +export const WithHeadingDivider = styled.div``; + +const Block = styled(WithHeadingDivider)` flex-shrink: 0; padding: 1rem; @@ -27,7 +29,7 @@ const Block = styled.div` `; export const SybHeaderGlobalStyle = createGlobalStyle` - body:not(.top) ${Block} { + body:not(.top) ${WithHeadingDivider} { &:after { content: ''; display: block; diff --git a/packages/uikit/src/components/home/Balance.tsx b/packages/uikit/src/components/home/Balance.tsx index 380e692f2..73f60ffe4 100644 --- a/packages/uikit/src/components/home/Balance.tsx +++ b/packages/uikit/src/components/home/Balance.tsx @@ -13,7 +13,7 @@ import { formatFiatCurrency } from '../../hooks/balance'; import { useTranslation } from '../../hooks/translation'; import { QueryKey } from '../../libs/queryKey'; import { TokenRate, getRateKey } from '../../state/rates'; -import { SkeletonText } from '../Skeleton'; +import { SkeletonText } from '../shared/Skeleton'; import { Body3, Label2, Num2 } from '../Text'; import { AssetData } from './Jettons'; diff --git a/packages/uikit/src/components/jettons/Info.tsx b/packages/uikit/src/components/jettons/Info.tsx index 5ed61806b..e3d313b3d 100644 --- a/packages/uikit/src/components/jettons/Info.tsx +++ b/packages/uikit/src/components/jettons/Info.tsx @@ -1,6 +1,6 @@ import React, { FC } from 'react'; import styled from 'styled-components'; -import { SkeletonImage, SkeletonText } from '../Skeleton'; +import { SkeletonImage, SkeletonText } from '../shared/Skeleton'; import { H2 } from '../Text'; import { Body } from './CroppedText'; diff --git a/packages/uikit/src/components/shared/Skeleton.tsx b/packages/uikit/src/components/shared/Skeleton.tsx new file mode 100644 index 000000000..a97e4d4a9 --- /dev/null +++ b/packages/uikit/src/components/shared/Skeleton.tsx @@ -0,0 +1,96 @@ +import styled, { css } from 'styled-components'; +import React, { FC } from 'react'; +import { randomIntFromInterval } from '../../libs/common'; + +function randomWidth() { + return randomIntFromInterval(30, 90) + '%'; +} + +const Base = styled.div` + display: inline-block; + + @keyframes placeHolderShimmer { + 0% { + background-position: -800px 0; + } + 100% { + background-position: 800px 0; + } + } + + animation-duration: 2s; + animation-fill-mode: forwards; + animation-iteration-count: infinite; + animation-name: placeHolderShimmer; + animation-timing-function: linear; + background-color: #f6f7f8; + background: linear-gradient(to right, #4f5a70 8%, #bbbbbb 18%, #4f5a70 33%); + background-size: 800px 104px; + + opacity: 0.1; + + position: relative; +`; +const Block = styled(Base)<{ size?: string; width?: string }>` + border-radius: ${props => props.theme.cornerExtraExtraSmall}; + + ${props => css` + width: ${props.width ?? randomWidth()}; + `} + + ${props => { + switch (props.size) { + case 'large': + return css` + height: 1.5rem; + `; + case 'small': + return css` + height: 0.5rem; + `; + default: + return css` + height: 1rem; + `; + } + }} +`; + +export const SkeletonText: FC<{ size?: 'large' | 'small'; width?: string; className?: string }> = + React.memo(({ size, width, className }) => { + return ; + }); + +export const Skeleton = styled(Base)<{ + width: string; + height?: string; + borderRadius?: string; + margin?: string; + marginBottom?: string; +}>` + display: block; + border-radius: ${props => + props.borderRadius + ? props.theme[props.borderRadius] || props.theme.cornerExtraExtraSmall + : props.theme.cornerExtraExtraSmall}; + + ${props => css` + width: ${props.width ?? '3rem'}; + height: ${props.height ?? '20px'}; + ${props.margin && `margin: ${props.margin};`} + ${props.marginBottom && `margin-bottom: ${props.marginBottom};`} + `} +`; + +const Image = styled(Base)<{ width?: string }>` + border-radius: ${props => props.theme.cornerFull}; + + ${props => css` + width: ${props.width ?? '44px'}; + height: ${props.width ?? '44px'}; + `} +`; + +export const SkeletonImage: FC<{ width?: string }> = React.memo(({ width }) => { + return ; +}); diff --git a/packages/uikit/src/components/shared/carousel/index.tsx b/packages/uikit/src/components/shared/carousel/index.tsx new file mode 100644 index 000000000..ecd8d3cd4 --- /dev/null +++ b/packages/uikit/src/components/shared/carousel/index.tsx @@ -0,0 +1,123 @@ +import { FC, PropsWithChildren, useRef, useState, WheelEvent } from 'react'; +import styled from 'styled-components'; +import Slider, { Settings } from 'react-slick'; +import { ChevronLeftIcon, ChevronRightIcon } from '../../Icon'; +import 'slick-carousel/slick/slick.css'; +import 'slick-carousel/slick/slick-theme.css'; + +const SwipeButton = styled.button<{ position: 'left' | 'right' }>` + width: 40px; + height: 40px; + border-radius: ${props => props.theme.cornerFull}; + color: ${props => props.theme.textPrimary}; + background-color: ${props => props.theme.backgroundContentTint}; + opacity: 0.64; + display: flex; + justify-content: center; + align-items: center; + position: absolute; + z-index: 2; + border: none; + cursor: pointer; + top: calc(50% - 20px); + ${props => (props.position === 'left' ? 'left: 12px;' : 'right: 12px;')}; + transition: opacity 0.2s ease-in-out; + + &:hover { + opacity: 0.8; + } +`; + +const CarouselWrapper = styled.div<{ gap: string; isFirst?: boolean }>` + overflow: hidden; + position: relative; + + .slick-list { + margin: 0 -${props => parseFloat(props.gap) / 2}px; + ${props => props.isFirst && 'padding-left: 16px !important;'} + } + .slick-slide > div { + margin: 0 ${props => parseFloat(props.gap) / 2}px; + } +`; + +export interface CarouselProps { + gap: string; + className?: string; +} + +const defaultSettings = { + speed: 500, + slidesToShow: 1, + slidesToScroll: 1, + centerMode: true, + arrows: false +}; + +export const Carousel: FC = ({ + children, + gap, + className, + ...settings +}) => { + const isInfinite = settings.infinite !== false; + const sliderRef = useRef(null); + const isSwiping = useRef(false); + + const [hideRightButton, setHideRightButton] = useState(false); + const [hideLeftButton, setHideLeftButton] = useState(!isInfinite); + + const onWheel = (e: WheelEvent) => { + if (!isSwiping.current) { + isSwiping.current = true; + + if (e.deltaX > 0) { + return sliderRef.current?.slickNext(); + } + + if (e.deltaX < 0) { + return sliderRef.current?.slickPrev(); + } + } + }; + + const blockSwipe = () => { + isSwiping.current = true; + }; + + const unblockSwipe = () => { + isSwiping.current = false; + }; + + const beforeChange = (_: number, nextIndex: number) => { + blockSwipe(); + + const childrenLength = children && Array.isArray(children) ? children.length : 0; + setHideRightButton(nextIndex === childrenLength - 1 && !isInfinite); + setHideLeftButton(nextIndex === 0 && !isInfinite); + }; + + return ( + + {!hideLeftButton && ( + sliderRef.current?.slickPrev()}> + + + )} + + {children} + + {!hideRightButton && ( + sliderRef.current?.slickNext()}> + + + )} + + ); +}; diff --git a/packages/uikit/src/components/shared/index.ts b/packages/uikit/src/components/shared/index.ts new file mode 100644 index 000000000..5f6643893 --- /dev/null +++ b/packages/uikit/src/components/shared/index.ts @@ -0,0 +1 @@ +export * from './carousel'; diff --git a/packages/uikit/src/components/skeletons/BrowserSkeletons.tsx b/packages/uikit/src/components/skeletons/BrowserSkeletons.tsx new file mode 100644 index 000000000..d41cab06c --- /dev/null +++ b/packages/uikit/src/components/skeletons/BrowserSkeletons.tsx @@ -0,0 +1,61 @@ +import { PromotedItem, PromotedItemText } from '../../pages/browser/PromotedItem'; +import { Skeleton } from '../shared/Skeleton'; +import React, { FC } from 'react'; +import { ListBlock, ListItem } from '../List'; +import styled from 'styled-components'; + +const ListItemStyled = styled(ListItem)` + padding-left: 1rem; +`; + +const Heading = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + padding: 14px 0; + gap: 1rem; +`; + +const CarouselSkeleton = styled(Skeleton)` + height: auto; + aspect-ratio: 2 / 1; +`; + +export const RecommendationsPageBodySkeleton: FC = () => { + return ( + <> + + + + + ); +}; + +export const RecommendationPageListItemSkeleton = () => { + return ( + + + + + + + + + + ); +}; + +const CategorySkeleton: FC<{ className?: string }> = ({ className }) => { + return ( +
+ + + + + + + + +
+ ); +}; diff --git a/packages/uikit/src/hooks/appContext.ts b/packages/uikit/src/hooks/appContext.ts index a2363ed7a..b7aac34a9 100644 --- a/packages/uikit/src/hooks/appContext.ts +++ b/packages/uikit/src/hooks/appContext.ts @@ -34,7 +34,7 @@ export const AppContext = React.createContext({ auth: defaultAuthState, fiat: FiatCurrencies.USD, config: defaultTonendpointConfig, - tonendpoint: new Tonendpoint({}, {}), + tonendpoint: new Tonendpoint({ targetEnv: 'web' }, {}), standalone: false, extension: false, ios: false, diff --git a/packages/uikit/src/hooks/browser/useRecommendations.ts b/packages/uikit/src/hooks/browser/useRecommendations.ts new file mode 100644 index 000000000..afa376252 --- /dev/null +++ b/packages/uikit/src/hooks/browser/useRecommendations.ts @@ -0,0 +1,15 @@ +import { useUserCountry } from '../../state/country'; +import { useQuery } from '@tanstack/react-query'; +import { QueryKey } from '../../libs/queryKey'; +import { Recommendations } from '@tonkeeper/core/dist/tonkeeperApi/tonendpoint'; +import { useAppContext } from '../appContext'; + +export function useRecommendations() { + const { tonendpoint } = useAppContext(); + const country = useUserCountry(); + const lang = country.data || 'en'; + + return useQuery([QueryKey.featuredRecommendations, lang], async () => { + return tonendpoint.getAppsPopular(lang); + }); +} diff --git a/packages/uikit/src/hooks/useAreaClick.ts b/packages/uikit/src/hooks/useAreaClick.ts new file mode 100644 index 000000000..182d421b7 --- /dev/null +++ b/packages/uikit/src/hooks/useAreaClick.ts @@ -0,0 +1,55 @@ +import { useCallback, useRef } from 'react'; +import { useEventListener } from './useEventListener'; +import { useAppSdk } from './appSdk'; + +export function useAreaClick({ + callback, + precisionXPx, + precisionYPx +}: { + callback: () => void; + precisionXPx?: number; + precisionYPx?: number; +}) { + const clickedPosition = useRef<{ clientX: number; clientY: number }>({ + clientX: 0, + clientY: 0 + }); + + const ref = useRef(null); + + const onMouseDown = useCallback((e: MouseEvent) => { + clickedPosition.current = { + clientY: e.clientY, + clientX: e.clientX + }; + }, []); + + const onMouseUp = useCallback( + (e: MouseEvent) => { + const xInArea = + Math.abs(e.clientX - clickedPosition.current.clientX) < (precisionXPx ?? 10); + const yInArea = + Math.abs(e.clientY - clickedPosition.current.clientY) < (precisionYPx ?? 10); + if (xInArea && yInArea) { + callback(); + } + }, + [callback, precisionXPx, precisionYPx] + ); + + useEventListener<'mousedown', HTMLElement>('mousedown', onMouseDown, ref!); + useEventListener('mouseup', onMouseUp, ref!); + + return ref; +} + +export function useOpenLinkOnAreaClick(url: string) { + const sdk = useAppSdk(); + + const callback = useCallback(() => { + sdk.openPage(url); + }, [url, sdk]); + + return useAreaClick({ callback }); +} diff --git a/packages/uikit/src/hooks/useElementSize.ts b/packages/uikit/src/hooks/useElementSize.ts new file mode 100644 index 000000000..9639f1436 --- /dev/null +++ b/packages/uikit/src/hooks/useElementSize.ts @@ -0,0 +1,33 @@ +import { useCallback, useLayoutEffect, useState } from 'react'; +import { useEventListener } from './useEventListener'; + +interface Size { + width: number; + height: number; +} + +export function useElementSize(): [ + (node: T | null) => void, + Size +] { + const [ref, setRef] = useState(null); + const [size, setSize] = useState({ + width: 0, + height: 0 + }); + + const handleSize = useCallback(() => { + setSize({ + width: ref?.offsetWidth || 0, + height: ref?.offsetHeight || 0 + }); + }, [ref?.offsetHeight, ref?.offsetWidth]); + + useEventListener('resize', handleSize); + + useLayoutEffect(() => { + handleSize(); + }, [ref?.offsetHeight, ref?.offsetWidth]); + + return [setRef, size]; +} diff --git a/packages/uikit/src/hooks/useEventListener.ts b/packages/uikit/src/hooks/useEventListener.ts new file mode 100644 index 000000000..e3d42d02a --- /dev/null +++ b/packages/uikit/src/hooks/useEventListener.ts @@ -0,0 +1,68 @@ +import { RefObject, useEffect, useLayoutEffect, useRef } from 'react'; + +function useEventListener( + eventName: K, + handler: (event: MediaQueryListEventMap[K]) => void, + element: RefObject, + options?: boolean | AddEventListenerOptions +): void; + +function useEventListener( + eventName: K, + handler: (event: WindowEventMap[K]) => void, + element?: undefined, + options?: boolean | AddEventListenerOptions +): void; + +function useEventListener< + K extends keyof HTMLElementEventMap, + T extends HTMLElement = HTMLDivElement +>( + eventName: K, + handler: (event: HTMLElementEventMap[K]) => void, + element: RefObject, + options?: boolean | AddEventListenerOptions +): void; + +function useEventListener( + eventName: K, + handler: (event: DocumentEventMap[K]) => void, + element: RefObject, + options?: boolean | AddEventListenerOptions +): void; + +function useEventListener< + KW extends keyof WindowEventMap, + KH extends keyof HTMLElementEventMap, + KM extends keyof MediaQueryListEventMap, + T extends HTMLElement | MediaQueryList | void = void +>( + eventName: KW | KH | KM, + handler: ( + event: WindowEventMap[KW] | HTMLElementEventMap[KH] | MediaQueryListEventMap[KM] | Event + ) => void, + element?: RefObject, + options?: boolean | AddEventListenerOptions +) { + const savedHandler = useRef(handler); + + useLayoutEffect(() => { + savedHandler.current = handler; + }, [handler]); + + useEffect(() => { + const targetElement: T | Window = element?.current ?? window; + + if (!(targetElement && targetElement.addEventListener)) return; + + const listener: typeof handler = event => savedHandler.current(event); + + targetElement.addEventListener(eventName, listener, options); + + return () => { + targetElement.removeEventListener(eventName, listener, options); + }; + }, [eventName, element, options]); +} + +export { useEventListener }; diff --git a/packages/uikit/src/libs/common.ts b/packages/uikit/src/libs/common.ts index 1135a9966..cae49b453 100644 --- a/packages/uikit/src/libs/common.ts +++ b/packages/uikit/src/libs/common.ts @@ -1,3 +1,5 @@ +import { MutableRefObject, Ref, RefCallback } from 'react'; + export const scrollToTop = () => { if (!document.body.classList.contains('top')) { const body = document.getElementById('body'); @@ -12,3 +14,27 @@ export const scrollToTop = () => { } } }; + +export function mergeRefs(...inputRefs: (Ref | undefined)[]): Ref | RefCallback { + const filteredInputRefs = inputRefs.filter(Boolean); + + if (filteredInputRefs.length <= 1) { + const firstRef = filteredInputRefs[0]; + + return firstRef || null; + } + + return function mergedRefs(ref) { + filteredInputRefs.forEach(inputRef => { + if (typeof inputRef === 'function') { + inputRef(ref); + } else if (inputRef) { + (inputRef as MutableRefObject).current = ref; + } + }); + }; +} + +export function randomIntFromInterval(min: number, max: number) { + return Math.floor(Math.random() * (max - min + 1) + min); +} diff --git a/packages/uikit/src/libs/queryKey.ts b/packages/uikit/src/libs/queryKey.ts index 941c99811..30cc883a3 100644 --- a/packages/uikit/src/libs/queryKey.ts +++ b/packages/uikit/src/libs/queryKey.ts @@ -19,6 +19,7 @@ export enum QueryKey { connection = 'connection', subscribed = 'subscribed', + featuredRecommendations = 'recommendations', tron = 'tron', rate = 'rate', diff --git a/packages/uikit/src/libs/routes.ts b/packages/uikit/src/libs/routes.ts index 3973a2577..b9293340b 100644 --- a/packages/uikit/src/libs/routes.ts +++ b/packages/uikit/src/libs/routes.ts @@ -2,6 +2,7 @@ export enum AppRoute { import = '/import', settings = '/settings', + browser = '/browser', activity = '/activity', coins = '/coins', home = '/' @@ -29,6 +30,11 @@ export enum SettingsRoute { country = '/country' } +export enum BrowserRoute { + index = '/', + category = '/category' +} + export const any = (route: string): string => { return `${route}/*`; }; diff --git a/packages/uikit/src/pages/browser/BrowserRecommendationsPage.tsx b/packages/uikit/src/pages/browser/BrowserRecommendationsPage.tsx new file mode 100644 index 000000000..37feb3ee3 --- /dev/null +++ b/packages/uikit/src/pages/browser/BrowserRecommendationsPage.tsx @@ -0,0 +1,49 @@ +import { FC } from 'react'; +import styled from 'styled-components'; +import { useRecommendations } from '../../hooks/browser/useRecommendations'; +import { PromotionsCarousel } from './PromotionsCarousel'; +import { CategoryBlock } from './CategoryBlock'; +import { InnerBody } from '../../components/Body'; +import { RecommendationsPageBodySkeleton } from '../../components/skeletons/BrowserSkeletons'; +import { BrowserHeader } from '../../components/Header'; + +const InnerBodyStyled = styled(InnerBody)` + padding: 0; +`; + +const PromotionsCarouselStyled = styled(PromotionsCarousel)` + margin-bottom: 1rem; +`; + +const CategoryBlockStyled = styled(CategoryBlock)` + margin-bottom: 1rem; +`; + +const SkeletonContainer = styled.div` + padding: 0 1rem; +`; + +export const BrowserRecommendationsPage: FC = () => { + const { data } = useRecommendations(); + + return ( +
+ + + + {data ? ( + <> + + {data.categories.map(category => ( + + ))} + + ) : ( + + + + )} + +
+ ); +}; diff --git a/packages/uikit/src/pages/browser/CategoryBlock.tsx b/packages/uikit/src/pages/browser/CategoryBlock.tsx new file mode 100644 index 000000000..36320026b --- /dev/null +++ b/packages/uikit/src/pages/browser/CategoryBlock.tsx @@ -0,0 +1,142 @@ +import { FC, useMemo } from 'react'; +import styled from 'styled-components'; +import { Body3, H3, Label1, Label2 } from '../../components/Text'; +import { ListBlock, ListItem } from '../../components/List'; +import { Carousel } from '../../components/shared'; +import { PromotedItem, PromotedItemImage, PromotedItemText } from './PromotedItem'; +import { useElementSize } from '../../hooks/useElementSize'; +import { ChevronRightIcon } from '../../components/Icon'; +import { useOpenLinkOnAreaClick } from '../../hooks/useAreaClick'; +import { Link } from 'react-router-dom'; +import { BrowserRoute } from '../../libs/routes'; +import { PromotedApp, PromotionCategory } from '@tonkeeper/core/dist/tonkeeperApi/tonendpoint'; + +const Heading = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + padding: 14px 1rem; + gap: 1rem; +`; + +const AllButton = styled.button` + border: none; + background: transparent; + height: fit-content; + width: fit-content; + color: ${props => props.theme.textAccent}; + cursor: pointer; + padding: 4px 8px; +`; + +const ListContainer = styled.div` + padding-left: 1rem; + padding-right: 1rem; +`; + +const ListBlockStyled = styled(ListBlock)<{ width: string; marginLeft?: string }>` + width: ${props => props.width} !important; + margin-left: ${props => props.marginLeft} !important; + margin-bottom: 0; +`; + +const IconContainerStyled = styled.div` + margin-left: auto; + margin-right: 1rem; + color: ${props => props.theme.iconTertiary}; + transition: transform 0.2s ease; +`; + +const ListItemStyled = styled(ListItem)` + padding-left: 1rem; + + &:hover ${IconContainerStyled} { + transform: translateX(2px); + } +`; + +export const CategoryBlock: FC<{ category: PromotionCategory; className?: string }> = ({ + category, + className +}) => { + const [containerRef, { width: w }] = useElementSize(); + const width = w - 36; + const groups = useMemo( + () => + category.apps.reduce((acc, app, index) => { + if (index % 3 === 0) { + acc.push([app]); + } else { + acc[acc.length - 1].push(app); + } + return acc; + }, [] as PromotedApp[][]), + [category.apps] + ); + + const groupsKeys = useMemo(() => groups.map(group => group.map(i => i.url).join('')), [groups]); + const canExpand = groups.length > 1; + + return ( +
+ +

{category.title}

+ {canExpand && ( + + + All + + + )} +
+ {canExpand ? ( + + {groups.map((group, groupIndex) => ( + + {group.map(item => ( + + ))} + + ))} + + ) : ( + groups.map((group, groupIndex) => ( + + + {group.map(item => ( + + ))} + + + )) + )} +
+ ); +}; + +export const CategoryGroupItem: FC<{ item: PromotedApp }> = ({ item }) => { + const ref = useOpenLinkOnAreaClick(item.url); + + return ( + + + + + {item.name} + {item.description} + + + + + + + ); +}; diff --git a/packages/uikit/src/pages/browser/CategoryPage.tsx b/packages/uikit/src/pages/browser/CategoryPage.tsx new file mode 100644 index 000000000..5a633dc39 --- /dev/null +++ b/packages/uikit/src/pages/browser/CategoryPage.tsx @@ -0,0 +1,38 @@ +import { SubHeader } from '../../components/SubHeader'; +import { InnerBody } from '../../components/Body'; +import React from 'react'; +import { useParams } from 'react-router-dom'; +import { CategoryGroupItem } from './CategoryBlock'; +import { ListBlock } from '../../components/List'; +import { useRecommendations } from '../../hooks/browser/useRecommendations'; +import { RecommendationPageListItemSkeleton } from '../../components/skeletons/BrowserSkeletons'; + +export const CategoryPage = () => { + const { id } = useParams(); + const { data } = useRecommendations(); + + const group = data?.categories.find(item => item.id === id); + + return ( + <> + + + {group ? ( + + {group.apps.map(item => ( + + ))} + + ) : ( + + + + + + + + )} + + + ); +}; diff --git a/packages/uikit/src/pages/browser/PromotedItem.tsx b/packages/uikit/src/pages/browser/PromotedItem.tsx new file mode 100644 index 000000000..3f445a5dc --- /dev/null +++ b/packages/uikit/src/pages/browser/PromotedItem.tsx @@ -0,0 +1,33 @@ +import styled from 'styled-components'; + +export const PromotedItem = styled.div` + padding-top: 0 !important; + padding-bottom: 0 !important; + height: 76px; + display: flex; + align-items: center; + width: 100%; +`; + +export const PromotedItemImage = styled.img` + height: 44px; + width: 44px; + border-radius: ${props => props.theme.cornerExtraSmall}; +`; + +export const PromotedItemText = styled.div<{ color?: string }>` + display: flex; + min-width: 0; + flex-direction: column; + padding: 11px 12px 13px; + color: ${props => props.color || props.theme.textPrimary}; + + & > span:nth-child(2) { + opacity: 0.78; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + max-height: 32px; +`; diff --git a/packages/uikit/src/pages/browser/PromotionsCarousel.tsx b/packages/uikit/src/pages/browser/PromotionsCarousel.tsx new file mode 100644 index 000000000..44ca529d0 --- /dev/null +++ b/packages/uikit/src/pages/browser/PromotionsCarousel.tsx @@ -0,0 +1,63 @@ +import { Body3, Label2 } from '../../components/Text'; +import { FC } from 'react'; +import { Carousel } from '../../components/shared'; +import styled from 'styled-components'; +import { PromotedItem, PromotedItemImage, PromotedItemText } from './PromotedItem'; +import { useOpenLinkOnAreaClick } from '../../hooks/useAreaClick'; +import { CarouselApp } from '@tonkeeper/core/dist/tonkeeperApi/tonendpoint'; +import { useAppContext } from '../../hooks/appContext'; + +const CarouselCard = styled.div<{ img: string }>` + width: 100%; + aspect-ratio: 2 / 1; + + background-image: ${props => `url(${props.img})`}; + background-size: cover; + border-radius: ${props => props.theme.cornerSmall}; + + display: inline-flex !important; + align-items: flex-end; + justify-content: flex-start; + cursor: pointer; +`; +const CarouselCardFooter = styled(PromotedItem)` + margin-left: 1rem; +`; + +export const PromotionsCarousel: FC<{ apps: CarouselApp[]; className?: string }> = ({ + apps, + className +}) => { + const { config } = useAppContext(); + const speed = config.featured_play_interval || 1000 * 10; + + return ( + + {apps.map(item => ( + + ))} + + ); +}; + +const CarouselItem: FC<{ item: CarouselApp }> = ({ item }) => { + const ref = useOpenLinkOnAreaClick(item.url); + + return ( + + + + + {item.name} + {item.description} + + + + ); +}; diff --git a/packages/uikit/src/pages/browser/index.tsx b/packages/uikit/src/pages/browser/index.tsx new file mode 100644 index 000000000..b5e824dd9 --- /dev/null +++ b/packages/uikit/src/pages/browser/index.tsx @@ -0,0 +1,16 @@ +import React, { FC } from 'react'; +import { BrowserRoute } from '../../libs/routes'; +import { Route, Routes } from 'react-router-dom'; +import { CategoryPage } from './CategoryPage'; +import { BrowserRecommendationsPage } from './BrowserRecommendationsPage'; + +const BrowserPage: FC = () => { + return ( + + } /> + } /> + + ); +}; + +export default BrowserPage; diff --git a/packages/uikit/src/state/tonendpoint.ts b/packages/uikit/src/state/tonendpoint.ts index 4db1fdeec..fcdcc1a9b 100644 --- a/packages/uikit/src/state/tonendpoint.ts +++ b/packages/uikit/src/state/tonendpoint.ts @@ -6,18 +6,23 @@ import { TonendpoinFiatItem, Tonendpoint, TonendpointConfig, - getFiatMethods, getServerConfig } from '@tonkeeper/core/dist/tonkeeperApi/tonendpoint'; import { useMemo } from 'react'; import { useAppContext } from '../hooks/appContext'; import { QueryKey, TonkeeperApiKey } from '../libs/queryKey'; import { useUserCountry } from './country'; +import { TargetEnv } from '@tonkeeper/core/dist/AppSdk'; -export const useTonendpoint = (build: string, network?: Network, lang?: Language) => { +export const useTonendpoint = ( + targetEnv: TargetEnv, + build: string, + network?: Network, + lang?: Language +) => { return useMemo(() => { - return new Tonendpoint({ build, network, lang: localizationText(lang) }, {}); - }, [build, network, lang]); + return new Tonendpoint({ build, network, lang: localizationText(lang), targetEnv }, {}); + }, [targetEnv, build, network, lang]); }; export const useTonenpointConfig = (tonendpoint: Tonendpoint) => { @@ -37,7 +42,7 @@ export const useTonendpointBuyMethods = () => { return useQuery( [QueryKey.tonkeeperApi, TonkeeperApiKey.fiat, tonendpoint.params.lang, countryCode], async () => { - const methods = await getFiatMethods(tonendpoint, countryCode); + const methods = await tonendpoint.getFiatMethods(countryCode); const buy = methods.categories[0]; const layout = methods.layoutByCountry.find(item => item.countryCode === countryCode); diff --git a/packages/uikit/tsconfig.json b/packages/uikit/tsconfig.json index d1efb6706..0425dedbc 100644 --- a/packages/uikit/tsconfig.json +++ b/packages/uikit/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "jsx": "react", + "jsx": "react-jsx", "target": "esnext", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, @@ -16,7 +16,7 @@ "isolatedModules": true, "noEmit": false, "outDir": "dist", - "declaration": true + "declaration": true, }, "include": ["src"], "exclude": ["node_modules", "**/stories"] diff --git a/yarn.lock b/yarn.lock index 69ee60997..627446253 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6213,7 +6213,9 @@ __metadata: "@types/react-beautiful-dnd": "npm:^13.1.3" "@types/react-dom": "npm:^18.0.9" "@types/react-lottie": "npm:^1.2.6" + "@types/react-slick": "npm:^0" "@types/react-transition-group": "npm:^4.4.5" + "@types/slick-carousel": "npm:^1" "@types/styled-components": "npm:^5.1.26" "@types/uuid": "npm:^9.0.1" babel-loader: "npm:^8.3.0" @@ -6228,8 +6230,10 @@ __metadata: react-lottie: "npm:^1.2.4" react-qrcode-logo: "npm:^2.9.0" react-router-dom: "npm:^6.4.5" + react-slick: "npm:^0.29.0" react-transition-group: "npm:^4.4.5" require-from-string: "npm:^2.0.2" + slick-carousel: "npm:^1.8.1" storybook-addon-turbo-build: "npm:^1.1.0" styled-components: "npm:^6.1.1" ton: "npm:^13.4.1" @@ -6766,6 +6770,15 @@ __metadata: languageName: node linkType: hard +"@types/jquery@npm:*": + version: 3.5.29 + resolution: "@types/jquery@npm:3.5.29" + dependencies: + "@types/sizzle": "npm:*" + checksum: 364facf2cb0bc935cb4b7f4af0b12c8e901b02b08a85af213172485166123b10aad8563f8e51e363f65a778c4105a59a64dedd0b0fd73f96557219d46477af56 + languageName: node + linkType: hard + "@types/json-schema@npm:*, @types/json-schema@npm:^7.0.4, @types/json-schema@npm:^7.0.5, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": version: 7.0.15 resolution: "@types/json-schema@npm:7.0.15" @@ -7015,6 +7028,15 @@ __metadata: languageName: node linkType: hard +"@types/react-slick@npm:^0": + version: 0.23.13 + resolution: "@types/react-slick@npm:0.23.13" + dependencies: + "@types/react": "npm:*" + checksum: 3e363220dd7efb7eb9d60aec80588365ddffb366cc75307b86ce83d84e6664d9a66edc9d796cc0b4f87bfd217fa912d4005a00c2a87c41b36fbe4412ae7a15a7 + languageName: node + linkType: hard + "@types/react-transition-group@npm:^4.4.5": version: 4.4.10 resolution: "@types/react-transition-group@npm:4.4.10" @@ -7104,6 +7126,22 @@ __metadata: languageName: node linkType: hard +"@types/sizzle@npm:*": + version: 2.3.8 + resolution: "@types/sizzle@npm:2.3.8" + checksum: 2ac62443dc917f5f903cbd9afc51c7d6cc1c6569b4e1a15faf04aea5b13b486e7f208650014c3dc4fed34653eded3e00fe5abffe0e6300cbf0e8a01beebf11a6 + languageName: node + linkType: hard + +"@types/slick-carousel@npm:^1": + version: 1.6.40 + resolution: "@types/slick-carousel@npm:1.6.40" + dependencies: + "@types/jquery": "npm:*" + checksum: 59b3127722c3ca2d1f504163b0384cfc853434edcbf4b06614d1b7564e921f4c824bfd4f71669f57b29780c6a2df1cb3e4806385275b78d917a52105d48576e5 + languageName: node + linkType: hard + "@types/sockjs@npm:^0.3.33": version: 0.3.36 resolution: "@types/sockjs@npm:0.3.36" @@ -10016,6 +10054,13 @@ __metadata: languageName: node linkType: hard +"classnames@npm:^2.2.5": + version: 2.5.1 + resolution: "classnames@npm:2.5.1" + checksum: 58eb394e8817021b153bb6e7d782cfb667e4ab390cb2e9dac2fc7c6b979d1cc2b2a733093955fc5c94aa79ef5c8c89f11ab77780894509be6afbb91dddd79d15 + languageName: node + linkType: hard + "clean-css@npm:^4.2.3": version: 4.2.4 resolution: "clean-css@npm:4.2.4" @@ -12089,6 +12134,13 @@ __metadata: languageName: node linkType: hard +"enquire.js@npm:^2.1.6": + version: 2.1.6 + resolution: "enquire.js@npm:2.1.6" + checksum: 246b4ec2cc7a4eb8e24e9ae836b3222b889d8d982ac1583f90f9641222610a688c8a3fab53e2dc6ee56457c2798ba487814f61f5553d30ae23cc74664e6f78f8 + languageName: node + linkType: hard + "entities@npm:^2.0.0": version: 2.2.0 resolution: "entities@npm:2.2.0" @@ -17262,6 +17314,15 @@ __metadata: languageName: node linkType: hard +"json2mq@npm:^0.2.0": + version: 0.2.0 + resolution: "json2mq@npm:0.2.0" + dependencies: + string-convert: "npm:^0.2.0" + checksum: 0ad2f6a268308beeaf3077652b5ae2b0701ef357840e1542cc838198424a79af21dad759595e2cce8cd9b154e0b0f758c217adea4b3dfbaafff3ff9bf82394a1 + languageName: node + linkType: hard + "json5@npm:^1.0.1, json5@npm:^1.0.2": version: 1.0.2 resolution: "json5@npm:1.0.2" @@ -21871,6 +21932,22 @@ __metadata: languageName: node linkType: hard +"react-slick@npm:^0.29.0": + version: 0.29.0 + resolution: "react-slick@npm:0.29.0" + dependencies: + classnames: "npm:^2.2.5" + enquire.js: "npm:^2.1.6" + json2mq: "npm:^0.2.0" + lodash.debounce: "npm:^4.0.8" + resize-observer-polyfill: "npm:^1.5.0" + peerDependencies: + react: ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 + checksum: b50a5a005c1bd62350c29c516be62a8c2a5e9ea1b5940b1bd5748e36b9f8378016990fdd7a38c9346bb8b51a59faf7522a0b365dc9ed4d0fc4d67c9356513e5f + languageName: node + linkType: hard + "react-transition-group@npm:^4.4.5": version: 4.4.5 resolution: "react-transition-group@npm:4.4.5" @@ -22368,6 +22445,13 @@ __metadata: languageName: node linkType: hard +"resize-observer-polyfill@npm:^1.5.0": + version: 1.5.1 + resolution: "resize-observer-polyfill@npm:1.5.1" + checksum: e10ee50cd6cf558001de5c6fb03fee15debd011c2f694564b71f81742eef03fb30d6c2596d1d5bf946d9991cb692fcef529b7bd2e4057041377ecc9636c753ce + languageName: node + linkType: hard + "resolve-alpn@npm:^1.0.0": version: 1.2.1 resolution: "resolve-alpn@npm:1.2.1" @@ -23355,6 +23439,15 @@ __metadata: languageName: node linkType: hard +"slick-carousel@npm:^1.8.1": + version: 1.8.1 + resolution: "slick-carousel@npm:1.8.1" + peerDependencies: + jquery: ">=1.8.0" + checksum: b383e6f96b3bec573522467cfeeac1fce22d1cd53a68f5699cd3d62f0167777116b925dc48fbe64979a7a830777e4440036b8a7d4575c9ff8d18c3dd141b1ea2 + languageName: node + linkType: hard + "smart-buffer@npm:^4.2.0": version: 4.2.0 resolution: "smart-buffer@npm:4.2.0" @@ -23879,6 +23972,13 @@ __metadata: languageName: node linkType: hard +"string-convert@npm:^0.2.0": + version: 0.2.1 + resolution: "string-convert@npm:0.2.1" + checksum: f3eb484a45d29aa2ba2d9fe0471c971d5a56353633b56a4c8bc3e67237a2cdb1b6437f006a67d489b3d41e0a1c1f02e18d334c161a27fd7219e4aee1a9f68aac + languageName: node + linkType: hard + "string-length@npm:^4.0.1": version: 4.0.2 resolution: "string-length@npm:4.0.2"