From ef2e5a07ddb13515caeffdb5f3382044477ad81f Mon Sep 17 00:00:00 2001 From: Yauheni Mileika Date: Fri, 22 Aug 2025 13:53:08 +0300 Subject: [PATCH] feat: implement validation --- packages/ui/src/app/App.tsx | 7 ++++- packages/ui/src/app/utils/dom-validation.ts | 31 +++++++++++++++++++ packages/ui/src/app/utils/index.ts | 3 ++ .../ui/src/app/views/account-button/index.tsx | 18 ++++++++++- .../modals/actions-modal/actions-modal.tsx | 10 +++++- .../wallets-modal/single-wallet-modal.tsx | 6 ++-- .../modals/wallets-modal/wallets-modal.tsx | 3 ++ packages/ui/src/app/widget-controller.tsx | 14 +++++++-- packages/ui/src/library.ts | 1 + packages/ui/src/ton-connect-ui.ts | 20 ++++++++++++ 10 files changed, 105 insertions(+), 8 deletions(-) create mode 100644 packages/ui/src/app/utils/dom-validation.ts create mode 100644 packages/ui/src/app/utils/index.ts diff --git a/packages/ui/src/app/App.tsx b/packages/ui/src/app/App.tsx index 4feef6c39..ed16c7526 100644 --- a/packages/ui/src/app/App.tsx +++ b/packages/ui/src/app/App.tsx @@ -1,5 +1,5 @@ import './global.d.ts'; -import { Show } from 'solid-js'; +import { Show, createEffect } from 'solid-js'; import type { Component } from 'solid-js'; import { Dynamic, Portal } from 'solid-js/web'; import { ThemeProvider } from 'solid-styled-components'; @@ -16,6 +16,7 @@ import { createI18nContext, I18nContext } from '@solid-primitives/i18n'; import { appState } from 'src/app/state/app.state'; import { defineStylesRoot, fixMobileSafariActiveTransition } from 'src/app/utils/web-api'; import { SingleWalletModal } from 'src/app/views/modals/wallets-modal/single-wallet-modal'; +import { validateWidgetRoot } from './utils/dom-validation'; export type AppProps = { tonConnectUI: TonConnectUI; @@ -27,6 +28,10 @@ const App: Component = props => { defineStylesRoot(); fixMobileSafariActiveTransition(); + createEffect(() => { + validateWidgetRoot('tc-widget-root'); + }); + return ( diff --git a/packages/ui/src/app/utils/dom-validation.ts b/packages/ui/src/app/utils/dom-validation.ts new file mode 100644 index 000000000..4aa3e92ce --- /dev/null +++ b/packages/ui/src/app/utils/dom-validation.ts @@ -0,0 +1,31 @@ +/** + * Utility functions for DOM validation in TON Connect UI + */ + +/** + * Checks if the TON Connect widget root element exists in the DOM + * @param rootId - The ID of the root element to check + * @returns true if the element exists, false otherwise + */ +export function checkWidgetRootExists(rootId: string): boolean { + const element = document.getElementById(rootId); + return element !== null; +} + +export function logWidgetRootMissingError(rootId: string): void { + console.error(`[TON Connect UI] CRITICAL ERROR: Widget root element not found!`); + console.error(`[TON Connect UI] The element with ID "${rootId}" was not found in the DOM.`); +} + +/** + * Validates that the widget root element exists and logs an error if it doesn't + * @param rootId - The ID of the root element to validate + * @returns true if the element exists, false otherwise + */ +export function validateWidgetRoot(rootId: string): boolean { + const exists = checkWidgetRootExists(rootId); + if (!exists) { + logWidgetRootMissingError(rootId); + } + return exists; +} diff --git a/packages/ui/src/app/utils/index.ts b/packages/ui/src/app/utils/index.ts new file mode 100644 index 000000000..2b4b07556 --- /dev/null +++ b/packages/ui/src/app/utils/index.ts @@ -0,0 +1,3 @@ +export * from './web-api'; +export * from './animate'; +export * from './dom-validation'; diff --git a/packages/ui/src/app/views/account-button/index.tsx b/packages/ui/src/app/views/account-button/index.tsx index ecd15b165..79a5196d0 100644 --- a/packages/ui/src/app/views/account-button/index.tsx +++ b/packages/ui/src/app/views/account-button/index.tsx @@ -1,4 +1,12 @@ -import { Component, createSignal, onCleanup, onMount, Show, useContext } from 'solid-js'; +import { + Component, + createSignal, + onCleanup, + onMount, + Show, + useContext, + createEffect +} from 'solid-js'; import { ArrowIcon, Text, TonIcon } from 'src/app/components'; import { ConnectorContext } from 'src/app/state/connector.context'; import { TonConnectUiContext } from 'src/app/state/ton-connect-ui.context'; @@ -19,6 +27,7 @@ import { Transition } from 'solid-transition-group'; import { useTheme } from 'solid-styled-components'; import { globalStylesTag } from 'src/app/styles/global-styles'; import { animate } from 'src/app/utils/animate'; +import { validateWidgetRoot } from 'src/app/utils/dom-validation'; interface AccountButtonProps {} @@ -41,6 +50,13 @@ export const AccountButton: Component = () => { middleware: [flip(), shift()] }); + // Validate that the widget root element exists when account button is rendered + createEffect(() => { + if (account() || restoringProcess()) { + validateWidgetRoot('tc-widget-root'); + } + }); + const normalizedAddress = (): string => { const acc = account(); if (acc) { diff --git a/packages/ui/src/app/views/modals/actions-modal/actions-modal.tsx b/packages/ui/src/app/views/modals/actions-modal/actions-modal.tsx index ddc270423..9e7d525db 100644 --- a/packages/ui/src/app/views/modals/actions-modal/actions-modal.tsx +++ b/packages/ui/src/app/views/modals/actions-modal/actions-modal.tsx @@ -1,4 +1,4 @@ -import { Component, Match, Switch } from 'solid-js'; +import { Component, Match, Switch, createEffect } from 'solid-js'; import { Modal } from 'src/app/components'; import { appState } from 'src/app/state/app.state'; import { action, setAction } from 'src/app/state/modals-state'; @@ -8,8 +8,16 @@ import { TransactionSentModal } from 'src/app/views/modals/actions-modal/transac import { ConfirmSignDataModal } from './confirm-sign-data-modal'; import { SignDataCanceledModal } from './sign-data-canceled-modal'; import { DataSignedModal } from './data-signed-modal'; +import { validateWidgetRoot } from 'src/app/utils/dom-validation'; export const ActionsModal: Component = () => { + // Validate that the widget root element exists when modal is opened + createEffect(() => { + if (action() !== null && action()?.openModal === true) { + validateWidgetRoot('tc-widget-root'); + } + }); + return ( { const { locale } = useI18n()[1]; @@ -32,7 +33,8 @@ export const SingleWalletModal: Component = () => { createEffect(() => { if (getSingleWalletModalIsOpened()) { - updateIsMobile(); + // Validate that the widget root element exists when modal is opened + validateWidgetRoot('tc-widget-root'); } }); diff --git a/packages/ui/src/app/views/modals/wallets-modal/wallets-modal.tsx b/packages/ui/src/app/views/modals/wallets-modal/wallets-modal.tsx index bad695552..299add608 100644 --- a/packages/ui/src/app/views/modals/wallets-modal/wallets-modal.tsx +++ b/packages/ui/src/app/views/modals/wallets-modal/wallets-modal.tsx @@ -43,6 +43,7 @@ import { WalletsModalCloseReason } from 'src/models'; import { DesktopFeatureNotSupportModal } from './feature-not-supoprt-modal'; import { widgetController } from 'src/app/widget-controller'; import { ChooseSupportedFeatureWalletsModal } from 'src/models/wallets-modal'; +import { validateWidgetRoot } from 'src/app/utils/dom-validation'; export const WalletsModal: Component = () => { const { locale } = useI18n()[1]; @@ -51,6 +52,8 @@ export const WalletsModal: Component = () => { createEffect(() => { if (getWalletsModalIsOpened()) { updateIsMobile(); + + validateWidgetRoot('tc-widget-root'); } else { setSelectedWalletInfo(null); setSelectedTab('universal'); diff --git a/packages/ui/src/app/widget-controller.tsx b/packages/ui/src/app/widget-controller.tsx index bdaf5998f..f4da526b6 100644 --- a/packages/ui/src/app/widget-controller.tsx +++ b/packages/ui/src/app/widget-controller.tsx @@ -13,6 +13,7 @@ import App from './App'; import { WalletInfoWithOpenMethod, WalletOpenMethod } from 'src/models/connected-wallet'; import { WalletsModalCloseReason } from 'src/models'; import { WalletInfoRemote, WalletNotSupportFeatureError } from '@tonconnect/sdk'; +import { validateWidgetRoot } from './utils/dom-validation'; export const widgetController = { openWalletsModal: (): void => @@ -63,9 +64,16 @@ export const widgetController = { } | null => lastSelectedWalletInfo(), removeSelectedWalletInfo: (): void => setLastSelectedWalletInfo(null), - renderApp: (root: string, tonConnectUI: TonConnectUI): (() => void) => - render( + renderApp: (root: string, tonConnectUI: TonConnectUI): (() => void) => { + // Validate that the widget root element exists before rendering + if (!validateWidgetRoot(root)) { + // Return a no-op cleanup function since we can't render + return () => {}; + } + + return render( () => , document.getElementById(root) as HTMLElement - ) + ); + } }; diff --git a/packages/ui/src/library.ts b/packages/ui/src/library.ts index c7444a686..bc923282a 100644 --- a/packages/ui/src/library.ts +++ b/packages/ui/src/library.ts @@ -3,3 +3,4 @@ export { TonConnectUI } from './ton-connect-ui'; export type { UserActionEvent } from './tracker/types'; export * from './models'; export * from './errors'; +export * from './app/utils/dom-validation'; diff --git a/packages/ui/src/ton-connect-ui.ts b/packages/ui/src/ton-connect-ui.ts index 9ecff56f1..ddee3955d 100644 --- a/packages/ui/src/ton-connect-ui.ts +++ b/packages/ui/src/ton-connect-ui.ts @@ -54,6 +54,7 @@ import { SingleWalletModal, SingleWalletModalState } from 'src/models/single-wal import { TonConnectUITracker } from 'src/tracker/ton-connect-ui-tracker'; import { tonConnectUiVersion } from 'src/constants/version'; import { ReturnStrategy } from './models'; +import { validateWidgetRoot } from './app/utils/dom-validation'; export class TonConnectUI { public static getWallets(): Promise { @@ -349,6 +350,8 @@ export class TonConnectUI { * Opens the modal window, returns a promise that resolves after the modal window is opened. */ public async openModal(): Promise { + // Validate that the widget root element exists before opening modal + validateWidgetRoot('tc-widget-root'); return this.modal.open(); } @@ -378,6 +381,8 @@ export class TonConnectUI { * @experimental */ public async openSingleWalletModal(wallet: string): Promise { + // Validate that the widget root element exists before opening modal + validateWidgetRoot('tc-widget-root'); return this.singleWalletModal.open(wallet); } @@ -445,6 +450,9 @@ export class TonConnectUI { tx: SendTransactionRequest, options?: ActionConfiguration & { onRequestSent?: (redirectToWallet: () => void) => void } ): Promise { + // Validate that the widget root element exists before sending transaction + validateWidgetRoot('tc-widget-root'); + this.tracker.trackTransactionSentForSignature(this.wallet, tx); if (!this.connected) { @@ -572,6 +580,9 @@ export class TonConnectUI { data: SignDataPayload, options?: { onRequestSent?: (redirectToWallet: () => void) => void } ): Promise { + // Validate that the widget root element exists before signing data + validateWidgetRoot('tc-widget-root'); + this.tracker.trackDataSentForSignature(this.wallet, data); if (!this.connected) { @@ -1092,8 +1103,17 @@ export class TonConnectUI { const rootElement = document.createElement('div'); rootElement.id = rootId; document.body.appendChild(rootElement); + + // Log that we created the default root element + console.warn( + `%c[TON Connect UI] Created default widget root element with ID "${rootId}"`, + 'color: #ff6b35; font-weight: bold;' + ); } + // Validate the root element exists and log any issues + validateWidgetRoot(rootId); + return rootId; }