diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml
index e14679728..958fae35b 100644
--- a/.github/workflows/cd.yaml
+++ b/.github/workflows/cd.yaml
@@ -95,6 +95,12 @@ jobs:
environment: 'main'
secrets: inherit
+ swap-widget-build:
+ uses: ./.github/workflows/swap-widget-build.yaml
+ with:
+ environment: 'main'
+ secrets: inherit
+
web-tests:
needs: web-build
uses: ./.github/workflows/web-tests.yaml
diff --git a/.github/workflows/deploy-swap-widget.yaml b/.github/workflows/deploy-swap-widget.yaml
new file mode 100644
index 000000000..e7b7aca13
--- /dev/null
+++ b/.github/workflows/deploy-swap-widget.yaml
@@ -0,0 +1,18 @@
+name: Tonkeeper Swap Widget Deploy
+on:
+ workflow_dispatch:
+ inputs:
+ environment:
+ type: choice
+ description: Deploy to env
+ required: true
+ options:
+ - dev
+ - main
+
+jobs:
+ swap-widget:
+ uses: ./.github/workflows/swap-widget-build.yaml
+ with:
+ environment: ${{ inputs.environment }}
+ secrets: inherit
diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml
index 965bbb454..c9eee8e68 100644
--- a/.github/workflows/pull-request.yaml
+++ b/.github/workflows/pull-request.yaml
@@ -1,4 +1,4 @@
-name: Tonkeeper Pull-Request
+name: PR
on:
workflow_dispatch:
pull_request:
@@ -263,6 +263,12 @@ jobs:
uses: ./.github/workflows/ipad-build.yaml
secrets: inherit
+ swap-widget-build:
+ uses: ./.github/workflows/swap-widget-build.yaml
+ with:
+ environment: ${{ github.head_ref }}
+ secrets: inherit
+
web-tests:
needs: web-build
uses: ./.github/workflows/web-tests.yaml
diff --git a/.github/workflows/swap-widget-build.yaml b/.github/workflows/swap-widget-build.yaml
new file mode 100644
index 000000000..8153164a3
--- /dev/null
+++ b/.github/workflows/swap-widget-build.yaml
@@ -0,0 +1,103 @@
+name: Tonkeeper Swap Widget Build
+on:
+ workflow_call:
+ inputs:
+ environment:
+ required: true
+ type: string
+ secrets:
+ REACT_APP_AMPLITUDE_EXTENSION:
+ required: true
+ REACT_APP_MEASUREMENT_ID:
+ required: true
+ VITE_APP_APTABASE:
+ required: true
+ REACT_APP_TG_BOT_ID:
+ required: true
+ REACT_APP_STONFI_REFERRAL_ADDRESS:
+ required: true
+ CLOUDFLARE_API_TOKEN:
+ required: true
+ CLOUDFLARE_ACCOUNT_ID:
+ required: true
+ outputs:
+ deployment-url:
+ description: 'The app deployment url'
+ value: ${{ jobs.web-build.outputs.deployment-url }}
+env:
+ node-version: 20.11.1
+
+jobs:
+ swap-widget:
+ name: Swap Widget
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+
+ outputs:
+ deployment-url: ${{ steps.deploy.outputs.deployment-url }}
+ steps:
+ - name: Checkout to git repository
+ uses: actions/checkout@v4
+
+ - name: Set up Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ env.node-version }}
+
+ - name: Enable Corepack
+ run: |
+ corepack enable
+
+ - name: Yarn cache
+ uses: actions/cache@v4
+ with:
+ path: ./.yarn
+ key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-yarn-
+
+ - name: Run install
+ uses: borales/actions-yarn@v5
+ with:
+ cmd: install
+
+ - name: Run build
+ uses: borales/actions-yarn@v5
+ env:
+ VITE_APP_MEASUREMENT_ID: ${{ secrets.REACT_APP_MEASUREMENT_ID }}
+ VITE_APP_APTABASE: ${{ secrets.VITE_APP_APTABASE }}
+ VITE_APP_APTABASE_HOST: https://anonymous-analytics.tonkeeper.com
+ VITE_APP_LOCALES: en,zh_TW,zh_CN,id,ru,it,es,uk,tr,bg,uz,bn
+ VITE_APP_TONCONSOLE_HOST: https://pro.tonconsole.com
+ VITE_APP_TG_BOT_ID: ${{ secrets.REACT_APP_TG_BOT_ID }}
+ VITE_APP_STONFI_REFERRAL_ADDRESS: UQCV6ZyNxqQ4Um30lhk2_1EgnzB6KMN8bHgxDOFAq3irZfgx
+ with:
+ cmd: build:swap-widget
+
+ - name: Publish to Cloudflare Pages
+ id: deploy
+ uses: cloudflare/wrangler-action@v3
+ with:
+ apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+ accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+ command:
+ pages deploy apps/web-swap-widget/dist --project-name=tonkeeper-swap-widget
+ --branch=${{ inputs.environment }}
+
+ - name: Summary
+ run: |
+ echo '### Successful swap widget deployment 🚀🚀🚀' >> $GITHUB_STEP_SUMMARY
+ echo 'Well done!' >> $GITHUB_STEP_SUMMARY
+ echo 'Link to test environment:' >> $GITHUB_STEP_SUMMARY
+ echo '${{ steps.deploy.outputs.deployment-url }}' >> $GITHUB_STEP_SUMMARY
+
+ - name: Comment PR
+ uses: thollander/actions-comment-pull-request@v3
+ if: github.event_name == 'pull_request'
+ with:
+ message: |
+ ### Successful swap widget deployment 🚀🚀🚀
+ Well done!
+ Link to test environment:
+ ${{ steps.deploy.outputs.deployment-url }}
+ comment-tag: swap_widget_deploy
diff --git a/apps/web-swap-widget/.eslintignore b/apps/web-swap-widget/.eslintignore
new file mode 100644
index 000000000..e69de29bb
diff --git a/apps/web-swap-widget/.eslintrc.js b/apps/web-swap-widget/.eslintrc.js
new file mode 100644
index 000000000..e331ffd24
--- /dev/null
+++ b/apps/web-swap-widget/.eslintrc.js
@@ -0,0 +1,3 @@
+module.exports = {
+ extends: ['../../.eslintrc.js']
+};
diff --git a/apps/web-swap-widget/.gitignore b/apps/web-swap-widget/.gitignore
new file mode 100644
index 000000000..7c6c32d78
--- /dev/null
+++ b/apps/web-swap-widget/.gitignore
@@ -0,0 +1,25 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+public/locales
\ No newline at end of file
diff --git a/apps/web-swap-widget/README.md b/apps/web-swap-widget/README.md
new file mode 100644
index 000000000..b87cb0044
--- /dev/null
+++ b/apps/web-swap-widget/README.md
@@ -0,0 +1,46 @@
+# Getting Started with Create React App
+
+This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
+
+## Available Scripts
+
+In the project directory, you can run:
+
+### `npm start`
+
+Runs the app in the development mode.\
+Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
+
+The page will reload if you make edits.\
+You will also see any lint errors in the console.
+
+### `npm test`
+
+Launches the test runner in the interactive watch mode.\
+See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
+
+### `npm run build`
+
+Builds the app for production to the `build` folder.\
+It correctly bundles React in production mode and optimizes the build for the best performance.
+
+The build is minified and the filenames include the hashes.\
+Your app is ready to be deployed!
+
+See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
+
+### `npm run eject`
+
+**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
+
+If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
+
+Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
+
+You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
+
+## Learn More
+
+You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
+
+To learn React, check out the [React documentation](https://reactjs.org/).
diff --git a/apps/web-swap-widget/index.html b/apps/web-swap-widget/index.html
new file mode 100644
index 000000000..36dd28217
--- /dev/null
+++ b/apps/web-swap-widget/index.html
@@ -0,0 +1,20 @@
+
+
+
+
+ Tonkeeper Swap
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-swap-widget/package.json b/apps/web-swap-widget/package.json
new file mode 100644
index 000000000..838189601
--- /dev/null
+++ b/apps/web-swap-widget/package.json
@@ -0,0 +1,66 @@
+{
+ "name": "@tonkeeper/web-swap-widget",
+ "version": "3.0.0",
+ "author": "Ton APPS UK Limited ",
+ "description": "Web swap widget for Tonkeeper",
+ "dependencies": {
+ "@amplitude/analytics-browser": "^2.1.0",
+ "@aptabase/web": "^0.4.2",
+ "@tanstack/react-query": "4.3.4",
+ "@ton/core": "^0.56.0",
+ "@tonkeeper/core": "0.1.0",
+ "@tonkeeper/locales": "0.1.0",
+ "@tonkeeper/uikit": "0.1.0",
+ "buffer": "^6.0.3",
+ "copy-to-clipboard": "^3.3.3",
+ "i18next": "^22.1.4",
+ "i18next-browser-languagedetector": "^7.0.2",
+ "i18next-http-backend": "^2.0.2",
+ "process": "^0.11.10",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-i18next": "^12.1.1",
+ "styled-components": "^6.1.1"
+ },
+ "devDependencies": {
+ "@testing-library/dom": "^9.3.1",
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^13.5.0",
+ "@types/fs-extra": "^11.0.4",
+ "@types/jest": "^27.5.2",
+ "@types/node": "^20.11.0",
+ "@types/react": "^18.0.26",
+ "@types/react-dom": "^18.0.9",
+ "@types/styled-components": "^5.1.26",
+ "@vitejs/plugin-react": "^4.2.1",
+ "fs-extra": "^11.2.0",
+ "react-is": "^18.2.0",
+ "ts-node": "^10.9.1",
+ "typescript": "5.2.2",
+ "vite": "^5.0.11",
+ "vite-plugin-node-polyfills": "0.17.0"
+ },
+ "scripts": {
+ "locales": "ts-node ./task/locales",
+ "start": "yarn locales && vite dev",
+ "preview": "vite preview",
+ "build": "tsc && vite build && yarn locales",
+ "build:swap-widget": "yarn build"
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ },
+ "ts-standard": {
+ "project": "./tsconfig.json"
+ }
+}
diff --git a/apps/web-swap-widget/public/favicon.png b/apps/web-swap-widget/public/favicon.png
new file mode 100644
index 000000000..fa258c92e
Binary files /dev/null and b/apps/web-swap-widget/public/favicon.png differ
diff --git a/apps/web-swap-widget/public/img/pro/dashboard.webp b/apps/web-swap-widget/public/img/pro/dashboard.webp
new file mode 100644
index 000000000..ae3b91cd1
Binary files /dev/null and b/apps/web-swap-widget/public/img/pro/dashboard.webp differ
diff --git a/apps/web-swap-widget/public/img/pro/multisend.webp b/apps/web-swap-widget/public/img/pro/multisend.webp
new file mode 100644
index 000000000..96042c2f7
Binary files /dev/null and b/apps/web-swap-widget/public/img/pro/multisend.webp differ
diff --git a/apps/web-swap-widget/public/img/swap/dedust.png b/apps/web-swap-widget/public/img/swap/dedust.png
new file mode 100644
index 000000000..e51d630e9
Binary files /dev/null and b/apps/web-swap-widget/public/img/swap/dedust.png differ
diff --git a/apps/web-swap-widget/public/img/swap/stonfi.png b/apps/web-swap-widget/public/img/swap/stonfi.png
new file mode 100644
index 000000000..89519d09e
Binary files /dev/null and b/apps/web-swap-widget/public/img/swap/stonfi.png differ
diff --git a/apps/web-swap-widget/public/img/toncoin.svg b/apps/web-swap-widget/public/img/toncoin.svg
new file mode 100644
index 000000000..34fe73172
--- /dev/null
+++ b/apps/web-swap-widget/public/img/toncoin.svg
@@ -0,0 +1,4 @@
+
diff --git a/apps/web-swap-widget/public/img/usdt.svg b/apps/web-swap-widget/public/img/usdt.svg
new file mode 100644
index 000000000..3c7aa98e6
--- /dev/null
+++ b/apps/web-swap-widget/public/img/usdt.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/apps/web-swap-widget/public/logo-128x128.png b/apps/web-swap-widget/public/logo-128x128.png
new file mode 100644
index 000000000..59d440d36
Binary files /dev/null and b/apps/web-swap-widget/public/logo-128x128.png differ
diff --git a/apps/web-swap-widget/public/logo-16x16.png b/apps/web-swap-widget/public/logo-16x16.png
new file mode 100644
index 000000000..3c4e7dd21
Binary files /dev/null and b/apps/web-swap-widget/public/logo-16x16.png differ
diff --git a/apps/web-swap-widget/public/logo-64x64.png b/apps/web-swap-widget/public/logo-64x64.png
new file mode 100644
index 000000000..9048edc70
Binary files /dev/null and b/apps/web-swap-widget/public/logo-64x64.png differ
diff --git a/apps/web-swap-widget/public/manifest.json b/apps/web-swap-widget/public/manifest.json
new file mode 100644
index 000000000..d2033a3f1
--- /dev/null
+++ b/apps/web-swap-widget/public/manifest.json
@@ -0,0 +1,15 @@
+{
+ "short_name": "Tonkeeper",
+ "name": "Tonkeeper Web",
+ "icons": [
+ {
+ "src": "favicon.png",
+ "type": "image/png",
+ "sizes": "32x32"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/apps/web-swap-widget/public/robots.txt b/apps/web-swap-widget/public/robots.txt
new file mode 100644
index 000000000..e9e57dc4d
--- /dev/null
+++ b/apps/web-swap-widget/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/apps/web-swap-widget/src/App.tsx b/apps/web-swap-widget/src/App.tsx
new file mode 100644
index 000000000..1a32849cc
--- /dev/null
+++ b/apps/web-swap-widget/src/App.tsx
@@ -0,0 +1,251 @@
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { localizationText } from '@tonkeeper/core/dist/entries/language';
+import { getApiConfig, Network } from '@tonkeeper/core/dist/entries/network';
+import { WalletVersion } from '@tonkeeper/core/dist/entries/wallet';
+import { CopyNotification } from '@tonkeeper/uikit/dist/components/CopyNotification';
+import { DarkThemeContext } from '@tonkeeper/uikit/dist/components/Icon';
+import { AmplitudeAnalyticsContext, useTrackLocation } from '@tonkeeper/uikit/dist/hooks/amplitude';
+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';
+import {
+ I18nContext,
+ TranslationContext,
+ useTWithReplaces
+} from '@tonkeeper/uikit/dist/hooks/translation';
+import { UserThemeProvider } from '@tonkeeper/uikit/dist/providers/UserThemeProvider';
+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 { useProBackupState } from '@tonkeeper/uikit/dist/state/pro';
+import { useTonendpoint, useTonenpointConfig } from '@tonkeeper/uikit/dist/state/tonendpoint';
+import { useAccountsStateQuery, useActiveAccountQuery } from '@tonkeeper/uikit/dist/state/wallet';
+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 { useAnalytics, useAppHeight, useApplyQueryParams, useAppWidth } from './libs/hooks';
+import { useGlobalPreferencesQuery } from '@tonkeeper/uikit/dist/state/global-preferences';
+import { useGlobalSetup } from '@tonkeeper/uikit/dist/state/globalSetup';
+import { useWindowsScroll } from '@tonkeeper/uikit/dist/components/Body';
+import { useKeyboardHeight } from '@tonkeeper/uikit/dist/pages/import/hooks';
+import { useDebuggingTools } from '@tonkeeper/uikit/dist/hooks/useDebuggingTools';
+import styled, { createGlobalStyle } from 'styled-components';
+import { SwapWidgetPage } from './components/SwapWidgetPage';
+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';
+
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ staleTime: 30000,
+ refetchOnWindowFocus: false
+ }
+ }
+});
+
+const sdk = new BrowserAppSdk();
+const TARGET_ENV = 'swap-widget-web';
+
+export const App: FC = () => {
+ const { t: tSimple, i18n } = useTranslation();
+
+ const t = useTWithReplaces(tSimple);
+
+ const translation = useMemo(() => {
+ const languages = (import.meta.env.VITE_APP_LOCALES ?? 'en').split(',');
+ const client: I18nContext = {
+ t,
+ i18n: {
+ enable: true,
+ reloadResources: i18n.reloadResources,
+ changeLanguage: i18n.changeLanguage as unknown as (lang: string) => Promise,
+ language: i18n.language,
+ languages: languages
+ }
+ };
+ return client;
+ }, [t, i18n]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const WidgetGlobalStyle = createGlobalStyle`
+ html, body, #root {
+ height: 100%;
+ }
+
+ * {
+ -webkit-tap-highlight-color: transparent;
+ }
+`;
+
+const ThemeAndContent = () => {
+ const { data } = useProBackupState();
+ return (
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const ProvideActiveAccount: FC = ({ children }) => {
+ const storage = useAccountsStorage();
+ const [isLoading, setIsLoading] = useState(true);
+ useEffect(() => {
+ const addressFriendly = getTonkeeperInjectionContext()?.address;
+
+ if (!addressFriendly) {
+ return;
+ }
+
+ const addressRaw = Address.parse(addressFriendly).toRawString();
+
+ storage
+ .setAccounts([
+ new AccountTonWatchOnly(addressRaw, 'Wallet', '🙂', {
+ rawAddress: addressRaw,
+ id: addressRaw
+ })
+ ])
+ .then(() => setIsLoading(false));
+ }, []);
+
+ if (isLoading) {
+ return null;
+ }
+
+ return <>{children}>;
+};
+
+const Loader: FC = () => {
+ const { data: activeAccount, isLoading: activeWalletLoading } = useActiveAccountQuery();
+ const { data: accounts, isLoading: isWalletsLoading } = useAccountsStateQuery();
+ const { data: lang, isLoading: isLangLoading } = useUserLanguage();
+ const { data: fiat } = useUserFiatQuery();
+ const { data: devSettings } = useDevSettings();
+ const { isLoading: globalPreferencesLoading } = useGlobalPreferencesQuery();
+ const { isLoading: globalSetupLoading } = useGlobalSetup();
+
+ const [ios, standalone] = useMemo(() => {
+ return [sdk.isIOs(), sdk.isStandalone()] as const;
+ }, []);
+
+ const { i18n } = useTranslation();
+
+ const tonendpoint = useTonendpoint({
+ targetEnv: TARGET_ENV,
+ build: sdk.version,
+ lang
+ });
+ const { data: serverConfig } = useTonenpointConfig(tonendpoint);
+
+ useAppHeight();
+
+ const { data: tracker } = useAnalytics(activeAccount || undefined, accounts, sdk.version);
+
+ useEffect(() => {
+ if (activeAccount && lang && i18n.language !== localizationText(lang)) {
+ i18n.reloadResources([localizationText(lang)]).then(() =>
+ i18n.changeLanguage(localizationText(lang))
+ );
+ }
+ }, [activeAccount, i18n]);
+
+ if (
+ isWalletsLoading ||
+ activeWalletLoading ||
+ isLangLoading ||
+ serverConfig === undefined ||
+ fiat === undefined ||
+ !devSettings ||
+ globalPreferencesLoading ||
+ globalSetupLoading
+ ) {
+ return null;
+ }
+
+ const context: IAppContext = {
+ mainnetApi: getApiConfig(
+ serverConfig.mainnetConfig,
+ Network.MAINNET,
+ import.meta.env.VITE_APP_TONCONSOLE_HOST
+ ),
+ testnetApi: getApiConfig(serverConfig.mainnetConfig, Network.TESTNET),
+ fiat,
+ mainnetConfig: serverConfig.mainnetConfig,
+ testnetConfig: serverConfig.testnetConfig,
+ tonendpoint,
+ standalone,
+ extension: false,
+ proFeatures: false,
+ ios,
+ defaultWalletVersion: WalletVersion.V5R1,
+ hideMultisig: true,
+ env: {
+ tgAuthBotId: import.meta.env.VITE_APP_TG_BOT_ID,
+ stonfiReferralAddress: import.meta.env.VITE_APP_STONFI_REFERRAL_ADDRESS
+ }
+ };
+
+ return (
+
+
+
+
+
+
+ );
+};
+
+const Wrapper = styled.div`
+ box-sizing: border-box;
+ padding: 0 16px 34px;
+ height: 100%;
+`;
+
+const Content: FC<{
+ standalone: boolean;
+}> = ({ standalone }) => {
+ useWindowsScroll();
+ useAppWidth(standalone);
+ useKeyboardHeight();
+ useTrackLocation();
+ useDebuggingTools();
+ const isApplied = useApplyQueryParams();
+
+ if (!isApplied) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+};
diff --git a/apps/web-swap-widget/src/components/SwapWidgetFooter.tsx b/apps/web-swap-widget/src/components/SwapWidgetFooter.tsx
new file mode 100644
index 000000000..0c91e5429
--- /dev/null
+++ b/apps/web-swap-widget/src/components/SwapWidgetFooter.tsx
@@ -0,0 +1,103 @@
+import styled from 'styled-components';
+import { useTranslation } from 'react-i18next';
+import { Dot } from '@tonkeeper/uikit/dist/components/Dot';
+import { Body3Class } from '@tonkeeper/uikit';
+
+const Wrapper = styled.div``;
+
+const Row = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: ${p => p.theme.textTertiary};
+ ${Body3Class}
+`;
+
+const Link = styled.a`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ text-decoration: unset;
+ color: ${p => p.theme.textSecondary};
+
+ &:active {
+ color: ${p => p.theme.textPrimary};
+ }
+`;
+
+export const SwapWidgetFooter = () => {
+ const { t } = useTranslation();
+ return (
+
+
+ {t('legal_powered_by')}
+ {/*
+
+ Dedust
+
+ ,*/}
+
+
+ STON.fi
+
+ {t('and')} TON
+
+
+
+ {t('legal_privacy')}
+
+
+
+ {t('legal_terms')}
+
+
+
+ );
+};
+
+/*const DedustIcon = () => {
+ return (
+
+ );
+};*/
+
+const StonfiIcon = () => {
+ return (
+
+ );
+};
diff --git a/apps/web-swap-widget/src/components/SwapWidgetHeader.tsx b/apps/web-swap-widget/src/components/SwapWidgetHeader.tsx
new file mode 100644
index 000000000..61d14c219
--- /dev/null
+++ b/apps/web-swap-widget/src/components/SwapWidgetHeader.tsx
@@ -0,0 +1,55 @@
+import { FC } from 'react';
+import styled from 'styled-components';
+import { SwapRefreshButton } from '@tonkeeper/uikit/dist/components/swap/icon-buttons/SwapRefreshButton';
+import { SwapSettingsButton } from '@tonkeeper/uikit/dist/components/swap/icon-buttons/SwapSettingsButton';
+import { Label2 } from '@tonkeeper/uikit';
+import { IconButtonTransparentBackground } from '@tonkeeper/uikit/dist/components/fields/IconButton';
+import { CloseIcon } from '@tonkeeper/uikit/dist/components/Icon';
+import { useTranslation } from 'react-i18next';
+import { getTonkeeperInjectionContext } from '../libs/tonkeeper-injection-context';
+
+const Wrapper = styled.div`
+ margin: 8px -10px 0;
+ position: relative;
+ display: flex;
+ align-items: center;
+ height: 36px;
+ box-sizing: border-box;
+`;
+
+const ButtonsWrapper = styled.div`
+ position: absolute;
+ inset: 0;
+ gap: 12px;
+ display: flex;
+ align-items: center;
+`;
+
+const CloseButton = styled(IconButtonTransparentBackground)`
+ margin-left: auto;
+`;
+
+const Heading = styled(Label2)`
+ margin: 0 auto;
+`;
+
+export const SwapWidgetHeader: FC = () => {
+ const { t } = useTranslation();
+
+ const onClose = () => {
+ getTonkeeperInjectionContext()?.close();
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ {t('swap_title')}
+
+ );
+};
diff --git a/apps/web-swap-widget/src/components/SwapWidgetPage.tsx b/apps/web-swap-widget/src/components/SwapWidgetPage.tsx
new file mode 100644
index 000000000..417366fc5
--- /dev/null
+++ b/apps/web-swap-widget/src/components/SwapWidgetPage.tsx
@@ -0,0 +1,106 @@
+import { styled } from 'styled-components';
+import { useEncodeSwapToTonConnectParams } from '@tonkeeper/uikit/dist/state/swap/useEncodeSwap';
+import { useState } from 'react';
+import {
+ useSelectedSwap,
+ useSwapFromAmount,
+ useSwapFromAsset,
+ useSwapToAsset
+} from '@tonkeeper/uikit/dist/state/swap/useSwapForm';
+import { CalculatedSwap } from '@tonkeeper/uikit/dist/state/swap/useCalculatedSwap';
+import { SwapFromField } from '@tonkeeper/uikit/dist/components/swap/SwapFromField';
+import { SwapIcon } from '@tonkeeper/uikit/dist/components/Icon';
+import { SwapToField } from '@tonkeeper/uikit/dist/components/swap/SwapToField';
+import { SwapButton } from '@tonkeeper/uikit/dist/components/swap/SwapButton';
+import { SwapTokensListNotification } from '@tonkeeper/uikit/dist/components/swap/tokens-list/SwapTokensListNotification';
+import { IconButton } from '@tonkeeper/uikit/dist/components/fields/IconButton';
+import { NonNullableFields } from '@tonkeeper/core/dist/utils/types';
+import { SwapWidgetHeader } from './SwapWidgetHeader';
+import { getTonkeeperInjectionContext } from '../libs/tonkeeper-injection-context';
+import { SwapWidgetFooter } from './SwapWidgetFooter';
+
+const MainFormWrapper = styled.div`
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+`;
+
+const Spacer = styled.div`
+ flex: 1;
+`;
+
+const ChangeIconStyled = styled(IconButton)`
+ height: 32px;
+ width: 32px;
+ position: absolute;
+ right: calc(50% - 16px);
+ bottom: -20px;
+ border: none;
+
+ background-color: ${props => props.theme.buttonTertiaryBackground};
+
+ > svg {
+ transition: color 0.15s ease-in-out;
+ }
+
+ &:hover {
+ background-color: ${props => props.theme.buttonTertiaryBackgroundHighlighted};
+ > svg {
+ color: ${props => props.theme.iconPrimary};
+ }
+ }
+`;
+
+export const SwapWidgetPage = () => {
+ const { isLoading, mutateAsync: encode } = useEncodeSwapToTonConnectParams({
+ ignoreBattery: true
+ });
+ const [hasBeenSent, setHasBeenSent] = useState(false);
+ const [selectedSwap] = useSelectedSwap();
+ const [fromAsset, setFromAsset] = useSwapFromAsset();
+ const [toAsset, setToAsset] = useSwapToAsset();
+ const [_, setFromAmount] = useSwapFromAmount();
+
+ 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);
+ };
+
+ const onChangeFields = () => {
+ setFromAsset(toAsset);
+ setToAsset(fromAsset);
+ if (selectedSwap?.trade) {
+ setFromAmount(selectedSwap.trade.to.relativeAmount);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/apps/web-swap-widget/src/i18n.ts b/apps/web-swap-widget/src/i18n.ts
new file mode 100644
index 000000000..58fede7d7
--- /dev/null
+++ b/apps/web-swap-widget/src/i18n.ts
@@ -0,0 +1,23 @@
+import resources from '@tonkeeper/locales/dist/i18n/default.json';
+import i18n from 'i18next';
+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 default i18n;
diff --git a/apps/web-swap-widget/src/index.tsx b/apps/web-swap-widget/src/index.tsx
new file mode 100644
index 000000000..41b41115a
--- /dev/null
+++ b/apps/web-swap-widget/src/index.tsx
@@ -0,0 +1,7 @@
+import ReactDOM from 'react-dom/client';
+import { App } from './App';
+import './i18n';
+
+const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
+
+root.render();
diff --git a/apps/web-swap-widget/src/libs/appSdk.ts b/apps/web-swap-widget/src/libs/appSdk.ts
new file mode 100644
index 000000000..5ddd73019
--- /dev/null
+++ b/apps/web-swap-widget/src/libs/appSdk.ts
@@ -0,0 +1,48 @@
+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';
+
+function iOS() {
+ return (
+ ['iPad Simulator', 'iPhone Simulator', 'iPod Simulator', 'iPad', 'iPhone', 'iPod'].includes(
+ navigator.platform
+ ) ||
+ // iPad on iOS 13 detection
+ (navigator.userAgent.includes('Mac') && 'ontouchend' in document)
+ );
+}
+
+export class BrowserAppSdk extends BaseApp {
+ constructor() {
+ super(new BrowserStorage());
+ }
+
+ copyToClipboard = (value: string, notification?: string) => {
+ copyToClipboard(value);
+
+ this.topMessage(notification);
+ };
+
+ openPage = async (url: string) => {
+ window.open(url, '_black');
+ };
+
+ disableScroll = disableScroll;
+
+ enableScroll = enableScroll;
+
+ getScrollbarWidth = getScrollbarWidth;
+
+ getKeyboardHeight = () => 0;
+
+ isIOs = iOS;
+
+ isStandalone = () =>
+ iOS() && ((window.navigator as unknown as { standalone: boolean }).standalone as boolean);
+
+ version = packageJson.version ?? 'Unknown';
+
+ targetEnv = 'web' as const;
+}
diff --git a/apps/web-swap-widget/src/libs/hooks.ts b/apps/web-swap-widget/src/libs/hooks.ts
new file mode 100644
index 000000000..5fe41d80a
--- /dev/null
+++ b/apps/web-swap-widget/src/libs/hooks.ts
@@ -0,0 +1,124 @@
+import { useQuery } from '@tanstack/react-query';
+import { Account } from '@tonkeeper/core/dist/entries/account';
+import { seeIfValidTonAddress, throttle } from '@tonkeeper/core/dist/utils/common';
+import { Analytics, AnalyticsGroup, toWalletType } from '@tonkeeper/uikit/dist/hooks/analytics';
+import { AptabaseWeb } from '@tonkeeper/uikit/dist/hooks/analytics/aptabase-web';
+import { Gtag } from '@tonkeeper/uikit/dist/hooks/analytics/gtag';
+import { QueryKey } from '@tonkeeper/uikit/dist/libs/queryKey';
+import { useActiveTonNetwork } from '@tonkeeper/uikit/dist/state/wallet';
+import { useEffect, useState } from 'react';
+import { useSwapFromAsset, useSwapToAsset } from '@tonkeeper/uikit/dist/state/swap/useSwapForm';
+import { useAllSwapAssets } from '@tonkeeper/uikit/dist/state/swap/useSwapAssets';
+import { tonAssetAddressToString } from '@tonkeeper/core/dist/entries/crypto/asset/ton-asset';
+import { Address } from '@ton/core';
+
+export const useAppHeight = () => {
+ useEffect(() => {
+ const appHeight = throttle(() => {
+ const doc = document.documentElement;
+ doc.style.setProperty('--app-height', `${window.innerHeight}px`);
+ }, 50);
+ window.addEventListener('resize', appHeight);
+ appHeight();
+
+ return () => {
+ window.removeEventListener('resize', appHeight);
+ };
+ }, []);
+};
+
+export const useAppWidth = (standalone: boolean) => {
+ useEffect(() => {
+ const appWidth = throttle(() => {
+ if (standalone) {
+ const doc = document.documentElement;
+ doc.style.setProperty('--app-width', `${window.innerWidth}px`);
+ } else {
+ const doc = document.documentElement;
+ const app = (document.getElementById('root') as HTMLDivElement).childNodes.item(
+ 0
+ ) as HTMLDivElement;
+
+ doc.style.setProperty('--app-width', `${app.clientWidth}px`);
+ }
+ }, 50);
+ window.addEventListener('resize', appWidth);
+
+ appWidth();
+
+ return () => {
+ window.removeEventListener('resize', appWidth);
+ };
+ }, [standalone]);
+};
+
+export const useAnalytics = (activeAccount?: Account, accounts?: Account[], version?: string) => {
+ const network = useActiveTonNetwork();
+ return useQuery(
+ [QueryKey.analytics, network],
+ async () => {
+ const tracker = new AnalyticsGroup(
+ new AptabaseWeb(
+ import.meta.env.VITE_APP_APTABASE_HOST,
+ import.meta.env.VITE_APP_APTABASE,
+ version
+ ),
+ new Gtag(import.meta.env.VITE_APP_MEASUREMENT_ID)
+ );
+
+ tracker.init({
+ application: 'Swap-widget-web',
+ walletType: toWalletType(activeAccount?.activeTonWallet),
+ activeAccount: activeAccount!,
+ accounts: accounts!,
+ network
+ });
+
+ return tracker;
+ },
+ { enabled: accounts != null && activeAccount !== undefined }
+ );
+};
+
+export const useApplyQueryParams = () => {
+ const [_, setFromAsset] = useSwapFromAsset();
+ const [__, setToAsset] = useSwapToAsset();
+ const { data: allAssets } = useAllSwapAssets();
+ const [isApplied, setIsApplied] = useState(false);
+
+ useEffect(() => {
+ if (!allAssets || isApplied) {
+ return;
+ }
+ setIsApplied(true);
+ const searchParams = new URLSearchParams(window.location.search);
+ const fromAssetParam = searchParams.get('ft');
+ const toAssetParam = searchParams.get('tt');
+
+ const fromAssetToSet =
+ fromAssetParam && seeIfValidTonAddress(fromAssetParam)
+ ? allAssets.find(
+ asset =>
+ tonAssetAddressToString(asset.address).toLowerCase() ===
+ Address.parse(fromAssetParam).toRawString()
+ )
+ : undefined;
+ if (fromAssetToSet) {
+ setFromAsset(fromAssetToSet);
+ }
+
+ const toAssetToSet =
+ toAssetParam && seeIfValidTonAddress(toAssetParam)
+ ? allAssets.find(
+ asset =>
+ tonAssetAddressToString(asset.address).toLowerCase() ===
+ Address.parse(toAssetParam).toRawString()
+ )
+ : undefined;
+ if (toAssetToSet) {
+ setToAsset(toAssetToSet);
+ }
+ }, [allAssets, isApplied]);
+
+ return isApplied;
+};
diff --git a/apps/web-swap-widget/src/libs/router-stub.tsx b/apps/web-swap-widget/src/libs/router-stub.tsx
new file mode 100644
index 000000000..4dae5113f
--- /dev/null
+++ b/apps/web-swap-widget/src/libs/router-stub.tsx
@@ -0,0 +1,13 @@
+export const useNavigate = () => {
+ return () => {};
+};
+
+export const useLocation = () => {
+ return {
+ pathname: ''
+ };
+};
+
+export const Link = () => {
+ return <>>;
+};
diff --git a/apps/web-swap-widget/src/libs/scroll.ts b/apps/web-swap-widget/src/libs/scroll.ts
new file mode 100644
index 000000000..d8d76b46d
--- /dev/null
+++ b/apps/web-swap-widget/src/libs/scroll.ts
@@ -0,0 +1,30 @@
+export const disableScroll = () => {
+ document.documentElement.classList.add('is-locked');
+ window.document.body.style.paddingRight = `${getScrollbarWidth()}px`;
+};
+
+export const enableScroll = () => {
+ document.documentElement.classList.remove('is-locked');
+ window.document.body.style.paddingRight = '0px';
+};
+
+export const getScrollbarWidth = () => {
+ // Creating invisible container
+ const outer = document.createElement('div');
+ outer.style.visibility = 'hidden';
+ outer.style.overflow = 'scroll'; // forcing scrollbar to appear
+ (outer.style as any).msOverflowStyle = 'scrollbar'; // needed for WinJS apps
+ document.body.appendChild(outer);
+
+ // Creating inner element and placing it in the container
+ const inner = document.createElement('div');
+ outer.appendChild(inner);
+
+ // Calculating difference between container's full width and the child width
+ const scrollbarWidth = outer.offsetWidth - inner.offsetWidth;
+
+ // Removing temporary elements from the DOM
+ outer.parentNode!.removeChild(outer);
+
+ return scrollbarWidth;
+};
diff --git a/apps/web-swap-widget/src/libs/storage.ts b/apps/web-swap-widget/src/libs/storage.ts
new file mode 100644
index 000000000..1ee17aba9
--- /dev/null
+++ b/apps/web-swap-widget/src/libs/storage.ts
@@ -0,0 +1,36 @@
+import { IStorage } from '@tonkeeper/core/dist/Storage';
+
+export class BrowserStorage implements IStorage {
+ prefix = 'tonkeeper-swap-widget';
+
+ get = async (key: string) => {
+ const value = localStorage.getItem(`${this.prefix}_${key}`);
+ if (!value) return null;
+ const { payload } = JSON.parse(value) as { payload: R };
+ return payload;
+ };
+
+ set = async (key: string, payload: R) => {
+ localStorage.setItem(`${this.prefix}_${key}`, JSON.stringify({ payload }));
+ return payload;
+ };
+
+ setBatch = async >(values: V) => {
+ Object.entries(values).forEach(([key, payload]) => {
+ localStorage.setItem(`${this.prefix}_${key}`, JSON.stringify({ payload }));
+ });
+ return values;
+ };
+
+ delete = async (key: string) => {
+ const payload = await this.get(key);
+ if (payload != null) {
+ localStorage.removeItem(`${this.prefix}_${key}`);
+ }
+ return payload;
+ };
+
+ clear = async () => {
+ localStorage.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
new file mode 100644
index 000000000..ac3ed10a6
--- /dev/null
+++ b/apps/web-swap-widget/src/libs/tonkeeper-injection-context.ts
@@ -0,0 +1,28 @@
+import { getWindow } from '@tonkeeper/core/dist/service/telegramOauth';
+
+type UserFriendlyAddress = string;
+type TimestampMS = number;
+
+export type TonkeeperInjection = {
+ address: UserFriendlyAddress;
+ close: () => void;
+ sendTransaction: (params: {
+ source: UserFriendlyAddress;
+ valid_until: TimestampMS;
+ messages: {
+ address: UserFriendlyAddress;
+ amount: string;
+ payload?: string;
+ }[];
+ }) => Promise;
+};
+
+declare global {
+ interface Window {
+ tonkeeperStonfi?: TonkeeperInjection;
+ }
+}
+
+export const getTonkeeperInjectionContext = () => {
+ return getWindow()?.tonkeeperStonfi;
+};
diff --git a/apps/web-swap-widget/src/logo.svg b/apps/web-swap-widget/src/logo.svg
new file mode 100644
index 000000000..9dfc1c058
--- /dev/null
+++ b/apps/web-swap-widget/src/logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/web-swap-widget/src/setupTests.ts b/apps/web-swap-widget/src/setupTests.ts
new file mode 100644
index 000000000..8f2609b7b
--- /dev/null
+++ b/apps/web-swap-widget/src/setupTests.ts
@@ -0,0 +1,5 @@
+// jest-dom adds custom jest matchers for asserting on DOM nodes.
+// allows you to do things like:
+// expect(element).toHaveTextContent(/react/i)
+// learn more: https://github.com/testing-library/jest-dom
+import '@testing-library/jest-dom';
diff --git a/apps/web-swap-widget/src/vite-env.d.ts b/apps/web-swap-widget/src/vite-env.d.ts
new file mode 100644
index 000000000..11f02fe2a
--- /dev/null
+++ b/apps/web-swap-widget/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/apps/web-swap-widget/task/locales.ts b/apps/web-swap-widget/task/locales.ts
new file mode 100644
index 000000000..fbfbfa0fd
--- /dev/null
+++ b/apps/web-swap-widget/task/locales.ts
@@ -0,0 +1,23 @@
+import * as fs from 'fs-extra';
+import * as path from 'path';
+
+console.log('Copy Locales');
+const srcDir = `../../packages/locales/dist/locales`;
+const buildDestDir = `dist/locales`;
+const devDestDir = `public/locales`;
+
+console.log(path.resolve(srcDir));
+fs.readdirSync(srcDir).forEach(file => console.log(file));
+
+if (!fs.existsSync('build')) {
+ fs.mkdirSync('build');
+}
+if (!fs.existsSync(buildDestDir)) {
+ fs.mkdirSync(buildDestDir);
+}
+fs.rmSync(devDestDir, { recursive: true, force: true });
+if (!fs.existsSync(devDestDir)) {
+ fs.mkdirSync(devDestDir);
+}
+fs.copySync(srcDir, buildDestDir, { overwrite: true });
+fs.copySync(srcDir, devDestDir, { overwrite: true });
diff --git a/apps/web-swap-widget/tsconfig.json b/apps/web-swap-widget/tsconfig.json
new file mode 100644
index 000000000..8f9d0bf15
--- /dev/null
+++ b/apps/web-swap-widget/tsconfig.json
@@ -0,0 +1,30 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "allowSyntheticDefaultImports": true,
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }],
+ "ts-node": {
+ "compilerOptions": {
+ "module": "CommonJS"
+ }
+ }
+}
diff --git a/apps/web-swap-widget/tsconfig.node.json b/apps/web-swap-widget/tsconfig.node.json
new file mode 100644
index 000000000..af7c06e30
--- /dev/null
+++ b/apps/web-swap-widget/tsconfig.node.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "node",
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/apps/web-swap-widget/vite.config.mts b/apps/web-swap-widget/vite.config.mts
new file mode 100644
index 000000000..2d2a40619
--- /dev/null
+++ b/apps/web-swap-widget/vite.config.mts
@@ -0,0 +1,31 @@
+import react from '@vitejs/plugin-react';
+import * as path from 'path';
+import { defineConfig } from 'vite';
+import { nodePolyfills } from 'vite-plugin-node-polyfills';
+
+export default defineConfig({
+ plugins: [
+ nodePolyfills({
+ globals: {
+ Buffer: true,
+ global: true,
+ process: true
+ },
+ include: ['stream', 'buffer', 'crypto']
+ }),
+ react()
+ ],
+ resolve: {
+ alias: {
+ react: path.resolve(__dirname, './node_modules/react'),
+ 'react-dom': path.resolve(__dirname, './node_modules/react-dom'),
+ '@ton/core': path.resolve(__dirname, '../../packages/core/node_modules/@ton/core'),
+ '@ton/crypto': path.resolve(__dirname, '../../packages/core/node_modules/@ton/crypto'),
+ '@ton/ton': path.resolve(__dirname, '../../packages/core/node_modules/@ton/ton'),
+ 'react-router-dom': path.resolve(__dirname, './src/libs/router-stub'),
+ 'styled-components': path.resolve(__dirname, './node_modules/styled-components'),
+ 'react-i18next': path.resolve(__dirname, './node_modules/react-i18next'),
+ '@tanstack/react-query': path.resolve(__dirname, './node_modules/@tanstack/react-query')
+ }
+ }
+});
diff --git a/package.json b/package.json
index 42c3afbd1..0b48158ad 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
"build:extension": "npx turbo build:extension",
"build:desktop": "npx turbo build:desktop",
"build:ipad": "npx turbo build:ipad",
+ "build:swap-widget": "npx turbo build:swap-widget",
"build:pkg": "npx turbo build:pkg"
},
"private": true,
diff --git a/packages/core/src/AppSdk.ts b/packages/core/src/AppSdk.ts
index 48f6712a1..0c29413ac 100644
--- a/packages/core/src/AppSdk.ts
+++ b/packages/core/src/AppSdk.ts
@@ -182,4 +182,4 @@ export class MockAppSdk extends BaseApp {
}
}
-export type TargetEnv = 'web' | 'extension' | 'desktop' | 'twa' | 'tablet';
+export type TargetEnv = 'web' | 'extension' | 'desktop' | 'twa' | 'tablet' | 'swap-widget-web';
diff --git a/packages/locales/src/tonkeeper-web/en.json b/packages/locales/src/tonkeeper-web/en.json
index 7b196ba8c..8b12a35ae 100644
--- a/packages/locales/src/tonkeeper-web/en.json
+++ b/packages/locales/src/tonkeeper-web/en.json
@@ -19,6 +19,7 @@
"add_wallet_new_multisig_description": "For joint management and protection of cryptocurrency funds",
"add_wallet_new_multisig_title": "New Multisig Wallet",
"all_assets_jettons": "All Assets",
+ "and": "and",
"appExtensionDescription": "Your extension wallet on The Open Network",
"appName": "Tonkeeper",
"appTitle": "Tonkeeper — wallet for TON",
@@ -107,6 +108,7 @@
"enter_password": "Enter password",
"export_dot_csv": "Export .CSV",
"force_reload": "Force Reload",
+ "help": "Help",
"hide": "Hide",
"hide_others": "Hide Others",
"hide_tonkeeper_pro": "Hide Tonkeeper Pro",
@@ -167,6 +169,7 @@
"ledger_steps_connect": "Connect Ledger to your device",
"ledger_steps_install_ton": "Install TON App ",
"ledger_steps_open_ton": "Unlock it and open TON App",
+ "legal_powered_by": "Powered by",
"Localization": "Language",
"Lock_screen": "Lock screen",
"logout_on_unlock_many": "This will erase keys to all your wallets. Make sure you have backed up your secret recovery phrases.",
@@ -367,6 +370,22 @@
"transaction_type_mint": "Mint",
"transaction_type_purchase": "Purchase",
"try_again": "Try Again",
+ "two_fa_confirm_tg_cannot_access_tg": "Can’t access your Telegram account?",
+ "two_fa_confirm_tg_description": "Go to the @tonkeeper_2fa_bot and tap «Confirm» to complete the transaction.",
+ "two_fa_confirm_tg_title": "Confirm Transaction in Telegram",
+ "two_fa_send_continue_with_tg": "Continue with Telegram",
+ "two_fa_settings_heading_description": "This experimental feature allows you to enable two-factor authentication (2FA) for added wallet security. It will prompt you for confirmation in Telegram during large transactions and help restore your wallet if you lose your mnemonic. Installation steps:",
+ "two_fa_settings_heading_title": "Two-Factor Authentication",
+ "two_fa_settings_set_up_deploy_step_button": "Activate 2FA",
+ "two_fa_settings_set_up_deploy_step_description": "Confirm transaction to install 2FA extension",
+ "two_fa_settings_set_up_tg_connection_modal_copy_button": "Copy Link",
+ "two_fa_settings_set_up_tg_connection_modal_heading": "Scan QR code below with your mobile phone or open Telegram on this device to connect.",
+ "two_fa_settings_set_up_tg_connection_modal_open_button": "Open Telegram",
+ "two_fa_settings_set_up_tg_step_description": "Confirm your connection in your Telegram ",
+ "two_fa_settings_warning_balance_required": "A TON balance is required to install or uninstall the extension.",
+ "two_fa_settings_warning_battery_gasless": "Battery mode and gasless transactions are not compatible with 2FA.",
+ "two_fa_settings_warning_wallet_will_stop": "The same wallet will stop working on your other devices.",
+ "two_fa_short": "2FA",
"txActions_USDT_transfer": "USDT Transfer",
"Undo": "Undo",
"Unexpected_QR_Code": "Unexpected QR Code",
diff --git a/packages/locales/src/tonkeeper-web/ru-RU.json b/packages/locales/src/tonkeeper-web/ru-RU.json
index 8a9d6c459..f4f21613a 100644
--- a/packages/locales/src/tonkeeper-web/ru-RU.json
+++ b/packages/locales/src/tonkeeper-web/ru-RU.json
@@ -18,6 +18,7 @@
"add_wallet_modal_mam_title": "Новый аккаунт мульти-кошелек",
"add_wallet_new_multisig_description": "Для совместного управления и защиты криптовалюты",
"add_wallet_new_multisig_title": "Новый Multisig кошелек ",
+ "and": "и",
"appExtensionDescription": "Your extension wallet on The Open Network",
"appName": "Tonkeeper",
"appTitle": "Tonkeeper — wallet for TON",
@@ -105,6 +106,7 @@
"enter_password": "Введите пароль",
"export_dot_csv": "Экспорт в .CSV",
"force_reload": "Принудительная перезагрузка",
+ "help": "Помощь",
"hide": "Скрыть",
"hide_others": "Скрыть остальных",
"hide_tonkeeper_pro": "Скрыть Tonkeeper Pro",
@@ -160,6 +162,7 @@
"ledger_steps_connect": "Подключите Ledger к своему устройству",
"ledger_steps_install_ton": "Установить приложение ",
"ledger_steps_open_ton": "Разблокируйте его и откройте приложение TON ",
+ "legal_powered_by": "Совместно с",
"Localization": "Язык",
"Lock_screen": "Экран блокировки",
"logout_on_unlock_many": "Доступ к кошелькам будет отключен. Убедитесь, что вы сохранили секретные ключи.",
@@ -354,6 +357,22 @@
"transaction_type_mint": "Создание",
"transaction_type_purchase": "Покупка",
"try_again": "Повторить",
+ "two_fa_confirm_tg_cannot_access_tg": "Не можете получить доступ к своему Telegram аккаунту?",
+ "two_fa_confirm_tg_description": "Перейдите в @tonkeeper_2fa_bot и нажмите «Подтвердить», чтобы завершить транзакцию.",
+ "two_fa_confirm_tg_title": "Подтвердите транзакцию в Telegram",
+ "two_fa_send_continue_with_tg": "Продолжить с Telegram",
+ "two_fa_settings_heading_description": "Эта экспериментальная функция позволяет включить двухфакторную аутентификацию (2FA) для дополнительной безопасности кошелька. Она будет запрашивать подтверждение в Telegram при крупных транзакциях и поможет восстановить ваш кошелек, если вы потеряете мнемоническую фразу. Шаги установки:",
+ "two_fa_settings_heading_title": "Двухфакторная аутентификация",
+ "two_fa_settings_set_up_deploy_step_button": "Активировать 2FA",
+ "two_fa_settings_set_up_deploy_step_description": "Подтвердите транзакцию для установки расширения 2FA",
+ "two_fa_settings_set_up_tg_connection_modal_copy_button": "Копировать ссылку",
+ "two_fa_settings_set_up_tg_connection_modal_heading": "Отсканируйте QR-код ниже с помощью вашего мобильного телефона или откройте Telegram на этом устройстве для подключения.",
+ "two_fa_settings_set_up_tg_connection_modal_open_button": "Открыть Телеграм",
+ "two_fa_settings_set_up_tg_step_description": "Подтвердите соединение в Telegram ",
+ "two_fa_settings_warning_balance_required": "Для установки или удаления расширения требуется баланс TON.",
+ "two_fa_settings_warning_battery_gasless": "Батарейка Tonkeeper и безгазовые транзакции не работают с двухфакторной аутентификацией.",
+ "two_fa_settings_warning_wallet_will_stop": "Этот же кошелек перестанет работать на других ваших устройствах.",
+ "two_fa_short": "2ФА",
"txActions_USDT_transfer": "Перевод USDT",
"Undo": "Отменить",
"Unexpected_QR_Code": "Неожиданный QR-код",
diff --git a/packages/uikit/src/components/swap/SwapToField.tsx b/packages/uikit/src/components/swap/SwapToField.tsx
index 0e8e86b78..45be007c0 100644
--- a/packages/uikit/src/components/swap/SwapToField.tsx
+++ b/packages/uikit/src/components/swap/SwapToField.tsx
@@ -9,6 +9,7 @@ import { Skeleton } from '../shared/Skeleton';
import { SwapTransactionInfo } from './SwapTransactionInfo';
import { SwapRate } from './SwapRate';
import { useTranslation } from '../../hooks/translation';
+import { FC } from 'react';
const FiledContainerStyled = styled.div`
background: ${p => p.theme.backgroundContent};
@@ -73,7 +74,7 @@ const Num2Tertiary = styled(Num2)`
color: ${p => p.theme.textTertiary};
`;
-export const SwapToField = () => {
+export const SwapToField: FC<{ separateInfo?: boolean }> = ({ separateInfo }) => {
const { t } = useTranslation();
const [toAsset, setToAsset] = useSwapToAsset();
const { isFetching } = useCalculatedSwap();
@@ -81,28 +82,38 @@ export const SwapToField = () => {
const [selectedSwap] = useSelectedSwap();
return (
-
-
- {t('swap_receive')}
-
-
-
-
-
- {!selectedSwap?.trade && isFetching ? (
-
- ) : selectedSwap?.trade ? (
- {selectedSwap.trade.to.stringRelativeAmount}
- ) : (
- 0
- )}
-
-
-
-
-
-
-
-
+ <>
+
+
+ {t('swap_receive')}
+
+
+
+
+
+ {!selectedSwap?.trade && isFetching ? (
+
+ ) : selectedSwap?.trade ? (
+ {selectedSwap.trade.to.stringRelativeAmount}
+ ) : (
+ 0
+ )}
+
+
+
+
+
+
+ {!separateInfo && }
+
+ {separateInfo && (
+
+
+
+ )}
+ >
);
};
diff --git a/packages/uikit/src/providers/UserThemeProvider.tsx b/packages/uikit/src/providers/UserThemeProvider.tsx
index e5be6c5f6..0970f99a5 100644
--- a/packages/uikit/src/providers/UserThemeProvider.tsx
+++ b/packages/uikit/src/providers/UserThemeProvider.tsx
@@ -8,8 +8,9 @@ export const UserThemeProvider: FC<
displayType?: 'compact' | 'full-width';
isPro?: boolean;
isProSupported?: boolean;
+ isInsideTonkeeper?: boolean;
}>
-> = ({ children, displayType, isPro, isProSupported }) => {
+> = ({ children, displayType, isPro, isProSupported, isInsideTonkeeper }) => {
const { data: uiPreferences, isFetched: isUIPreferencesLoaded } = useUserUIPreferences();
const { mutateAsync } = useMutateUserUIPreferences();
const isProPrev = usePrevious(isPro);
@@ -31,7 +32,7 @@ export const UserThemeProvider: FC<
themeName = themeName || 'dark';
- const theme = availableThemes[themeName];
+ let theme = availableThemes[themeName];
if (displayType) {
theme.displayType = displayType;
@@ -41,8 +42,21 @@ export const UserThemeProvider: FC<
window.document.body.style.background = theme.backgroundPage;
+ if (isInsideTonkeeper) {
+ theme = {
+ ...theme,
+ corner3xSmall: '2px',
+ corner2xSmall: '4px',
+ cornerExtraSmall: '6px',
+ cornerSmall: '8px',
+ cornerMedium: '12px',
+ cornerLarge: '16px',
+ cornerFull: '100%'
+ };
+ }
+
return [theme, themeName];
- }, [uiPreferences?.theme, displayType, isPro, isProPrev]);
+ }, [uiPreferences?.theme, displayType, isPro, isProPrev, isInsideTonkeeper]);
useEffect(() => {
if (currentTheme && uiPreferences && currentThemeName !== uiPreferences.theme) {
diff --git a/packages/uikit/src/state/swap/useEncodeSwap.ts b/packages/uikit/src/state/swap/useEncodeSwap.ts
index c16e162e2..1c3bf69b3 100644
--- a/packages/uikit/src/state/swap/useEncodeSwap.ts
+++ b/packages/uikit/src/state/swap/useEncodeSwap.ts
@@ -3,7 +3,6 @@ import { CalculatedSwap } from './useCalculatedSwap';
import type { SwapService } from '@tonkeeper/core/dist/swapsApi';
import { assertUnreachable, NonNullableFields } from '@tonkeeper/core/dist/utils/types';
import { Address } from '@ton/core';
-import { useAppContext } from '../../hooks/appContext';
import { useSwapsConfig } from './useSwapsConfig';
import BigNumber from 'bignumber.js';
import { useSwapOptions } from './useSwapOptions';
@@ -44,7 +43,7 @@ export function useEncodeSwap() {
});
}
-export function useEncodeSwapToTonConnectParams() {
+export function useEncodeSwapToTonConnectParams(options: { ignoreBattery?: boolean } = {}) {
const { mutateAsync: encode } = useEncodeSwap();
const { data: batteryBalance } = useBatteryBalance();
const { excessAccount: batteryExcess } = useBatteryServiceConfig();
@@ -54,9 +53,9 @@ export function useEncodeSwapToTonConnectParams() {
async swap => {
const resultsPromises = [encode(swap)];
- const batterySwapsEnabled = activeWalletConfig
- ? activeWalletConfig.batterySettings.enabledForSwaps
- : true;
+ const batterySwapsEnabled =
+ (activeWalletConfig ? activeWalletConfig.batterySettings.enabledForSwaps : true) &&
+ !options.ignoreBattery;
if (batteryBalance?.batteryUnitsBalance.gt(0) && batterySwapsEnabled) {
resultsPromises.push(
encode({ ...swap, excessAddress: Address.parse(batteryExcess).toRawString() })
diff --git a/packages/uikit/src/state/wallet.ts b/packages/uikit/src/state/wallet.ts
index ae9d239e7..8703dd4ad 100644
--- a/packages/uikit/src/state/wallet.ts
+++ b/packages/uikit/src/state/wallet.ts
@@ -49,7 +49,6 @@ import { useAppContext } from '../hooks/appContext';
import { useAppSdk } from '../hooks/appSdk';
import { useAccountsStorage } from '../hooks/useStorage';
import { anyOfKeysParts, QueryKey } from '../libs/queryKey';
-import { useDevSettings } from './dev';
import { getAccountMnemonic, getPasswordByNotification } from './mnemonic';
import { useCheckTouchId } from './password';
import { seeIfMnemonicValid } from '@tonkeeper/core/dist/service/mnemonicService';
diff --git a/turbo.json b/turbo.json
index 1cc934d5a..2b14104b1 100644
--- a/turbo.json
+++ b/turbo.json
@@ -10,6 +10,10 @@
"dependsOn": ["^build:pkg"],
"outputs": [".next/**", "dist/**"]
},
+ "build:swap-widget": {
+ "dependsOn": ["^build:pkg"],
+ "outputs": [".next/**", "dist/**"]
+ },
"build:ipad": {
"dependsOn": ["^build:pkg"],
"outputs": [".next/**", "dist/**"]
diff --git a/yarn.lock b/yarn.lock
index 2f7260a33..35d14baeb 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7315,6 +7315,17 @@ __metadata:
languageName: node
linkType: hard
+"@ton/core@npm:^0.56.0":
+ version: 0.56.3
+ resolution: "@ton/core@npm:0.56.3"
+ dependencies:
+ symbol.inspect: "npm:1.0.1"
+ peerDependencies:
+ "@ton/crypto": ">=3.2.0"
+ checksum: 10/5fe0284bc66789e4b408cce7d459d30d0f8477ce981337ef7bfe7beeee93eda575e8178338fb3d73d47cd8f78065ed84988b82db839b463a39e576be39831243
+ languageName: node
+ linkType: hard
+
"@ton/crypto-primitives@npm:2.0.0":
version: 2.0.0
resolution: "@ton/crypto-primitives@npm:2.0.0"
@@ -7665,6 +7676,47 @@ __metadata:
languageName: unknown
linkType: soft
+"@tonkeeper/web-swap-widget@workspace:apps/web-swap-widget":
+ version: 0.0.0-use.local
+ resolution: "@tonkeeper/web-swap-widget@workspace:apps/web-swap-widget"
+ dependencies:
+ "@amplitude/analytics-browser": "npm:^2.1.0"
+ "@aptabase/web": "npm:^0.4.2"
+ "@tanstack/react-query": "npm:4.3.4"
+ "@testing-library/dom": "npm:^9.3.1"
+ "@testing-library/jest-dom": "npm:^5.16.5"
+ "@testing-library/react": "npm:^13.4.0"
+ "@testing-library/user-event": "npm:^13.5.0"
+ "@ton/core": "npm:^0.56.0"
+ "@tonkeeper/core": "npm:0.1.0"
+ "@tonkeeper/locales": "npm:0.1.0"
+ "@tonkeeper/uikit": "npm:0.1.0"
+ "@types/fs-extra": "npm:^11.0.4"
+ "@types/jest": "npm:^27.5.2"
+ "@types/node": "npm:^20.11.0"
+ "@types/react": "npm:^18.0.26"
+ "@types/react-dom": "npm:^18.0.9"
+ "@types/styled-components": "npm:^5.1.26"
+ "@vitejs/plugin-react": "npm:^4.2.1"
+ buffer: "npm:^6.0.3"
+ copy-to-clipboard: "npm:^3.3.3"
+ fs-extra: "npm:^11.2.0"
+ i18next: "npm:^22.1.4"
+ i18next-browser-languagedetector: "npm:^7.0.2"
+ i18next-http-backend: "npm:^2.0.2"
+ process: "npm:^0.11.10"
+ react: "npm:^18.2.0"
+ react-dom: "npm:^18.2.0"
+ react-i18next: "npm:^12.1.1"
+ react-is: "npm:^18.2.0"
+ styled-components: "npm:^6.1.1"
+ ts-node: "npm:^10.9.1"
+ typescript: "npm:5.2.2"
+ vite: "npm:^5.0.11"
+ vite-plugin-node-polyfills: "npm:0.17.0"
+ languageName: unknown
+ linkType: soft
+
"@tonkeeper/web@workspace:apps/web":
version: 0.0.0-use.local
resolution: "@tonkeeper/web@workspace:apps/web"