From 40a87ef40b6cba21a647534a1afb7141d5fdd84e Mon Sep 17 00:00:00 2001 From: Moiseev Ilya Date: Wed, 14 Jan 2026 22:37:42 +0530 Subject: [PATCH 01/14] feat(demo): create tonconnect-demo-unified app --- apps/tonconnect-demo-unified/.gitignore | 24 + apps/tonconnect-demo-unified/README.md | 73 + apps/tonconnect-demo-unified/components.json | 22 + apps/tonconnect-demo-unified/eslint.config.js | 23 + apps/tonconnect-demo-unified/index.html | 13 + apps/tonconnect-demo-unified/package.json | 80 + .../public/mockServiceWorker.js | 349 ++ apps/tonconnect-demo-unified/public/vite.svg | 1 + apps/tonconnect-demo-unified/src/App.tsx | 12 + .../src/assets/react.svg | 1 + .../src/components/DemoContent.tsx | 54 + .../src/components/Header.tsx | 34 + .../src/components/NetworkPicker.tsx | 67 + .../components/shared/ExpandableDocsCard.tsx | 103 + .../src/components/shared/FieldInfoModal.tsx | 48 + .../src/components/shared/FieldLabel.tsx | 35 + .../src/components/shared/FormContainer.tsx | 413 ++ .../src/components/shared/HistoryList.tsx | 426 ++ .../src/components/shared/HowItWorksCard.tsx | 164 + .../src/components/shared/JsonDisplay.tsx | 38 + .../src/components/shared/JsonViewer.tsx | 197 + .../src/components/shared/ResultCard.tsx | 235 + .../components/shared/SignDataResultCard.tsx | 166 + .../components/shared/TransactionResult.tsx | 144 + .../src/components/tabs/SettingsTab.tsx | 377 ++ .../src/components/tabs/SignDataTab.tsx | 129 + .../src/components/tabs/SubscriptionTab.tsx | 18 + .../src/components/tabs/TonProofTab.tsx | 187 + .../src/components/tabs/TransactionTab.tsx | 299 + .../src/components/tabs/index.ts | 5 + .../src/components/ui/alert.tsx | 61 + .../src/components/ui/badge.tsx | 36 + .../src/components/ui/button.tsx | 57 + .../src/components/ui/card.tsx | 76 + .../src/components/ui/checkbox.tsx | 28 + .../src/components/ui/collapsible.tsx | 9 + .../src/components/ui/dialog.tsx | 119 + .../src/components/ui/dropdown-menu.tsx | 201 + .../src/components/ui/input.tsx | 24 + .../src/components/ui/label.tsx | 24 + .../src/components/ui/scroll-area.tsx | 46 + .../src/components/ui/select.tsx | 157 + .../src/components/ui/sonner.tsx | 40 + .../src/components/ui/switch.tsx | 29 + .../src/components/ui/tabs.tsx | 53 + .../src/components/ui/textarea.tsx | 23 + .../src/context/SettingsContext.tsx | 24 + .../src/data/field-info.ts | 57 + .../src/docs/fields/sign-data/bytes.md | 14 + .../src/docs/fields/sign-data/cell.md | 20 + .../src/docs/fields/sign-data/data-type.md | 44 + .../src/docs/fields/sign-data/schema.md | 32 + .../src/docs/fields/sign-data/text.md | 13 + .../src/docs/fields/transaction/address.md | 43 + .../src/docs/fields/transaction/amount.md | 38 + .../src/docs/fields/transaction/from.md | 23 + .../src/docs/fields/transaction/messages.md | 36 + .../src/docs/fields/transaction/network.md | 38 + .../src/docs/fields/transaction/payload.md | 28 + .../src/docs/fields/transaction/state-init.md | 34 + .../docs/fields/transaction/valid-until.md | 45 + .../src/docs/sections/sign-data.md | 43 + .../src/docs/sections/transaction.md | 51 + .../src/hooks/useHistory.ts | 127 + .../src/hooks/useSettings.ts | 350 ++ .../src/hooks/useSignData.ts | 282 + .../src/hooks/useTheme.ts | 13 + .../src/hooks/useTonProof.ts | 189 + .../src/hooks/useTransaction.ts | 375 ++ .../src/hooks/useTransactionTracker.ts | 127 + apps/tonconnect-demo-unified/src/index.css | 257 + .../src/lib/codemirror-theme.ts | 98 + apps/tonconnect-demo-unified/src/lib/utils.ts | 6 + apps/tonconnect-demo-unified/src/main.tsx | 19 + apps/tonconnect-demo-unified/src/polyfills.ts | 11 + .../src/schemas/sign-data.schema.json | 33 + .../src/schemas/transaction.schema.json | 53 + .../src/server/handlers/check-proof.ts | 60 + .../src/server/handlers/check-sign-data.ts | 48 + .../src/server/handlers/find-transaction.ts | 49 + .../src/server/handlers/generate-payload.ts | 24 + .../src/server/handlers/get-account-info.ts | 48 + .../src/server/handlers/index.ts | 13 + .../src/server/index.ts | 12 + .../src/server/services/ton-api.service.ts | 40 + .../src/server/services/ton-proof.service.ts | 110 + .../src/server/utils/jwt.ts | 49 + .../src/server/utils/wallet-parsers.ts | 115 + .../src/utils/explorer-utils.ts | 4 + .../src/utils/sign-data-verification.ts | 190 + .../src/utils/transaction-utils.ts | 27 + .../src/utils/validator.ts | 74 + .../tailwind.config.js | 76 + .../tonconnect-demo-unified/tsconfig.app.json | 34 + apps/tonconnect-demo-unified/tsconfig.json | 13 + .../tsconfig.node.json | 26 + apps/tonconnect-demo-unified/vite.config.ts | 14 + pnpm-lock.yaml | 5575 ++++++++++++++++- 98 files changed, 13610 insertions(+), 234 deletions(-) create mode 100644 apps/tonconnect-demo-unified/.gitignore create mode 100644 apps/tonconnect-demo-unified/README.md create mode 100644 apps/tonconnect-demo-unified/components.json create mode 100644 apps/tonconnect-demo-unified/eslint.config.js create mode 100644 apps/tonconnect-demo-unified/index.html create mode 100644 apps/tonconnect-demo-unified/package.json create mode 100644 apps/tonconnect-demo-unified/public/mockServiceWorker.js create mode 100644 apps/tonconnect-demo-unified/public/vite.svg create mode 100644 apps/tonconnect-demo-unified/src/App.tsx create mode 100644 apps/tonconnect-demo-unified/src/assets/react.svg create mode 100644 apps/tonconnect-demo-unified/src/components/DemoContent.tsx create mode 100644 apps/tonconnect-demo-unified/src/components/Header.tsx create mode 100644 apps/tonconnect-demo-unified/src/components/NetworkPicker.tsx create mode 100644 apps/tonconnect-demo-unified/src/components/shared/ExpandableDocsCard.tsx create mode 100644 apps/tonconnect-demo-unified/src/components/shared/FieldInfoModal.tsx create mode 100644 apps/tonconnect-demo-unified/src/components/shared/FieldLabel.tsx create mode 100644 apps/tonconnect-demo-unified/src/components/shared/FormContainer.tsx create mode 100644 apps/tonconnect-demo-unified/src/components/shared/HistoryList.tsx create mode 100644 apps/tonconnect-demo-unified/src/components/shared/HowItWorksCard.tsx create mode 100644 apps/tonconnect-demo-unified/src/components/shared/JsonDisplay.tsx create mode 100644 apps/tonconnect-demo-unified/src/components/shared/JsonViewer.tsx create mode 100644 apps/tonconnect-demo-unified/src/components/shared/ResultCard.tsx create mode 100644 apps/tonconnect-demo-unified/src/components/shared/SignDataResultCard.tsx create mode 100644 apps/tonconnect-demo-unified/src/components/shared/TransactionResult.tsx create mode 100644 apps/tonconnect-demo-unified/src/components/tabs/SettingsTab.tsx create mode 100644 apps/tonconnect-demo-unified/src/components/tabs/SignDataTab.tsx create mode 100644 apps/tonconnect-demo-unified/src/components/tabs/SubscriptionTab.tsx create mode 100644 apps/tonconnect-demo-unified/src/components/tabs/TonProofTab.tsx create mode 100644 apps/tonconnect-demo-unified/src/components/tabs/TransactionTab.tsx create mode 100644 apps/tonconnect-demo-unified/src/components/tabs/index.ts create mode 100644 apps/tonconnect-demo-unified/src/components/ui/alert.tsx create mode 100644 apps/tonconnect-demo-unified/src/components/ui/badge.tsx create mode 100644 apps/tonconnect-demo-unified/src/components/ui/button.tsx create mode 100644 apps/tonconnect-demo-unified/src/components/ui/card.tsx create mode 100644 apps/tonconnect-demo-unified/src/components/ui/checkbox.tsx create mode 100644 apps/tonconnect-demo-unified/src/components/ui/collapsible.tsx create mode 100644 apps/tonconnect-demo-unified/src/components/ui/dialog.tsx create mode 100644 apps/tonconnect-demo-unified/src/components/ui/dropdown-menu.tsx create mode 100644 apps/tonconnect-demo-unified/src/components/ui/input.tsx create mode 100644 apps/tonconnect-demo-unified/src/components/ui/label.tsx create mode 100644 apps/tonconnect-demo-unified/src/components/ui/scroll-area.tsx create mode 100644 apps/tonconnect-demo-unified/src/components/ui/select.tsx create mode 100644 apps/tonconnect-demo-unified/src/components/ui/sonner.tsx create mode 100644 apps/tonconnect-demo-unified/src/components/ui/switch.tsx create mode 100644 apps/tonconnect-demo-unified/src/components/ui/tabs.tsx create mode 100644 apps/tonconnect-demo-unified/src/components/ui/textarea.tsx create mode 100644 apps/tonconnect-demo-unified/src/context/SettingsContext.tsx create mode 100644 apps/tonconnect-demo-unified/src/data/field-info.ts create mode 100644 apps/tonconnect-demo-unified/src/docs/fields/sign-data/bytes.md create mode 100644 apps/tonconnect-demo-unified/src/docs/fields/sign-data/cell.md create mode 100644 apps/tonconnect-demo-unified/src/docs/fields/sign-data/data-type.md create mode 100644 apps/tonconnect-demo-unified/src/docs/fields/sign-data/schema.md create mode 100644 apps/tonconnect-demo-unified/src/docs/fields/sign-data/text.md create mode 100644 apps/tonconnect-demo-unified/src/docs/fields/transaction/address.md create mode 100644 apps/tonconnect-demo-unified/src/docs/fields/transaction/amount.md create mode 100644 apps/tonconnect-demo-unified/src/docs/fields/transaction/from.md create mode 100644 apps/tonconnect-demo-unified/src/docs/fields/transaction/messages.md create mode 100644 apps/tonconnect-demo-unified/src/docs/fields/transaction/network.md create mode 100644 apps/tonconnect-demo-unified/src/docs/fields/transaction/payload.md create mode 100644 apps/tonconnect-demo-unified/src/docs/fields/transaction/state-init.md create mode 100644 apps/tonconnect-demo-unified/src/docs/fields/transaction/valid-until.md create mode 100644 apps/tonconnect-demo-unified/src/docs/sections/sign-data.md create mode 100644 apps/tonconnect-demo-unified/src/docs/sections/transaction.md create mode 100644 apps/tonconnect-demo-unified/src/hooks/useHistory.ts create mode 100644 apps/tonconnect-demo-unified/src/hooks/useSettings.ts create mode 100644 apps/tonconnect-demo-unified/src/hooks/useSignData.ts create mode 100644 apps/tonconnect-demo-unified/src/hooks/useTheme.ts create mode 100644 apps/tonconnect-demo-unified/src/hooks/useTonProof.ts create mode 100644 apps/tonconnect-demo-unified/src/hooks/useTransaction.ts create mode 100644 apps/tonconnect-demo-unified/src/hooks/useTransactionTracker.ts create mode 100644 apps/tonconnect-demo-unified/src/index.css create mode 100644 apps/tonconnect-demo-unified/src/lib/codemirror-theme.ts create mode 100644 apps/tonconnect-demo-unified/src/lib/utils.ts create mode 100644 apps/tonconnect-demo-unified/src/main.tsx create mode 100644 apps/tonconnect-demo-unified/src/polyfills.ts create mode 100644 apps/tonconnect-demo-unified/src/schemas/sign-data.schema.json create mode 100644 apps/tonconnect-demo-unified/src/schemas/transaction.schema.json create mode 100644 apps/tonconnect-demo-unified/src/server/handlers/check-proof.ts create mode 100644 apps/tonconnect-demo-unified/src/server/handlers/check-sign-data.ts create mode 100644 apps/tonconnect-demo-unified/src/server/handlers/find-transaction.ts create mode 100644 apps/tonconnect-demo-unified/src/server/handlers/generate-payload.ts create mode 100644 apps/tonconnect-demo-unified/src/server/handlers/get-account-info.ts create mode 100644 apps/tonconnect-demo-unified/src/server/handlers/index.ts create mode 100644 apps/tonconnect-demo-unified/src/server/index.ts create mode 100644 apps/tonconnect-demo-unified/src/server/services/ton-api.service.ts create mode 100644 apps/tonconnect-demo-unified/src/server/services/ton-proof.service.ts create mode 100644 apps/tonconnect-demo-unified/src/server/utils/jwt.ts create mode 100644 apps/tonconnect-demo-unified/src/server/utils/wallet-parsers.ts create mode 100644 apps/tonconnect-demo-unified/src/utils/explorer-utils.ts create mode 100644 apps/tonconnect-demo-unified/src/utils/sign-data-verification.ts create mode 100644 apps/tonconnect-demo-unified/src/utils/transaction-utils.ts create mode 100644 apps/tonconnect-demo-unified/src/utils/validator.ts create mode 100644 apps/tonconnect-demo-unified/tailwind.config.js create mode 100644 apps/tonconnect-demo-unified/tsconfig.app.json create mode 100644 apps/tonconnect-demo-unified/tsconfig.json create mode 100644 apps/tonconnect-demo-unified/tsconfig.node.json create mode 100644 apps/tonconnect-demo-unified/vite.config.ts diff --git a/apps/tonconnect-demo-unified/.gitignore b/apps/tonconnect-demo-unified/.gitignore new file mode 100644 index 000000000..a547bf36d --- /dev/null +++ b/apps/tonconnect-demo-unified/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/apps/tonconnect-demo-unified/README.md b/apps/tonconnect-demo-unified/README.md new file mode 100644 index 000000000..d2e77611f --- /dev/null +++ b/apps/tonconnect-demo-unified/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/apps/tonconnect-demo-unified/components.json b/apps/tonconnect-demo-unified/components.json new file mode 100644 index 000000000..1537d5030 --- /dev/null +++ b/apps/tonconnect-demo-unified/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/apps/tonconnect-demo-unified/eslint.config.js b/apps/tonconnect-demo-unified/eslint.config.js new file mode 100644 index 000000000..5e6b472f5 --- /dev/null +++ b/apps/tonconnect-demo-unified/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/apps/tonconnect-demo-unified/index.html b/apps/tonconnect-demo-unified/index.html new file mode 100644 index 000000000..8017610e4 --- /dev/null +++ b/apps/tonconnect-demo-unified/index.html @@ -0,0 +1,13 @@ + + + + + + + tonconnect-demo-unified + + +
+ + + diff --git a/apps/tonconnect-demo-unified/package.json b/apps/tonconnect-demo-unified/package.json new file mode 100644 index 000000000..f8827ab18 --- /dev/null +++ b/apps/tonconnect-demo-unified/package.json @@ -0,0 +1,80 @@ +{ + "name": "tonconnect-demo-unified", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@codemirror/lang-json": "^6.0.2", + "@codemirror/language": "^6.12.1", + "@codemirror/view": "^6.39.11", + "@hookform/resolvers": "^5.2.2", + "@lezer/highlight": "^1.2.3", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-toast": "^1.2.15", + "@tailwindcss/typography": "^0.5.19", + "@tailwindcss/vite": "^4.1.18", + "@ton/core": "^0.62.1", + "@ton/crypto": "3.3.0", + "@ton/ton": "^16.1.0", + "@tonconnect/sdk": "workspace:*", + "@tonconnect/ui-react": "workspace:*", + "@uiw/react-codemirror": "^4.25.4", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "autoprefixer": "^10.4.23", + "buffer": "^6.0.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "crc-32": "^1.2.2", + "jose": "^6.1.3", + "lucide-react": "^0.562.0", + "msw": "^2.12.7", + "next-themes": "^0.4.6", + "postcss": "^8.5.6", + "react": "^19.2.3", + "react-dom": "^19.2.3", + "react-markdown": "^10.1.0", + "remark-gfm": "^4.0.1", + "sonner": "^2.0.7", + "tailwind-merge": "^3.4.0", + "tailwindcss": "^4.1.18", + "tailwindcss-animate": "^1.0.7", + "tweetnacl": "^1.0.3", + "yaml": "^2.8.2", + "zod": "^4.3.5" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.8", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + }, + "msw": { + "workerDirectory": [ + "public" + ] + } +} \ No newline at end of file diff --git a/apps/tonconnect-demo-unified/public/mockServiceWorker.js b/apps/tonconnect-demo-unified/public/mockServiceWorker.js new file mode 100644 index 000000000..461e2600e --- /dev/null +++ b/apps/tonconnect-demo-unified/public/mockServiceWorker.js @@ -0,0 +1,349 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker. + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + */ + +const PACKAGE_VERSION = '2.12.7' +const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const activeClientIds = new Set() + +addEventListener('install', function () { + self.skipWaiting() +}) + +addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +addEventListener('message', async function (event) { + const clientId = Reflect.get(event.source || {}, 'id') + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: { + client: { + id: client.id, + frameType: client.frameType, + }, + }, + }) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +addEventListener('fetch', function (event) { + const requestInterceptedAt = Date.now() + + // Bypass navigation requests. + if (event.request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if ( + event.request.cache === 'only-if-cached' && + event.request.mode !== 'same-origin' + ) { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been terminated (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId, requestInterceptedAt)) +}) + +/** + * @param {FetchEvent} event + * @param {string} requestId + * @param {number} requestInterceptedAt + */ +async function handleRequest(event, requestId, requestInterceptedAt) { + const client = await resolveMainClient(event) + const requestCloneForEvents = event.request.clone() + const response = await getResponse( + event, + client, + requestId, + requestInterceptedAt, + ) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + const serializedRequest = await serializeRequest(requestCloneForEvents) + + // Clone the response so both the client and the library could consume it. + const responseClone = response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + isMockedResponse: IS_MOCKED_RESPONSE in response, + request: { + id: requestId, + ...serializedRequest, + }, + response: { + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + headers: Object.fromEntries(responseClone.headers.entries()), + body: responseClone.body, + }, + }, + }, + responseClone.body ? [serializedRequest.body, responseClone.body] : [], + ) + } + + return response +} + +/** + * Resolve the main client for the given event. + * Client that issues a request doesn't necessarily equal the client + * that registered the worker. It's with the latter the worker should + * communicate with during the response resolving phase. + * @param {FetchEvent} event + * @returns {Promise} + */ +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (activeClientIds.has(event.clientId)) { + return client + } + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +/** + * @param {FetchEvent} event + * @param {Client | undefined} client + * @param {string} requestId + * @param {number} requestInterceptedAt + * @returns {Promise} + */ +async function getResponse(event, client, requestId, requestInterceptedAt) { + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = event.request.clone() + + function passthrough() { + // Cast the request headers to a new Headers instance + // so the headers can be manipulated with. + const headers = new Headers(requestClone.headers) + + // Remove the "accept" header value that marked this request as passthrough. + // This prevents request alteration and also keeps it compliant with the + // user-defined CORS policies. + const acceptHeader = headers.get('accept') + if (acceptHeader) { + const values = acceptHeader.split(',').map((value) => value.trim()) + const filteredValues = values.filter( + (value) => value !== 'msw/passthrough', + ) + + if (filteredValues.length > 0) { + headers.set('accept', filteredValues.join(', ')) + } else { + headers.delete('accept') + } + } + + return fetch(requestClone, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const serializedRequest = await serializeRequest(event.request) + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + interceptedAt: requestInterceptedAt, + ...serializedRequest, + }, + }, + [serializedRequest.body], + ) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'PASSTHROUGH': { + return passthrough() + } + } + + return passthrough() +} + +/** + * @param {Client} client + * @param {any} message + * @param {Array} transferrables + * @returns {Promise} + */ +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage(message, [ + channel.port2, + ...transferrables.filter(Boolean), + ]) + }) +} + +/** + * @param {Response} response + * @returns {Response} + */ +function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, + }) + + return mockedResponse +} + +/** + * @param {Request} request + */ +async function serializeRequest(request) { + return { + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: await request.arrayBuffer(), + keepalive: request.keepalive, + } +} diff --git a/apps/tonconnect-demo-unified/public/vite.svg b/apps/tonconnect-demo-unified/public/vite.svg new file mode 100644 index 000000000..e7b8dfb1b --- /dev/null +++ b/apps/tonconnect-demo-unified/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/tonconnect-demo-unified/src/App.tsx b/apps/tonconnect-demo-unified/src/App.tsx new file mode 100644 index 000000000..a08e003a9 --- /dev/null +++ b/apps/tonconnect-demo-unified/src/App.tsx @@ -0,0 +1,12 @@ +import { TonConnectUIProvider } from '@tonconnect/ui-react' +import { DemoContent } from '@/components/DemoContent' + +function App() { + return ( + + + + ) +} + +export default App diff --git a/apps/tonconnect-demo-unified/src/assets/react.svg b/apps/tonconnect-demo-unified/src/assets/react.svg new file mode 100644 index 000000000..6c87de9bb --- /dev/null +++ b/apps/tonconnect-demo-unified/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/tonconnect-demo-unified/src/components/DemoContent.tsx b/apps/tonconnect-demo-unified/src/components/DemoContent.tsx new file mode 100644 index 000000000..ad8821624 --- /dev/null +++ b/apps/tonconnect-demo-unified/src/components/DemoContent.tsx @@ -0,0 +1,54 @@ +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { Toaster } from "@/components/ui/sonner" +import { SettingsProvider } from "@/context/SettingsContext" +import { Header } from "./Header" +import { TransactionTab, SignDataTab, SubscriptionTab, TonProofTab, SettingsTab } from "./tabs" + +function DemoContentInner() { + return ( +
+
+ +
+ + + Transaction + Sign Data + Subscription + Ton Proof + Settings + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ ) +} + +export function DemoContent() { + return ( + + + + ) +} diff --git a/apps/tonconnect-demo-unified/src/components/Header.tsx b/apps/tonconnect-demo-unified/src/components/Header.tsx new file mode 100644 index 000000000..3cf5ad5eb --- /dev/null +++ b/apps/tonconnect-demo-unified/src/components/Header.tsx @@ -0,0 +1,34 @@ +import { TonConnectButton } from "@tonconnect/ui-react" +import { Button } from "@/components/ui/button" +import { useSettingsContext } from "@/context/SettingsContext" +import { Moon, Sun } from "lucide-react" + +export function Header() { + const { theme, setTheme } = useSettingsContext() + + return ( +
+
+
+

+ TonConnect Demo +

+

+ Test and demonstrate wallet integration capabilities +

+
+
+ + +
+
+
+ ) +} diff --git a/apps/tonconnect-demo-unified/src/components/NetworkPicker.tsx b/apps/tonconnect-demo-unified/src/components/NetworkPicker.tsx new file mode 100644 index 000000000..9dc8e252a --- /dev/null +++ b/apps/tonconnect-demo-unified/src/components/NetworkPicker.tsx @@ -0,0 +1,67 @@ +import { useEffect } from "react" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { Label } from "@/components/ui/label" +import { useSettingsContext } from "@/context/SettingsContext" +import { useTonConnectUI, useTonWallet } from "@tonconnect/ui-react" + +function getNetworkLabel(chain: string): string { + switch (chain) { + case "-239": return "Mainnet" + case "-3": return "Testnet" + default: return chain + } +} + +export function NetworkPicker() { + const { selectedNetwork, setSelectedNetwork } = useSettingsContext() + const [tonConnectUI] = useTonConnectUI() + const wallet = useTonWallet() + + const isConnected = !!wallet + const walletNetwork = wallet?.account?.chain + + // Sync selected network with TonConnect SDK (only when not connected) + useEffect(() => { + if (!isConnected) { + const chainId = selectedNetwork || undefined + tonConnectUI.setConnectionNetwork(chainId) + } + }, [selectedNetwork, tonConnectUI, isConnected]) + + // When connected, show wallet's network + // When not connected, show selected network or "any" + const displayValue = isConnected && walletNetwork ? walletNetwork : (selectedNetwork || "any") + const handleChange = (v: string) => setSelectedNetwork(v === "any" ? "" : v) + + return ( +
+ + + {isConnected && ( +

+ Network is determined by connected wallet +

+ )} +
+ ) +} diff --git a/apps/tonconnect-demo-unified/src/components/shared/ExpandableDocsCard.tsx b/apps/tonconnect-demo-unified/src/components/shared/ExpandableDocsCard.tsx new file mode 100644 index 000000000..18f906252 --- /dev/null +++ b/apps/tonconnect-demo-unified/src/components/shared/ExpandableDocsCard.tsx @@ -0,0 +1,103 @@ +import { useState } from "react" +import ReactMarkdown from "react-markdown" +import remarkGfm from "remark-gfm" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { ExternalLink, ChevronDown, ChevronUp } from "lucide-react" +import { getSectionInfo } from "@/data/field-info" + +interface Step { + title: string + description: string +} + +interface ExpandableDocsCardProps { + sectionId: string + steps: Step[] +} + +export function ExpandableDocsCard({ sectionId, steps }: ExpandableDocsCardProps) { + const [expanded, setExpanded] = useState(false) + const info = getSectionInfo(sectionId) + + if (!info) return null + + return ( + + +
+ {info.name} + {expanded && ( + + )} +
+

{info.summary}

+
+ + + {!expanded && ( + <> + {/* Collapsed: Steps grid */} +
+ {steps.map((step, i) => ( +
+

{step.title}

+

{step.description}

+
+ ))} +
+ + {/* Expand button */} + + + )} + + {expanded && ( + /* Expanded: Full markdown */ +
+ + {info.content} + +
+ )} + + {/* Links (always visible) */} + {info.links && info.links.length > 0 && ( +
+ {info.links.map((link, i) => ( + + {link.title} + + + ))} +
+ )} +
+
+ ) +} diff --git a/apps/tonconnect-demo-unified/src/components/shared/FieldInfoModal.tsx b/apps/tonconnect-demo-unified/src/components/shared/FieldInfoModal.tsx new file mode 100644 index 000000000..de5628cf0 --- /dev/null +++ b/apps/tonconnect-demo-unified/src/components/shared/FieldInfoModal.tsx @@ -0,0 +1,48 @@ +import ReactMarkdown from "react-markdown" +import remarkGfm from "remark-gfm" +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import type { FieldInfo } from "@/data/field-info" + +interface FieldInfoModalProps { + open: boolean + onOpenChange: (open: boolean) => void + info: FieldInfo +} + +export function FieldInfoModal({ open, onOpenChange, info }: FieldInfoModalProps) { + return ( + + + + {info.name} +

{info.summary}

+
+ +
+ {info.content} +
+ + {info.links && info.links.length > 0 && ( +
+ {info.links.map((link, index) => ( + + {link.title} ↗ + + ))} +
+ )} +
+
+ ) +} diff --git a/apps/tonconnect-demo-unified/src/components/shared/FieldLabel.tsx b/apps/tonconnect-demo-unified/src/components/shared/FieldLabel.tsx new file mode 100644 index 000000000..b0ef822d3 --- /dev/null +++ b/apps/tonconnect-demo-unified/src/components/shared/FieldLabel.tsx @@ -0,0 +1,35 @@ +import { useState, type ReactNode } from "react" +import { Label } from "@/components/ui/label" +import { FieldInfoModal } from "./FieldInfoModal" +import { getFieldInfo } from "@/data/field-info" +import { Info } from "lucide-react" + +interface FieldLabelProps { + htmlFor?: string + fieldId: string + children: ReactNode + className?: string +} + +export function FieldLabel({ htmlFor, fieldId, children, className }: FieldLabelProps) { + const [open, setOpen] = useState(false) + const info = getFieldInfo(fieldId) + + return ( + <> +
+ + {info && ( + + )} +
+ {info && } + + ) +} diff --git a/apps/tonconnect-demo-unified/src/components/shared/FormContainer.tsx b/apps/tonconnect-demo-unified/src/components/shared/FormContainer.tsx new file mode 100644 index 000000000..66fc5f054 --- /dev/null +++ b/apps/tonconnect-demo-unified/src/components/shared/FormContainer.tsx @@ -0,0 +1,413 @@ +import { useState, useEffect, useMemo, useRef } from "react" +import type { ReactNode } from "react" +import CodeMirror from "@uiw/react-codemirror" +import { json } from "@codemirror/lang-json" +import { Card, CardContent, CardHeader } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Alert, AlertDescription } from "@/components/ui/alert" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { JsonViewer } from "./JsonViewer" +import { ResultCard } from "./ResultCard" +import { AlertCircle, AlertTriangle, Copy, ChevronDown, RotateCcw, Loader2 } from "lucide-react" +import { toast } from "sonner" +import { useSettingsContext } from "@/context/SettingsContext" +import { createTonConnectTheme } from "@/lib/codemirror-theme" +import type { ValidationResult } from "@/utils/validator" +import type { OperationResult } from "@/hooks/useTransaction" + +export interface PresetOption { + id: string + name: string + description: string +} + +type EditorMode = "form" | "raw" + +interface FormContainerProps { + // Metadata + title: string + submitButtonText?: string + codeEditorHeight?: string + + // Content + formContent: ReactNode + requestJson: string + + // Callbacks + onJsonChange?: (json: string) => void + onSend: () => void + onSendRaw?: (json: string) => void + + // Validation + validateJson?: (json: string) => ValidationResult + + // State + isConnected: boolean + isLoading?: boolean + + // Presets + presets?: PresetOption[] + onPresetSelect?: (presetId: string) => void + + // Result + lastResult?: OperationResult | null + onClearResult?: () => void + onLoadResult?: () => void +} + +function isValidJson(str: string): boolean { + try { + JSON.parse(str) + return true + } catch { + return false + } +} + +export function FormContainer({ + title, + submitButtonText = "Send Transaction", + codeEditorHeight = "400px", + formContent, + requestJson, + onJsonChange, + onSend, + onSendRaw, + validateJson, + isConnected, + isLoading = false, + presets, + onPresetSelect, + lastResult, + onClearResult, + onLoadResult, +}: FormContainerProps) { + const [mode, setMode] = useState("form") + const [editedJson, setEditedJson] = useState(requestJson) + const [validationResult, setValidationResult] = useState(null) + const resultRef = useRef(null) + + const { theme } = useSettingsContext() + + // Determine if dark mode based on theme setting + const isDark = useMemo(() => { + if (theme === "system") { + return typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches + } + return theme === "dark" + }, [theme]) + + // Create reactive CodeMirror theme + const codemirrorTheme = useMemo(() => createTonConnectTheme(isDark), [isDark]) + + // Sync JSON when in form mode or when requestJson changes + useEffect(() => { + if (mode === "form") { + setEditedJson(requestJson) + setValidationResult(null) // Clear validation when switching to form + } + }, [requestJson, mode]) + + // Smart scroll to result when it appears + useEffect(() => { + if (lastResult && resultRef.current) { + const rect = resultRef.current.getBoundingClientRect() + if (rect.top > window.innerHeight) { + resultRef.current.scrollIntoView({ behavior: "smooth", block: "nearest" }) + } + } + }, [lastResult?.id]) + + // Handle mode switch + const handleModeChange = (newMode: EditorMode) => { + if (newMode === mode) return + + if (mode === "raw" && newMode === "form") { + // Code → Form: check validity first + if (!isValidJson(editedJson)) { + // Syntax error - confirm discard + if (!confirm("Invalid JSON syntax. Discard changes and switch to Form?")) { + return + } + setEditedJson(requestJson) + setValidationResult(null) + setMode("form") + return + } + + // Check schema validation + if (validateJson) { + const result = validateJson(editedJson) + if (!result.valid) { + // Schema warnings - confirm discard + if (!confirm("JSON has validation errors. Some data may be lost. Switch to Form anyway?")) { + return + } + } + } + + // Apply changes + onJsonChange?.(editedJson) + setValidationResult(null) + } + + if (mode === "form" && newMode === "raw") { + // Form → Code: sync JSON + setEditedJson(requestJson) + setValidationResult(null) + } + + setMode(newMode) + } + + // Reset editor to form state + const handleReset = () => { + setEditedJson(requestJson) + setValidationResult(null) + toast.success("Reset to form state") + } + + // Actually send the transaction (internal) + const doSend = () => { + if (onSendRaw) { + onSendRaw(editedJson) + } else { + onJsonChange?.(editedJson) + onSend() + } + } + + // Handle send - validates first in Code mode + const handleSend = () => { + if (mode === "form") { + onSend() + } else { + // Code mode - validate before sending + if (!isValidJson(editedJson)) { + setValidationResult({ valid: false, errors: [{ path: "root", message: "Invalid JSON syntax" }] }) + return + } + + // Run schema validation + if (validateJson) { + const result = validateJson(editedJson) + if (!result.valid) { + // Show warnings and DON'T send - user must click "Send Anyway" + setValidationResult(result) + return + } + } + + // All validation passed - send + doSend() + } + } + + // Send anyway - bypasses schema validation (but still checks syntax) + const handleSendAnyway = () => { + if (!isValidJson(editedJson)) { + return // Syntax errors still block + } + setValidationResult(null) + doSend() + } + + // Determine validation state (only shown after Send click) + const hasSyntaxError = validationResult?.errors.some(e => e.message === "Invalid JSON syntax") + const hasSchemaWarnings = validationResult && !validationResult.valid && !hasSyntaxError + + // Send button disabled state - disabled if not connected or loading + const sendDisabled = !isConnected || isLoading + + return ( +
+ + {/* Header: Title + Toggle + Send */} + +

{title}

+ +
+ {/* Presets Dropdown */} + {presets && presets.length > 0 && ( + + + + + + {presets.map((preset) => ( + onPresetSelect?.(preset.id)} + className="flex flex-col items-start gap-0.5 cursor-pointer" + > + {preset.name} + {preset.description} + + ))} + + + )} + + {/* Segmented Toggle */} +
+ + +
+ + {/* Send Button */} + +
+
+ + + {mode === "form" ? ( + // Form mode: 2 columns in one card +
+ {/* LEFT: Form */} +
+

Configure

+ {formContent} +
+ + {/* RIGHT: Preview (with left border on lg) */} +
+
+

Request Preview

+ +
+ +
+
+ ) : ( + // Code mode: Full width editor with toolbar +
+ {/* Toolbar */} +
+ + +
+ + { + setEditedJson(value) + // Clear validation when user edits + if (validationResult) { + setValidationResult(null) + } + }} + extensions={[json(), ...codemirrorTheme]} + theme="none" + height={codeEditorHeight} + className="rounded-md border overflow-hidden" + /> + + {/* Syntax Error (shown after Send attempt) */} + {hasSyntaxError && ( + + + Invalid JSON syntax. Please fix before sending. + + )} + + {/* Schema Warnings (shown after Send attempt, blocks until "Send Anyway") */} + {hasSchemaWarnings && ( + + + + + {validationResult!.errors.map(e => `${e.path}: ${e.message}`).join("; ")} + + + + + )} + +
+ )} +
+
+ + {/* Result Card - ALWAYS visible (regardless of mode) */} + {lastResult && ( +
+ +
+ )} +
+ ) +} diff --git a/apps/tonconnect-demo-unified/src/components/shared/HistoryList.tsx b/apps/tonconnect-demo-unified/src/components/shared/HistoryList.tsx new file mode 100644 index 000000000..5c8c811c9 --- /dev/null +++ b/apps/tonconnect-demo-unified/src/components/shared/HistoryList.tsx @@ -0,0 +1,426 @@ +import { useState, useMemo, useCallback } from "react" +import { fromNano } from "@ton/core" +import { Button } from "@/components/ui/button" +import { Label } from "@/components/ui/label" +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible" +import { JsonViewer } from "./JsonViewer" +import { useHistory, type HistoryEntry } from "@/hooks/useHistory" +import { + ChevronDown, + ChevronRight, + CheckCircle, + XCircle, + Clock, + RotateCcw, + Copy, + Trash2, + History, + ExternalLink, + Search, + Loader2, +} from "lucide-react" +import { toast } from "sonner" +import { getExplorerUrl } from "@/utils/explorer-utils" +import { getNormalizedExtMessageHash } from "@/utils/transaction-utils" + +interface HistoryListProps { + currentWallet: string | null + onLoadToForm: (requestRaw: string) => void +} + +function formatTime(timestamp: number): string { + const now = Date.now() + const diff = now - timestamp + + // Less than 1 minute ago + if (diff < 60000) { + return "just now" + } + + // Less than 1 hour ago + if (diff < 3600000) { + const mins = Math.floor(diff / 60000) + return `${mins}m ago` + } + + // Today - show time + const date = new Date(timestamp) + const today = new Date() + if (date.toDateString() === today.toDateString()) { + return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) + } + + // Yesterday + const yesterday = new Date(today) + yesterday.setDate(yesterday.getDate() - 1) + if (date.toDateString() === yesterday.toDateString()) { + return "yesterday" + } + + // Older - show date + return date.toLocaleDateString([], { month: "short", day: "numeric" }) +} + +function formatAmount(request: Record): string { + try { + const messages = request.messages as Array<{ amount?: string }> | undefined + if (messages && messages.length > 0) { + const totalNano = messages.reduce((sum, msg) => { + return sum + BigInt(msg.amount || "0") + }, BigInt(0)) + const ton = fromNano(totalNano) + const num = parseFloat(ton) + if (num >= 1) { + return `${num.toFixed(2)} TON` + } + if (num >= 0.01) { + return `${num.toFixed(3)} TON` + } + return `${ton} TON` + } + } catch { + // Ignore + } + return "" +} + +function getMessageCount(request: Record): number { + const messages = request.messages as Array | undefined + return messages?.length || 0 +} + +function StatusIcon({ status }: { status: HistoryEntry["status"] }) { + switch (status) { + case "confirmed": + return + case "success": + return + case "error": + return + case "expired": + return + default: + return + } +} + +function statusLabel(status: HistoryEntry["status"]): string { + switch (status) { + case "confirmed": + return "Confirmed" + case "success": + return "Sent" + case "error": + return "Error" + case "expired": + return "Expired" + default: + return status + } +} + +interface HistoryEntryRowProps { + entry: HistoryEntry + expanded: boolean + onToggle: () => void + onLoadToForm: () => void + onDelete: () => void +} + +interface TransactionDetails { + lt: string + hash: string + fee: string + timestamp: number +} + +function HistoryEntryRow({ + entry, + expanded, + onToggle, + onLoadToForm, + onDelete, +}: HistoryEntryRowProps) { + const amount = formatAmount(entry.request) + const msgCount = getMessageCount(entry.request) + + // Compute hash from BOC (only if boc exists) + const hash = useMemo(() => { + if (!entry.boc) return null + try { + return getNormalizedExtMessageHash(entry.boc) + } catch { + return null + } + }, [entry.boc]) + + // Blockchain check state + const [checkLoading, setCheckLoading] = useState(false) + const [txDetails, setTxDetails] = useState(null) + const [checkError, setCheckError] = useState(null) + + const checkBlockchain = useCallback(async () => { + if (!hash) return + + setCheckLoading(true) + setCheckError(null) + + const endpoint = entry.network === "testnet" + ? "https://testnet.toncenter.com/api/v3" + : "https://toncenter.com/api/v3" + + try { + const response = await fetch( + `${endpoint}/transactionsByMessage?msg_hash=${hash}&direction=in` + ) + const data = await response.json() + + if (data.transactions?.length > 0) { + const tx = data.transactions[0] + setTxDetails({ + lt: tx.lt, + hash: tx.hash, + fee: tx.total_fees, + timestamp: tx.now, + }) + } else { + setCheckError("Transaction not found in blockchain") + } + } catch (err) { + setCheckError(err instanceof Error ? err.message : "Network error") + } finally { + setCheckLoading(false) + } + }, [hash, entry.network]) + + return ( +
+ {/* Collapsed row - clickable header */} + + + {/* Expanded content */} + {expanded && ( +
+ {/* 2-column layout for Request / Response+Hash */} +
+ {/* Left column: Request */} +
+ +
+ + {/* Right column: Response + Hash + Blockchain */} +
+ + + {/* Hash with explorer link */} + {hash && ( +
+
+ +
+ + +
+
+ + {hash} + + + {/* Check blockchain button */} + {!txDetails && ( + + )} + + {/* Error */} + {checkError && ( +

{checkError}

+ )} + + {/* Transaction details */} + {txDetails && ( + + + Transaction Confirmed + + LT: {txDetails.lt} • Fee: {txDetails.fee} nanotons • {new Date(txDetails.timestamp * 1000).toLocaleString()} + + + )} +
+ )} +
+
+ + {/* Actions */} +
+ + +
+
+ )} +
+ ) +} + +export function HistoryList({ currentWallet, onLoadToForm }: HistoryListProps) { + const history = useHistory() + const [expanded, setExpanded] = useState>({}) + const [sectionOpen, setSectionOpen] = useState(true) + + const entries = useMemo(() => { + return currentWallet ? history.getByWallet(currentWallet) : [] + }, [currentWallet, history]) + + if (!currentWallet || entries.length === 0) { + return null + } + + const toggleEntry = (id: string) => { + setExpanded(prev => ({ ...prev, [id]: !prev[id] })) + } + + const handleClear = () => { + if (currentWallet) { + history.clearWallet(currentWallet) + } + } + + return ( + + {/* Section header */} +
+ + {sectionOpen ? ( + + ) : ( + + )} + + HISTORY ({entries.length}) + + + +
+ + +
+ {entries.map(entry => ( + toggleEntry(entry.id)} + onLoadToForm={() => onLoadToForm(entry.requestRaw)} + onDelete={() => history.deleteEntry(entry.id)} + /> + ))} +
+
+
+ ) +} diff --git a/apps/tonconnect-demo-unified/src/components/shared/HowItWorksCard.tsx b/apps/tonconnect-demo-unified/src/components/shared/HowItWorksCard.tsx new file mode 100644 index 000000000..c6f81bee7 --- /dev/null +++ b/apps/tonconnect-demo-unified/src/components/shared/HowItWorksCard.tsx @@ -0,0 +1,164 @@ +import { useMemo } from "react" +import ReactMarkdown from "react-markdown" +import remarkGfm from "remark-gfm" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { ExternalLink, Check } from "lucide-react" +import { getSectionInfo } from "@/data/field-info" + +// Feature lists for each section +const SECTION_FEATURES: Record = { + transaction: [ + "Multiple messages per transaction", + "Custom payloads for smart contracts", + "Contract deployment support", + "Batch operations", + ], + signData: [ + "Text, binary, cell formats", + "Domain-bound signatures", + "Timestamp protection", + "Off-chain verification", + ], +} + +interface HowItWorksCardProps { + sectionId: string +} + +export function HowItWorksCard({ sectionId }: HowItWorksCardProps) { + const info = getSectionInfo(sectionId) + const features = SECTION_FEATURES[sectionId] || [] + + // Extract h2 headings from markdown for navigation + const headings = useMemo(() => { + if (!info?.content) return [] + const matches = info.content.match(/^## (.+)$/gm) + return matches?.map(h => h.replace("## ", "")) || [] + }, [info?.content]) + + if (!info) return null + + const scrollToHeading = (heading: string) => { + // Find the heading element and scroll to it + const slug = heading.toLowerCase().replace(/\s+/g, "-").replace(/[^\w-]/g, "") + const element = document.getElementById(slug) + element?.scrollIntoView({ behavior: "smooth", block: "start" }) + } + + return ( + + + {info.name} + + +
+ + {/* LEFT: Summary - hidden on mobile */} +
+ {/* Summary */} +

{info.summary}

+ + {/* Features checklist */} + {features.length > 0 && ( +
+

Key Features

+
    + {features.map((feature, i) => ( +
  • + + {feature} +
  • + ))} +
+
+ )} + + {/* Navigation */} + {headings.length > 0 && ( + + )} + + {/* Links */} + {info.links && info.links.length > 0 && ( +
+ {info.links.map((link, i) => ( + + {link.title} + + + ))} +
+ )} +
+ + {/* RIGHT: Full markdown content */} +
+ { + const text = String(children) + const id = text.toLowerCase().replace(/\s+/g, "-").replace(/[^\w-]/g, "") + return

{children}

+ } + }} + > + {info.content} +
+
+ +
+ + {/* Links on mobile - shown only on mobile */} + {info.links && info.links.length > 0 && ( +
+ {info.links.map((link, i) => ( + + {link.title} + + + ))} +
+ )} +
+
+ ) +} diff --git a/apps/tonconnect-demo-unified/src/components/shared/JsonDisplay.tsx b/apps/tonconnect-demo-unified/src/components/shared/JsonDisplay.tsx new file mode 100644 index 000000000..88a3b3430 --- /dev/null +++ b/apps/tonconnect-demo-unified/src/components/shared/JsonDisplay.tsx @@ -0,0 +1,38 @@ +import { Button } from "@/components/ui/button" +import { Label } from "@/components/ui/label" +import { ScrollArea } from "@/components/ui/scroll-area" +import { Copy } from "lucide-react" +import { toast } from "sonner" + +interface JsonDisplayProps { + title: string + json: string +} + +export function JsonDisplay({ title, json }: JsonDisplayProps) { + if (!json) return null + + const copyToClipboard = async () => { + try { + await navigator.clipboard.writeText(json) + toast.success("Copied to clipboard") + } catch { + toast.error("Failed to copy") + } + } + + return ( +
+
+ + +
+ +
{json}
+
+
+ ) +} diff --git a/apps/tonconnect-demo-unified/src/components/shared/JsonViewer.tsx b/apps/tonconnect-demo-unified/src/components/shared/JsonViewer.tsx new file mode 100644 index 000000000..640fe6b34 --- /dev/null +++ b/apps/tonconnect-demo-unified/src/components/shared/JsonViewer.tsx @@ -0,0 +1,197 @@ +import { useMemo, useState } from "react" +import CodeMirror from "@uiw/react-codemirror" +import { json } from "@codemirror/lang-json" +import { EditorView } from "@codemirror/view" +import { Button } from "@/components/ui/button" +import { Label } from "@/components/ui/label" +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible" +import { Copy, ChevronRight, ChevronDown } from "lucide-react" +import { toast } from "sonner" +import { useSettingsContext } from "@/context/SettingsContext" +import { createTonConnectTheme } from "@/lib/codemirror-theme" + +interface JsonViewerProps { + title: string + json: string + /** Default expanded state */ + defaultExpanded?: boolean + /** Max height for long JSON (default 200px) */ + maxHeight?: number + /** Show inline for short JSON (default 80 chars) */ + inlineThreshold?: number + /** Allow collapsing (default true) */ + collapsible?: boolean +} + +export function JsonViewer({ + title, + json: jsonString, + defaultExpanded = true, + maxHeight = 200, + inlineThreshold = 80, + collapsible = true, +}: JsonViewerProps) { + const [expanded, setExpanded] = useState(defaultExpanded) + const { theme } = useSettingsContext() + + const isDark = useMemo(() => { + if (theme === "system") { + return typeof window !== "undefined" && + window.matchMedia("(prefers-color-scheme: dark)").matches + } + return theme === "dark" + }, [theme]) + + const codemirrorTheme = useMemo(() => createTonConnectTheme(isDark), [isDark]) + + // Parse and format JSON + const { formatted, lineCount, isShort } = useMemo(() => { + try { + const parsed = JSON.parse(jsonString) + const formatted = JSON.stringify(parsed, null, 2) + const lines = formatted.split("\n") + return { + formatted, + lineCount: lines.length, + isShort: formatted.length <= inlineThreshold && lines.length <= 2, + } + } catch { + return { formatted: jsonString, lineCount: 1, isShort: jsonString.length <= inlineThreshold } + } + }, [jsonString, inlineThreshold]) + + const copyToClipboard = async (e: React.MouseEvent) => { + e.stopPropagation() // Don't toggle collapsible + try { + await navigator.clipboard.writeText(formatted) + toast.success("Copied to clipboard") + } catch { + toast.error("Failed to copy") + } + } + + // Calculate height: line-height ~18px + padding + const calculatedHeight = useMemo(() => { + const lineHeight = 18 + const padding = 16 + const naturalHeight = lineCount * lineHeight + padding + return Math.min(naturalHeight, maxHeight) + }, [lineCount, maxHeight]) + + // Short JSON - show inline without collapsible + if (isShort) { + return ( +
+
+ + +
+ + {formatted} + +
+ ) + } + + // Non-collapsible mode - just show CodeMirror with optional header + if (!collapsible) { + return ( +
+ {title && ( +
+ + +
+ )} + +
+ ) + } + + // Long JSON - collapsible with CodeMirror + return ( + +
+ + {expanded ? ( + + ) : ( + + )} + {title} + {!expanded && ( + + ({lineCount} lines) + + )} + + +
+ + + +
+ ) +} diff --git a/apps/tonconnect-demo-unified/src/components/shared/ResultCard.tsx b/apps/tonconnect-demo-unified/src/components/shared/ResultCard.tsx new file mode 100644 index 000000000..f8a2625e6 --- /dev/null +++ b/apps/tonconnect-demo-unified/src/components/shared/ResultCard.tsx @@ -0,0 +1,235 @@ +import { useTonWallet, CHAIN } from "@tonconnect/ui-react" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Label } from "@/components/ui/label" +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" +import { JsonViewer } from "./JsonViewer" +import { useTransactionTracker } from "@/hooks/useTransactionTracker" +import { getExplorerUrl } from "@/utils/explorer-utils" +import { + Copy, + ExternalLink, + CheckCircle, + XCircle, + Clock, + Loader2, + X, + RotateCcw, +} from "lucide-react" +import { toast } from "sonner" +import type { OperationResult } from "@/hooks/useTransaction" + +interface ResultCardProps { + result: OperationResult + onDismiss?: () => void + onLoadToForm?: () => void +} + +function copyToClipboard(text: string) { + navigator.clipboard.writeText(text) + toast.success("Copied to clipboard") +} + +export function ResultCard({ result, onDismiss, onLoadToForm }: ResultCardProps) { + const wallet = useTonWallet() + const network = wallet?.account.chain === CHAIN.TESTNET ? "testnet" : "mainnet" + + // Transaction tracking (if we have boc) + const tracking = useTransactionTracker({ + boc: result.boc || null, + validUntil: result.validUntil || 0, + network, + }) + + // Determine overall status for badge + const displayStatus = result.status === 'error' + ? 'error' + : result.boc + ? tracking.status + : 'success' + + return ( +
+ {/* Header: timestamp + status + dismiss */} +
+
+ + {new Date(result.timestamp).toLocaleTimeString()} + + + {displayStatus === "pending" && ( + <> + + Pending + + )} + {displayStatus === "confirmed" && ( + <> + + Confirmed + + )} + {displayStatus === "success" && !result.boc && ( + <> + + Sent + + )} + {displayStatus === "expired" && ( + <> + + Expired + + )} + {displayStatus === "error" && ( + <> + + Error + + )} + + {displayStatus === "pending" && ( + + {tracking.formattedTime} + + )} +
+ {onDismiss && ( + + )} +
+ + {/* Content: 2-column layout on desktop */} +
+ {/* Left column: Request Sent */} +
+ +
+ + {/* Right column: Response + Transaction tracking */} +
+ {/* Response */} + + + {/* Transaction tracking (if boc exists) */} + {result.boc && ( +
+ {/* BOC - truncated with length indicator */} +
+ +
+ copyToClipboard(result.boc!)} + title="Click to copy full BOC" + > + {result.boc.slice(0, 50)}... + + +
+
+ + {/* Hash - full display with word-break */} + {tracking.hash && ( +
+
+ +
+ + +
+
+ + {tracking.hash} + +
+ )} + + {/* Confirmed details - compact */} + {tracking.status === "confirmed" && tracking.transaction && ( + + + Transaction Confirmed + + LT: {tracking.transaction.lt} • Fee: {tracking.transaction.fee} nanotons • {new Date(tracking.transaction.timestamp * 1000).toLocaleString()} + + + )} + + {/* Expired */} + {tracking.status === "expired" && ( + + + Transaction Expired + + Transaction not found before validUntil expired. + + + )} + + {/* Network error */} + {tracking.error && tracking.status === "pending" && ( + + + Network Error + + {tracking.error}. Retrying... + + + )} +
+ )} +
+
+ + {/* Footer: Load to form button */} + {onLoadToForm && ( +
+ +
+ )} +
+ ) +} diff --git a/apps/tonconnect-demo-unified/src/components/shared/SignDataResultCard.tsx b/apps/tonconnect-demo-unified/src/components/shared/SignDataResultCard.tsx new file mode 100644 index 000000000..17006140c --- /dev/null +++ b/apps/tonconnect-demo-unified/src/components/shared/SignDataResultCard.tsx @@ -0,0 +1,166 @@ +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Label } from "@/components/ui/label" +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" +import { JsonViewer } from "./JsonViewer" +import { + CheckCircle, + XCircle, + Loader2, + ShieldCheck, + Server, + X, + RotateCcw, +} from "lucide-react" +import type { SignDataOperationResult } from "@/hooks/useSignData" +import type { VerificationResult } from "@/utils/sign-data-verification" + +interface SignDataResultCardProps { + result: SignDataOperationResult + onDismiss?: () => void + onLoadToForm?: () => void + // Verification + canVerify: boolean + onVerifyClient: () => void + onVerifyServer: () => void + isVerifyingClient: boolean + isVerifyingServer: boolean + clientResult: VerificationResult | null + serverResult: VerificationResult | null +} + +function VerificationAlert({ title, result }: { title: string; result: VerificationResult }) { + return ( + + {result.valid ? : } + + {title}: {result.valid ? "Valid Signature" : "Invalid Signature"} + + + {result.message} + {result.details && ( + + Address: {result.details.addressMatch ? "✓" : "✗"} | + Public key: {result.details.publicKeyMatch ? "✓" : "✗"} | + Signature: {result.details.signatureValid ? "✓" : "✗"} + + )} + + + ) +} + +export function SignDataResultCard({ + result, + onDismiss, + onLoadToForm, + canVerify, + onVerifyClient, + onVerifyServer, + isVerifyingClient, + isVerifyingServer, + clientResult, + serverResult, +}: SignDataResultCardProps) { + return ( +
+ {/* Header: timestamp + status + dismiss */} +
+
+ + {new Date(result.timestamp).toLocaleTimeString()} + + + {result.status === 'success' ? ( + <>Signed + ) : ( + <>Error + )} + +
+ {onDismiss && ( + + )} +
+ + {/* Content: Request + Response */} +
+
+ +
+
+ +
+
+ + {/* Verification section (only if success) */} + {result.status === 'success' && ( +
+
+ +
+ + +
+
+ + {/* Verification results */} + {clientResult && ( + + )} + {serverResult && ( + + )} +
+ )} + + {/* Footer: Load to form */} + {onLoadToForm && ( +
+ +
+ )} +
+ ) +} diff --git a/apps/tonconnect-demo-unified/src/components/shared/TransactionResult.tsx b/apps/tonconnect-demo-unified/src/components/shared/TransactionResult.tsx new file mode 100644 index 000000000..edb9db0f5 --- /dev/null +++ b/apps/tonconnect-demo-unified/src/components/shared/TransactionResult.tsx @@ -0,0 +1,144 @@ +import { useTonWallet, CHAIN } from "@tonconnect/ui-react" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" +import { Label } from "@/components/ui/label" +import { useTransactionTracker } from "@/hooks/useTransactionTracker" +import { getExplorerUrl } from "@/utils/explorer-utils" +import { Copy, ExternalLink, CheckCircle, XCircle, Clock, Loader2 } from "lucide-react" +import { toast } from "sonner" + +interface TransactionResultProps { + boc: string + validUntil: number +} + +function copyToClipboard(text: string) { + navigator.clipboard.writeText(text) + toast.success("Copied to clipboard") +} + +export function TransactionResult({ boc, validUntil }: TransactionResultProps) { + const wallet = useTonWallet() + const network = wallet?.account.chain === CHAIN.TESTNET ? "testnet" : "mainnet" + + const { hash, status, transaction, formattedTime, error } = useTransactionTracker({ + boc, + validUntil, + network, + }) + + return ( + + + + + Transaction Result + + {status === "pending" && ( + <> + + Pending + + )} + {status === "confirmed" && ( + <> + + Confirmed + + )} + {status === "expired" && ( + <> + + Expired + + )} + + + + {status === "pending" && ( + + {formattedTime} + + )} + + + + + {/* BOC */} +
+ +
+ {boc} + +
+
+ + {/* Hash with explorer link */} + {hash && ( +
+ +
+ + {hash.slice(0, 16)}...{hash.slice(-8)} + + + +
+
+ )} + + {/* Confirmed */} + {status === "confirmed" && transaction && ( + + + Transaction Confirmed + +
LT: {transaction.lt}
+
Fee: {transaction.fee} nanotons
+
Time: {new Date(transaction.timestamp * 1000).toLocaleString()}
+
+
+ )} + + {/* Expired */} + {status === "expired" && ( + + + Transaction Expired + + Transaction not found before validUntil expired. + It may have been rejected by wallet or not sent to blockchain. + + + )} + + {/* Network error */} + {error && status === "pending" && ( + + + Network Error + + {error}. Retrying... + + + )} +
+
+ ) +} diff --git a/apps/tonconnect-demo-unified/src/components/tabs/SettingsTab.tsx b/apps/tonconnect-demo-unified/src/components/tabs/SettingsTab.tsx new file mode 100644 index 000000000..46d639208 --- /dev/null +++ b/apps/tonconnect-demo-unified/src/components/tabs/SettingsTab.tsx @@ -0,0 +1,377 @@ +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Label } from "@/components/ui/label" +import { Input } from "@/components/ui/input" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { Switch } from "@/components/ui/switch" +import { Button } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" +import { useSettingsContext } from "@/context/SettingsContext" +import { NetworkPicker } from "@/components/NetworkPicker" +import { RotateCcw } from "lucide-react" +import type { ThemeOption, ColorsConfig, FeaturesMode } from "@/hooks/useSettings" + +function ColorInput({ + label, + value, + onChange +}: { + label: string + value: string + onChange: (value: string) => void +}) { + return ( +
+ +
+ onChange(e.target.value)} + className="h-8 w-8 cursor-pointer rounded border border-input bg-transparent" + /> + onChange(e.target.value)} + className="w-24 font-mono text-xs" + /> +
+
+ ) +} + +function ColorsCard({ + title, + description, + colors, + onUpdate +}: { + title: string + description: string + colors: ColorsConfig + onUpdate: (key: keyof ColorsConfig, value: string) => void +}) { + return ( + + + {title} + {description} + + +
+

Constants

+ onUpdate("constantBlack", v)} /> + onUpdate("constantWhite", v)} /> +
+
+

Connect Button

+ onUpdate("connectButtonBg", v)} /> + onUpdate("connectButtonFg", v)} /> +
+
+

General

+ onUpdate("accent", v)} /> + onUpdate("telegramButton", v)} /> +
+
+

Icons

+ onUpdate("iconPrimary", v)} /> + onUpdate("iconSecondary", v)} /> + onUpdate("iconTertiary", v)} /> + onUpdate("iconSuccess", v)} /> + onUpdate("iconError", v)} /> +
+
+

Background

+ onUpdate("backgroundPrimary", v)} /> + onUpdate("backgroundSecondary", v)} /> + onUpdate("backgroundSegment", v)} /> + onUpdate("backgroundTint", v)} /> + onUpdate("backgroundQr", v)} /> +
+
+

Text

+ onUpdate("textPrimary", v)} /> + onUpdate("textSecondary", v)} /> +
+
+
+ ) +} + +export function SettingsTab() { + const { + language, setLanguage, + theme, setTheme, + borderRadius, setBorderRadius, + darkColors, updateDarkColor, + lightColors, updateLightColor, + resetColors, + modalsBefore, setModalsBefore, + modalsSuccess, setModalsSuccess, + modalsError, setModalsError, + notificationsBefore, setNotificationsBefore, + notificationsSuccess, setNotificationsSuccess, + notificationsError, setNotificationsError, + returnStrategy, setReturnStrategy, + skipRedirect, setSkipRedirect, + twaReturnUrl, setTwaReturnUrl, + enableAndroidBackHandler, setEnableAndroidBackHandler, + featuresMode, setFeaturesMode, + minMessages, setMinMessages, + extraCurrencyRequired, setExtraCurrencyRequired, + signDataTypes, setSignDataTypes, + } = useSettingsContext() + + const handleSignDataTypeChange = (type: string, checked: boolean) => { + if (checked) { + setSignDataTypes([...signDataTypes, type]) + } else { + setSignDataTypes(signDataTypes.filter(t => t !== type)) + } + } + + return ( +
+ {/* Row 1: Connection Settings + Modals + Notifications */} +
+ + + Connection Settings + Network and wallet filtering + + + + +
+ + +
+ + {featuresMode !== "none" && ( +
+
+ + setMinMessages(e.target.value ? parseInt(e.target.value) : undefined)} + /> +
+
+ + +
+
+ +
+ {["text", "cell", "binary"].map(type => ( +
+ handleSignDataTypeChange(type, !!checked)} + /> + +
+ ))} +
+
+
+ )} +
+
+ + + + Modals + Show modal dialogs for actions + + +
+ + +
+
+ + +
+
+ + +
+
+
+ + + + Notifications + Show toast notifications + + +
+ + +
+
+ + +
+
+ + +
+
+
+
+ + {/* Row 2: UI Settings + Redirect Settings */} +
+ + + UI Settings + TonConnect UI appearance + + +
+ + +
+
+ + +
+
+ + +
+
+
+ + + + Redirect Settings + Configure wallet redirect behavior + + +
+ + +
+
+ + +
+
+ + setTwaReturnUrl(e.target.value)} + placeholder="tg://resolve?domain=..." + /> +

+ Return URL for Telegram Web App connections +

+
+
+
+
+ + {/* Row 3: Android Settings */} + + + Android Settings + Android-specific behavior + + +
+
+ +

+ Use Android back button to close modals +

+
+ +
+
+
+ + {/* Row 4: Colors */} +
+
+

Colors

+ +
+
+ + +
+
+
+ ) +} diff --git a/apps/tonconnect-demo-unified/src/components/tabs/SignDataTab.tsx b/apps/tonconnect-demo-unified/src/components/tabs/SignDataTab.tsx new file mode 100644 index 000000000..7c078377e --- /dev/null +++ b/apps/tonconnect-demo-unified/src/components/tabs/SignDataTab.tsx @@ -0,0 +1,129 @@ +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { FormContainer } from "@/components/shared/FormContainer" +import { SignDataResultCard } from "@/components/shared/SignDataResultCard" +import { HowItWorksCard } from "@/components/shared/HowItWorksCard" +import { FieldLabel } from "@/components/shared/FieldLabel" +import { useSignData } from "@/hooks/useSignData" +import { useSettingsContext } from "@/context/SettingsContext" +import { validateSignDataJson } from "@/utils/validator" + +export function SignDataTab() { + const { notificationsBefore, notificationsSuccess, notificationsError } = useSettingsContext() + const { + dataType, setDataType, + dataText, setDataText, + schema, setSchema, + requestJson, + sign, + setFromJson, + isConnected, + isSigning, + // Result + lastResult, + clearResult, + loadResultToForm, + // Verification + canVerify, + verify, + verificationResult, + isVerifying, + verifyOnServer, + serverVerificationResult, + isVerifyingOnServer, + } = useSignData(notificationsBefore, notificationsSuccess, notificationsError) + + const formContent = ( + <> +
+ Data Type +
+ + + +
+
+ + {dataType === "cell" && ( +
+ Schema (TL-B) + setSchema(e.target.value)} + placeholder="e.g. transfer#123abc amount:Coins" + /> +
+ )} + +
+ + {dataType === "text" ? "Text" : dataType === "binary" ? "Data (base64)" : "Cell (BOC)"} + +