From ee21cc45671ce2e850a80821bad0d5f8b6844994 Mon Sep 17 00:00:00 2001 From: Natalia Stus Date: Tue, 23 Jun 2026 16:43:26 +0200 Subject: [PATCH] feature/tk-1518: twa stub --- .gitignore | 1 + apps/twa/dev.html | 26 ++ apps/twa/package.json | 1 + apps/twa/src/App.test.tsx | 8 - apps/twa/src/App.tsx | 393 +++------------- apps/twa/src/components/InitData.tsx | 35 -- .../src/components/ReceiveNotifications.tsx | 45 -- apps/twa/src/components/TwaQrScanner.tsx | 32 -- apps/twa/src/components/nft/NftIndexView.tsx | 21 - .../src/components/nft/NftNotification.tsx | 204 --------- .../src/components/swap/SwapNotification.tsx | 55 --- .../transfer/FavoriteNotification.tsx | 170 ------- .../transfer/SendNotificationButtons.tsx | 133 ------ .../transfer/SendNotificationHeader.tsx | 62 --- .../components/transfer/SendNotifications.tsx | 422 ----------------- apps/twa/src/dev/main.tsx | 165 +++++++ apps/twa/src/dev/seed.ts | 83 ++++ apps/twa/src/dev/storage.ts | 44 ++ apps/twa/src/i18n.ts | 29 +- apps/twa/src/libs/appSdk.ts | 20 +- apps/twa/src/libs/twaHooks.ts | 9 +- apps/twa/src/libs/twaNotification.ts | 115 ----- apps/twa/src/stub/AccountActionsSheet.tsx | 77 ++++ apps/twa/src/stub/MiniAppClosed.tsx | 239 ++++++++++ apps/twa/src/stub/RecoveryPhrase.tsx | 142 ++++++ apps/twa/src/stub/SignOutSheet.tsx | 84 ++++ apps/twa/src/twaApi/.openapi-generator-ignore | 23 - apps/twa/src/twaApi/.openapi-generator/FILES | 19 - .../twa/src/twaApi/.openapi-generator/VERSION | 1 - apps/twa/src/twaApi/apis/DefaultApi.ts | 406 ----------------- apps/twa/src/twaApi/apis/index.ts | 3 - apps/twa/src/twaApi/index.ts | 5 - ...ountEventsSubscriptionStatus200Response.ts | 66 --- .../AccountEventsSubscriptionStatusRequest.ts | 75 --- apps/twa/src/twaApi/models/Balance.ts | 66 --- .../src/twaApi/models/BridgeWebhookRequest.ts | 75 --- .../models/GetTonConnectPayload200Response.ts | 66 --- .../GetTonConnectPayloadDefaultResponse.ts | 66 --- apps/twa/src/twaApi/models/ModelError.ts | 66 --- .../models/SubscribeToAccountEventsRequest.ts | 91 ---- .../SubscribeToAccountEventsRequestProof.ts | 108 ----- ...scribeToAccountEventsRequestProofDomain.ts | 74 --- .../models/SubscribeToBridgeEventsRequest.ts | 84 ---- .../UnsubscribeFromAccountEventsRequest.ts | 66 --- .../UnsubscribeFromBridgeEventsRequest.ts | 74 --- apps/twa/src/twaApi/models/index.ts | 15 - apps/twa/src/twaApi/runtime.ts | 431 ------------------ apps/twa/task/locales.ts | 8 +- packages/core/src/AppSdk.ts | 3 - .../__tests__/mnemonicService.compat.test.ts | 68 +++ packages/core/vitest.config.mts | 14 + packages/locales/src/tonkeeper/ar.json | 16 +- packages/locales/src/tonkeeper/bg.json | 16 +- packages/locales/src/tonkeeper/bn.json | 16 +- packages/locales/src/tonkeeper/de.json | 16 +- packages/locales/src/tonkeeper/en.json | 16 +- packages/locales/src/tonkeeper/es.json | 16 +- packages/locales/src/tonkeeper/fa.json | 16 +- packages/locales/src/tonkeeper/fr.json | 16 +- packages/locales/src/tonkeeper/hi.json | 16 +- packages/locales/src/tonkeeper/id.json | 16 +- packages/locales/src/tonkeeper/it.json | 16 +- packages/locales/src/tonkeeper/pa.json | 16 +- packages/locales/src/tonkeeper/pt.json | 16 +- packages/locales/src/tonkeeper/ru-RU.json | 16 +- packages/locales/src/tonkeeper/tr-TR.json | 16 +- packages/locales/src/tonkeeper/uk.json | 16 +- packages/locales/src/tonkeeper/uz.json | 16 +- packages/locales/src/tonkeeper/vi.json | 16 +- .../locales/src/tonkeeper/zh-Hans-CN.json | 16 +- packages/locales/src/tonkeeper/zh-Hant.json | 16 +- packages/uikit/src/components/Body.tsx | 2 - .../connect/TonConnectNotification.tsx | 8 +- .../connect/TonTransactionNotification.tsx | 7 - .../uikit/src/components/create/Words.tsx | 7 - .../uikit/src/components/home/Actions.tsx | 5 - .../staking/StakingTransactionModal.tsx | 7 - .../swap/SwapConfirmationNotification.tsx | 6 - .../uikit/src/pages/import/Initialize.tsx | 1 - 79 files changed, 1343 insertions(+), 3608 deletions(-) create mode 100644 apps/twa/dev.html delete mode 100644 apps/twa/src/App.test.tsx delete mode 100644 apps/twa/src/components/InitData.tsx delete mode 100644 apps/twa/src/components/ReceiveNotifications.tsx delete mode 100644 apps/twa/src/components/TwaQrScanner.tsx delete mode 100644 apps/twa/src/components/nft/NftIndexView.tsx delete mode 100644 apps/twa/src/components/nft/NftNotification.tsx delete mode 100644 apps/twa/src/components/swap/SwapNotification.tsx delete mode 100644 apps/twa/src/components/transfer/FavoriteNotification.tsx delete mode 100644 apps/twa/src/components/transfer/SendNotificationButtons.tsx delete mode 100644 apps/twa/src/components/transfer/SendNotificationHeader.tsx delete mode 100644 apps/twa/src/components/transfer/SendNotifications.tsx create mode 100644 apps/twa/src/dev/main.tsx create mode 100644 apps/twa/src/dev/seed.ts create mode 100644 apps/twa/src/dev/storage.ts delete mode 100644 apps/twa/src/libs/twaNotification.ts create mode 100644 apps/twa/src/stub/AccountActionsSheet.tsx create mode 100644 apps/twa/src/stub/MiniAppClosed.tsx create mode 100644 apps/twa/src/stub/RecoveryPhrase.tsx create mode 100644 apps/twa/src/stub/SignOutSheet.tsx delete mode 100644 apps/twa/src/twaApi/.openapi-generator-ignore delete mode 100644 apps/twa/src/twaApi/.openapi-generator/FILES delete mode 100644 apps/twa/src/twaApi/.openapi-generator/VERSION delete mode 100644 apps/twa/src/twaApi/apis/DefaultApi.ts delete mode 100644 apps/twa/src/twaApi/apis/index.ts delete mode 100644 apps/twa/src/twaApi/index.ts delete mode 100644 apps/twa/src/twaApi/models/AccountEventsSubscriptionStatus200Response.ts delete mode 100644 apps/twa/src/twaApi/models/AccountEventsSubscriptionStatusRequest.ts delete mode 100644 apps/twa/src/twaApi/models/Balance.ts delete mode 100644 apps/twa/src/twaApi/models/BridgeWebhookRequest.ts delete mode 100644 apps/twa/src/twaApi/models/GetTonConnectPayload200Response.ts delete mode 100644 apps/twa/src/twaApi/models/GetTonConnectPayloadDefaultResponse.ts delete mode 100644 apps/twa/src/twaApi/models/ModelError.ts delete mode 100644 apps/twa/src/twaApi/models/SubscribeToAccountEventsRequest.ts delete mode 100644 apps/twa/src/twaApi/models/SubscribeToAccountEventsRequestProof.ts delete mode 100644 apps/twa/src/twaApi/models/SubscribeToAccountEventsRequestProofDomain.ts delete mode 100644 apps/twa/src/twaApi/models/SubscribeToBridgeEventsRequest.ts delete mode 100644 apps/twa/src/twaApi/models/UnsubscribeFromAccountEventsRequest.ts delete mode 100644 apps/twa/src/twaApi/models/UnsubscribeFromBridgeEventsRequest.ts delete mode 100644 apps/twa/src/twaApi/models/index.ts delete mode 100644 apps/twa/src/twaApi/runtime.ts create mode 100644 packages/core/src/service/__tests__/mnemonicService.compat.test.ts create mode 100644 packages/core/vitest.config.mts diff --git a/.gitignore b/.gitignore index 908755290..fac88aa64 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,4 @@ Thumbs.db package-lock.json +.playwright-mcp/ diff --git a/apps/twa/dev.html b/apps/twa/dev.html new file mode 100644 index 000000000..353ef45d3 --- /dev/null +++ b/apps/twa/dev.html @@ -0,0 +1,26 @@ + + + + + Tonkeeper TWA — local preview + + + + + + + +
+
+
+ + + diff --git a/apps/twa/package.json b/apps/twa/package.json index 5328d1745..067e3fc6b 100644 --- a/apps/twa/package.json +++ b/apps/twa/package.json @@ -47,6 +47,7 @@ "vite": "^5.4.20" }, "scripts": { + "dev": "vite", "locales": "ts-node ./task/locales", "build": "tsc && yarn locales && vite build", "generate:twaApi": "rm -fr src/twaApi && docker build -f task/Dockerfile.twaApi . -t twaapisdk && docker run --rm --user=$(id -u):$(id -g) -v \"$PWD\":/local twaapisdk", diff --git a/apps/twa/src/App.test.tsx b/apps/twa/src/App.test.tsx deleted file mode 100644 index b7ee179a5..000000000 --- a/apps/twa/src/App.test.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import { App } from './App'; - -test('renders learn react link', () => { - render(); - const linkElement = screen.getByText(/Wallet/i); - expect(linkElement).toBeInTheDocument(); -}); diff --git a/apps/twa/src/App.tsx b/apps/twa/src/App.tsx index a7f734eb0..a3b9950a8 100644 --- a/apps/twa/src/App.tsx +++ b/apps/twa/src/App.tsx @@ -1,23 +1,11 @@ import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query'; -import { Account } from '@tonkeeper/core/dist/entries/account'; -import { getApiConfig, Network } from '@tonkeeper/core/dist/entries/network'; +import { localizationText } from '@tonkeeper/core/dist/entries/language'; +import { getApiConfig } from '@tonkeeper/core/dist/entries/network'; import { WalletVersion } from '@tonkeeper/core/dist/entries/wallet'; -import { InnerBody, useWindowsScroll } from '@tonkeeper/uikit/dist/components/Body'; import { CopyNotification } from '@tonkeeper/uikit/dist/components/CopyNotification'; -import { Footer, FooterGlobalStyle } from '@tonkeeper/uikit/dist/components/Footer'; -import { Header, HeaderGlobalStyle } from '@tonkeeper/uikit/dist/components/Header'; import { DarkThemeContext } from '@tonkeeper/uikit/dist/components/Icon'; import { GlobalListStyle } from '@tonkeeper/uikit/dist/components/List'; import { Loading } from '@tonkeeper/uikit/dist/components/Loading'; -import MemoryScroll from '@tonkeeper/uikit/dist/components/MemoryScroll'; -import { - ActivitySkeletonPage, - BrowserSkeletonPage, - CoinSkeletonPage, - HomeSkeleton, - SettingsSkeletonPage -} from '@tonkeeper/uikit/dist/components/Skeleton'; -import { SybHeaderGlobalStyle } from '@tonkeeper/uikit/dist/components/SubHeader'; import { AppContext, IAppContext } from '@tonkeeper/uikit/dist/hooks/appContext'; import { AppSdkContext } from '@tonkeeper/uikit/dist/hooks/appSdk'; import { StorageContext } from '@tonkeeper/uikit/dist/hooks/storage'; @@ -26,64 +14,25 @@ import { TranslationContext, useTWithReplaces } from '@tonkeeper/uikit/dist/hooks/translation'; -import { AppRoute } from '@tonkeeper/uikit/dist/libs/routes'; -import { Unlock } from '@tonkeeper/uikit/dist/pages/home/Unlock'; - -import { Platform as TwaPlatform, initViewport } from '@tma.js/sdk'; -import { SDKProvider } from '@tma.js/sdk-react'; -import { ModalsRoot } from '@tonkeeper/uikit/dist/components/ModalsRoot'; -import { useTrackLocation } from '@tonkeeper/uikit/dist/hooks/analytics'; -import { useLock } from '@tonkeeper/uikit/dist/hooks/lock'; -import { useDebuggingTools } from '@tonkeeper/uikit/dist/hooks/useDebuggingTools'; import { UnlockNotification } from '@tonkeeper/uikit/dist/pages/home/UnlockNotification'; -import { useDevSettings } from '@tonkeeper/uikit/dist/state/dev'; import { useUserFiatQuery } from '@tonkeeper/uikit/dist/state/fiat'; import { useUserLanguage } from '@tonkeeper/uikit/dist/state/language'; -import { useSwapMobileNotification } from '@tonkeeper/uikit/dist/state/swap/useSwapMobileNotification'; import { useTonendpoint, useTonenpointConfig } from '@tonkeeper/uikit/dist/state/tonendpoint'; -import { - useAccountsStateQuery, - useActiveAccountQuery, - useActiveTonNetwork -} from '@tonkeeper/uikit/dist/state/wallet'; +import { useActiveTonNetwork } from '@tonkeeper/uikit/dist/state/wallet'; import { defaultTheme } from '@tonkeeper/uikit/dist/styles/defaultTheme'; -import { Container, GlobalStyle } from '@tonkeeper/uikit/dist/styles/globalStyle'; +import { GlobalStyle } from '@tonkeeper/uikit/dist/styles/globalStyle'; import { lightTheme } from '@tonkeeper/uikit/dist/styles/lightTheme'; -import React, { FC, PropsWithChildren, Suspense, useEffect, useMemo } from 'react'; + +import { initViewport } from '@tma.js/sdk'; +import { SDKProvider } from '@tma.js/sdk-react'; +import { FC, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { BrowserRouter, Route, Switch, useLocation } from "react-router-dom"; -import styled, { ThemeProvider } from 'styled-components'; +import { BrowserRouter } from 'react-router-dom'; +import { ThemeProvider } from 'styled-components'; import StandardErrorBoundary from './components/ErrorBoundary'; -import { InitDataLogger } from './components/InitData'; -import { TwaReceiveNotification } from './components/ReceiveNotifications'; -import { TwaQrScanner } from './components/TwaQrScanner'; -import { TwaNftNotification } from './components/nft/NftNotification'; -import { SwapScreen } from './components/swap/SwapNotification'; -import { TwaSendNotification } from './components/transfer/SendNotifications'; import { TwaAppSdk } from './libs/appSdk'; -import { useAnalytics, useTwaAppViewport } from './libs/hooks'; -import { useGlobalPreferencesQuery } from '@tonkeeper/uikit/dist/state/global-preferences'; -import { useGlobalSetup } from '@tonkeeper/uikit/dist/state/globalSetup'; -import { useNavigate } from "@tonkeeper/uikit/dist/hooks/router/useNavigate"; -import { useRealtimeUpdatesInvalidation } from '@tonkeeper/uikit/dist/hooks/realtime'; -import { RedirectFromDesktopSettings } from "@tonkeeper/uikit/dist/pages/settings/RedirectFromDesktopSettings"; - -const Initialize = React.lazy(() => 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')); -const Coin = React.lazy(() => import('@tonkeeper/uikit/dist/pages/coin/Coin')); -const WebTonConnectSubscription = React.lazy( - () => import('@tonkeeper/uikit/dist/components/connect/WebTonConnectSubscription') -); -const PairSignerNotification = React.lazy( - () => import('@tonkeeper/uikit/dist/components/PairSignerNotification') -); -const PairKeystoneNotification = React.lazy( - () => import('@tonkeeper/uikit/dist/components/PairKeystoneNotification') -); +import { useTwaAppViewport } from './libs/hooks'; +import { MiniAppClosed } from './stub/MiniAppClosed'; const queryClient = new QueryClient({ defaultOptions: { @@ -94,8 +43,6 @@ const queryClient = new QueryClient({ } }); -const TARGET_ENV = 'twa'; - export const App = () => { return ( @@ -115,7 +62,7 @@ const TwaLoader = () => { }); useEffect(() => { - if (!sdk) return undefined; + if (!sdk) return; const theme = sdk.miniApp.isDark ? defaultTheme : lightTheme; @@ -126,9 +73,6 @@ const TwaLoader = () => { sdk.miniApp.setHeaderColor(theme.backgroundPage); } - sdk.mainButton.setBgColor(theme.buttonPrimaryBackground); - sdk.mainButton.setTextColor(theme.buttonPrimaryForeground); - document.body.style.backgroundColor = theme.backgroundPage; }, [sdk]); @@ -136,8 +80,8 @@ const TwaLoader = () => { return
{error.message}
; } - if (!sdk || sdk == null) { - return
; + if (!sdk) { + return
; } return ( @@ -145,32 +89,20 @@ const TwaLoader = () => { - + + ); }; -const getUsePadding = (platform: TwaPlatform): boolean => { - switch (platform) { - case 'ios': - return true; - case 'android': - case 'android_x': - return false; - default: - return false; - } -}; - -const TwaApp: FC<{ sdk: TwaAppSdk }> = ({ sdk }) => { +const StubApp: FC<{ sdk: TwaAppSdk }> = ({ sdk }) => { const { t: tSimple, i18n } = useTranslation(); - const t = useTWithReplaces(tSimple); - const translation = useMemo(() => { - const client: I18nContext = { + const translation = useMemo( + () => ({ t, i18n: { enable: false, @@ -179,64 +111,39 @@ const TwaApp: FC<{ sdk: TwaAppSdk }> = ({ sdk }) => { language: i18n.language, languages: [] } - }; - return client; - }, [t, i18n]); + }), + [t, i18n] + ); return ( - - - - - - - + ); }; -const FullSizeWrapper = styled(Container)``; - -const Wrapper = styled(FullSizeWrapper)<{ standalone: boolean }>` - height: var(--app-height); - transition: height 0.4s ease; - - box-sizing: border-box; - padding-top: 64px; - padding-bottom: ${props => (props.standalone ? '96' : '80')}px; -`; - -const seeIfShowQrScanner = (platform: TwaPlatform): boolean => { - switch (platform) { - case 'ios': - case 'android': - case 'android_x': - return true; - default: - return false; - } -}; - -export const Loader: FC<{ sdk: TwaAppSdk }> = ({ sdk }) => { - const { data: activeAccount, isLoading: activeWalletLoading } = useActiveAccountQuery(); - const { data: accounts, isLoading: isWalletsLoading } = useAccountsStateQuery(); +const Loader: FC<{ sdk: TwaAppSdk }> = ({ sdk }) => { const { data: lang, isLoading: isLangLoading } = useUserLanguage(); const { data: fiat } = useUserFiatQuery(); - const { data: devSettings } = useDevSettings(); - const { isLoading: globalPreferencesLoading } = useGlobalPreferencesQuery(); - const { isLoading: globalSetupLoading } = useGlobalSetup(); - - const lock = useLock(sdk); const network = useActiveTonNetwork(); + const { i18n } = useTranslation(); + + useTwaAppViewport(false, sdk); + + // Apply the user's stored language (set in the old app) to i18next so the + // stub renders localized; falls back to English when unset/unsupported. + useEffect(() => { + if (lang && i18n.language !== localizationText(lang)) { + i18n.reloadResources([localizationText(lang)]).then(() => + i18n.changeLanguage(localizationText(lang)) + ); + } + }, [lang, i18n]); const tonendpoint = useTonendpoint({ build: sdk.version, @@ -246,208 +153,42 @@ export const Loader: FC<{ sdk: TwaAppSdk }> = ({ sdk }) => { }); const { data: serverConfig } = useTonenpointConfig(tonendpoint); - const { data: tracker } = useAnalytics( - activeAccount || undefined, - accounts, - network, - sdk.version - ); + const context = useMemo(() => { + if (!serverConfig || !fiat) { + return undefined; + } + return { + mainnetApi: getApiConfig(serverConfig.mainnetConfig), + testnetApi: getApiConfig(serverConfig.testnetConfig), + fiat, + mainnetConfig: serverConfig.mainnetConfig, + testnetConfig: serverConfig.testnetConfig, + tonendpoint, + standalone: true, + extension: false, + ios: true, + proFeatures: false, + hideLedger: true, + hideSigner: true, + hideKeystone: true, + hideQrScanner: true, + hideMam: true, + hideMultisig: true, + hideFireblocks: true, + defaultWalletVersion: WalletVersion.V5R1, + browserLength: 4, + tracker: undefined + }; + }, [serverConfig, fiat, tonendpoint]); - if ( - isWalletsLoading || - activeWalletLoading || - isLangLoading || - serverConfig === undefined || - lock === undefined || - fiat === undefined || - !devSettings || - globalPreferencesLoading || - globalSetupLoading - ) { + if (isLangLoading || !context) { return ; } - const showQrScan = seeIfShowQrScanner(sdk.launchParams.platform); - - const context: IAppContext = { - mainnetApi: getApiConfig(serverConfig.mainnetConfig), - testnetApi: getApiConfig(serverConfig.testnetConfig), - fiat, - mainnetConfig: serverConfig.mainnetConfig, - testnetConfig: serverConfig.testnetConfig, - tonendpoint, - standalone: true, - extension: false, - ios: true, - proFeatures: false, - hideLedger: true, - hideSigner: !showQrScan, - hideKeystone: !showQrScan, - hideQrScanner: !showQrScan, - hideMam: true, - hideMultisig: true, - hideFireblocks: true, - defaultWalletVersion: WalletVersion.V5R1, - browserLength: 4, - tracker: tracker?.track - }; - return ( - + - - {showQrScan && } ); }; - -const InitWrapper = styled(Container)` - height: var(--app-height); - min-height: auto; - - transition: height 0.4s ease; - - overflow: auto; - display: flex; - flex-direction: column; - padding: 16px; - box-sizing: border-box; - position: relative; -`; - -const InitPages: FC<{ sdk: TwaAppSdk }> = ({ sdk }) => { - useTwaAppViewport(true, sdk); - return ( - - }> - - - - ); -}; - -const Content: FC<{ - sdk: TwaAppSdk; - activeAccount?: Account | null; - lock: boolean; - showQrScan: boolean; -}> = ({ activeAccount, lock, showQrScan, sdk }) => { - const location = useLocation(); - useWindowsScroll(); - useTrackLocation(); - useDebuggingTools(); - useRealtimeUpdatesInvalidation(); - - if (lock) { - return ( - - - - ); - } - - if (!activeAccount || location.pathname.startsWith(AppRoute.import)) { - return ; - } - - return ( - <> - - - - - - - - - - - - ); -}; - -const TwaNotification: FC = ({ children }) => { - return ( - - - {children} - - - ); -}; - -const MainPages: FC<{ showQrScan: boolean; sdk: TwaAppSdk }> = ({ showQrScan, sdk }) => { - useTwaAppViewport(false, sdk); - - const [isOpen] = useSwapMobileNotification(); - const navigate = useNavigate(); - - useEffect(() => { - if (isOpen) { - navigate(AppRoute.swap); - } - }, [isOpen]); - - return ( - - - - - }> - - - - - }> - - - - - }> - - - - - - - - }> - - - - - <> -
- - }> - - - - - - -