Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion packages/ui/src/app/App.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
Expand All @@ -27,6 +28,10 @@ const App: Component<AppProps> = props => {
defineStylesRoot();
fixMobileSafariActiveTransition();

createEffect(() => {
validateWidgetRoot('tc-widget-root');
});

return (
<I18nContext.Provider value={translations}>
<TonConnectUiContext.Provider value={props.tonConnectUI}>
Expand Down
31 changes: 31 additions & 0 deletions packages/ui/src/app/utils/dom-validation.ts
Original file line number Diff line number Diff line change
@@ -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;
}
3 changes: 3 additions & 0 deletions packages/ui/src/app/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './web-api';
export * from './animate';
export * from './dom-validation';
18 changes: 17 additions & 1 deletion packages/ui/src/app/views/account-button/index.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 {}

Expand All @@ -41,6 +50,13 @@ export const AccountButton: Component<AccountButtonProps> = () => {
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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 (
<Modal
opened={action() !== null && action()?.openModal === true}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
import { H1Styled, LoaderContainerStyled, StyledModal } from './style';
import { useI18n } from '@solid-primitives/i18n';
import { appState } from 'src/app/state/app.state';
import { isMobile, updateIsMobile } from 'src/app/hooks/isMobile';
import { isMobile } from 'src/app/hooks/isMobile';
import { LoaderIcon } from 'src/app/components';
import { LoadableReady } from 'src/models/loadable';
import { DesktopConnectionModal } from 'src/app/views/modals/wallets-modal/desktop-connection-modal';
Expand All @@ -25,14 +25,16 @@ import { MobileConnectionModal } from 'src/app/views/modals/wallets-modal/mobile
import { Dynamic } from 'solid-js/web';
import { WalletsModalCloseReason } from 'src/models';
import { TonConnectUiContext } from 'src/app/state/ton-connect-ui.context';
import { validateWidgetRoot } from 'src/app/utils/dom-validation';

export const SingleWalletModal: Component = () => {
const { locale } = useI18n()[1];
createEffect(() => locale(appState.language));

createEffect(() => {
if (getSingleWalletModalIsOpened()) {
updateIsMobile();
// Validate that the widget root element exists when modal is opened
validateWidgetRoot('tc-widget-root');
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand All @@ -51,6 +52,8 @@ export const WalletsModal: Component = () => {
createEffect(() => {
if (getWalletsModalIsOpened()) {
updateIsMobile();

validateWidgetRoot('tc-widget-root');
} else {
setSelectedWalletInfo(null);
setSelectedTab('universal');
Expand Down
14 changes: 11 additions & 3 deletions packages/ui/src/app/widget-controller.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 =>
Expand Down Expand Up @@ -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(
() => <App tonConnectUI={tonConnectUI} />,
document.getElementById(root) as HTMLElement
)
);
}
};
1 change: 1 addition & 0 deletions packages/ui/src/library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
20 changes: 20 additions & 0 deletions packages/ui/src/ton-connect-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<WalletInfo[]> {
Expand Down Expand Up @@ -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<void> {
// Validate that the widget root element exists before opening modal
validateWidgetRoot('tc-widget-root');
return this.modal.open();
}

Expand Down Expand Up @@ -378,6 +381,8 @@ export class TonConnectUI {
* @experimental
*/
public async openSingleWalletModal(wallet: string): Promise<void> {
// Validate that the widget root element exists before opening modal
validateWidgetRoot('tc-widget-root');
return this.singleWalletModal.open(wallet);
}

Expand Down Expand Up @@ -445,6 +450,9 @@ export class TonConnectUI {
tx: SendTransactionRequest,
options?: ActionConfiguration & { onRequestSent?: (redirectToWallet: () => void) => void }
): Promise<SendTransactionResponse> {
// Validate that the widget root element exists before sending transaction
validateWidgetRoot('tc-widget-root');

this.tracker.trackTransactionSentForSignature(this.wallet, tx);

if (!this.connected) {
Expand Down Expand Up @@ -572,6 +580,9 @@ export class TonConnectUI {
data: SignDataPayload,
options?: { onRequestSent?: (redirectToWallet: () => void) => void }
): Promise<SignDataResponse> {
// Validate that the widget root element exists before signing data
validateWidgetRoot('tc-widget-root');

this.tracker.trackDataSentForSignature(this.wallet, data);

if (!this.connected) {
Expand Down Expand Up @@ -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;
}

Expand Down