diff --git a/packages/ui/src/app/utils/tma-api.ts b/packages/ui/src/app/utils/tma-api.ts index d0ffd5c8..eb6ca720 100644 --- a/packages/ui/src/app/utils/tma-api.ts +++ b/packages/ui/src/app/utils/tma-api.ts @@ -1,4 +1,4 @@ -import { getWindow } from 'src/app/utils/web-api'; +import { getWindow, openLinkBlank } from 'src/app/utils/web-api'; import { TonConnectUIError } from 'src/errors'; import { logError } from 'src/app/utils/log'; @@ -35,6 +35,7 @@ if (initParams?.tgWebAppPlatform) { tmaPlatform = (initParams.tgWebAppPlatform as TmaPlatform) ?? 'unknown'; } if (tmaPlatform === 'unknown') { + const window = getWindow(); tmaPlatform = window?.Telegram?.WebApp?.platform ?? 'unknown'; } @@ -43,6 +44,7 @@ if (initParams?.tgWebAppVersion) { webAppVersion = initParams.tgWebAppVersion; } if (!webAppVersion) { + const window = getWindow(); webAppVersion = window?.Telegram?.WebApp?.version ?? '6.0'; } @@ -89,13 +91,16 @@ export function sendOpenTelegramLink(link: string): void { if (isIframe() || versionAtLeast('6.1')) { postEvent('web_app_open_tg_link', { path_full: pathFull }); } else { - // TODO: alias for openLinkBlank('https://t.me' + pathFull);, remove duplicated code - window.open('https://t.me' + pathFull, '_blank', 'noreferrer noopener'); + openLinkBlank('https://t.me' + pathFull); } } function isIframe(): boolean { try { + const window = getWindow(); + if (!window) { + return false; + } return window.parent != null && window !== window.parent; } catch (e) { return false; @@ -106,6 +111,11 @@ function postEvent(eventType: 'web_app_open_tg_link', eventData: { path_full: st function postEvent(eventType: 'web_app_expand', eventData: {}): void; function postEvent(eventType: string, eventData: object): void { try { + const window = getWindow(); + if (!window) { + throw new TonConnectUIError(`Can't post event to parent window: window is not defined`); + } + if (window.TelegramWebviewProxy !== undefined) { window.TelegramWebviewProxy.postEvent(eventType, JSON.stringify(eventData)); } else if (window.external && 'notify' in window.external) { diff --git a/packages/ui/src/app/utils/url-strategy-helpers.ts b/packages/ui/src/app/utils/url-strategy-helpers.ts new file mode 100644 index 00000000..13916a6a --- /dev/null +++ b/packages/ui/src/app/utils/url-strategy-helpers.ts @@ -0,0 +1,169 @@ +import { ReturnStrategy } from 'src/models/return-strategy'; +import { isInTMA, isTmaPlatform, sendOpenTelegramLink } from 'src/app/utils/tma-api'; +import { isOS, openDeeplinkWithFallback, openLinkBlank } from 'src/app/utils/web-api'; +import { encodeTelegramUrlParameters, isTelegramUrl } from '@tonconnect/sdk'; + +/** + * Adds a return strategy to a url. + * @param url + * @param strategy + * TODO: refactor this method + */ +export function addReturnStrategy( + url: string, + strategy: + | ReturnStrategy + | { + returnStrategy: ReturnStrategy; + twaReturnUrl: `${string}://${string}` | undefined; + } +): string { + let returnStrategy; + if (typeof strategy === 'string') { + returnStrategy = strategy; + } else { + returnStrategy = isInTMA() ? strategy.twaReturnUrl || strategy.returnStrategy : 'none'; + } + const newUrl = addQueryParameter(url, 'ret', returnStrategy); + + if (!isTelegramUrl(url)) { + return newUrl; + } + + const lastParam = newUrl.slice(newUrl.lastIndexOf('&') + 1); + return newUrl.slice(0, newUrl.lastIndexOf('&')) + '-' + encodeTelegramUrlParameters(lastParam); +} + +/** + * Redirects the user to a specified Telegram link with various strategies for returning to the application. + * This function is primarily used for TON Space to handle different platforms and operating systems. + * + * @param universalLink A string representing the universal link to redirect to within Telegram. + * @param options An object containing specific properties to customize the redirect behavior: + * - returnStrategy: An enum `ReturnStrategy` dictating the method for returning to the app after the action is completed. + * - twaReturnUrl: A URL template string for TMA return, or `undefined` if not applicable. + * - forceRedirect: A boolean flag to force redirection, bypassing deep link fallback mechanisms. + * + * The function adapts its behavior based on the execution context, such as the TMA or browser environment, and the operating system. + * Different strategies involve manipulating URL parameters and utilizing platform-specific features for optimal user experience. + */ +export function redirectToTelegram( + universalLink: string, + options: { + returnStrategy: ReturnStrategy; + twaReturnUrl: `${string}://${string}` | undefined; + forceRedirect: boolean; + } +): void { + options = { ...options }; + // TODO: Remove this line after all dApps and the wallets-list.json have been updated + const directLink = convertToDirectLink(universalLink); + const directLinkUrl = new URL(directLink); + + if (!directLinkUrl.searchParams.has('startapp')) { + directLinkUrl.searchParams.append('startapp', 'tonconnect'); + } + + if (isInTMA()) { + if (isTmaPlatform('ios', 'android')) { + // Use the `none` strategy, the current TMA instance will keep open. + // TON Space should automatically open in stack and should close + // itself after the user action. + + options.returnStrategy = 'back'; + options.twaReturnUrl = undefined; + + sendOpenTelegramLink(addReturnStrategy(directLinkUrl.toString(), options)); + } else if (isTmaPlatform('macos', 'tdesktop')) { + // Use a strategy involving a direct link to return to the app. + // The current TMA instance will close, and TON Space should + // automatically open, and reopen the application once the user + // action is completed. + + sendOpenTelegramLink(addReturnStrategy(directLinkUrl.toString(), options)); + } else if (isTmaPlatform('weba')) { + // Similar to macos/tdesktop strategy, but opening another TMA occurs + // through sending `web_app_open_tg_link` event to `parent`. + + sendOpenTelegramLink(addReturnStrategy(directLinkUrl.toString(), options)); + } else if (isTmaPlatform('web')) { + // Similar to iOS/Android strategy, but opening another TMA occurs + // through sending `web_app_open_tg_link` event to `parent`. + + options.returnStrategy = 'back'; + options.twaReturnUrl = undefined; + + sendOpenTelegramLink(addReturnStrategy(directLinkUrl.toString(), options)); + } else { + // Fallback for unknown platforms. Should use desktop strategy. + + openLinkBlank(addReturnStrategy(directLinkUrl.toString(), options)); + } + } else { + // For browser + if (isOS('ios', 'android')) { + // Use the `none` strategy. TON Space should do nothing after the user action. + + options.returnStrategy = 'none'; + + openLinkBlank(addReturnStrategy(directLinkUrl.toString(), options.returnStrategy)); + } else if (isOS('macos', 'windows', 'linux')) { + // Use the `none` strategy. TON Space should do nothing after the user action. + + options.returnStrategy = 'none'; + options.twaReturnUrl = undefined; + + if (options.forceRedirect) { + openLinkBlank(addReturnStrategy(directLinkUrl.toString(), options)); + } else { + const link = addReturnStrategy(directLinkUrl.toString(), options); + const deepLink = convertToDeepLink(link); + + openDeeplinkWithFallback(deepLink, () => openLinkBlank(link)); + } + } else { + // Fallback for unknown platforms. Should use desktop strategy. + + openLinkBlank(addReturnStrategy(directLinkUrl.toString(), options)); + } + } +} + +/** + * Adds a query parameter to a URL. + * @param url + * @param key + * @param value + */ +function addQueryParameter(url: string, key: string, value: string): string { + const parsed = new URL(url); + parsed.searchParams.append(key, value); + return parsed.toString(); +} + +/** + * Converts a universal link to a direct link. + * @param universalLink + * TODO: Remove this method after all dApps and the wallets-list.json have been updated + */ +function convertToDirectLink(universalLink: string): string { + const url = new URL(universalLink); + + if (url.searchParams.has('attach')) { + url.searchParams.delete('attach'); + url.pathname += '/start'; + } + + return url.toString(); +} + +/** + * Converts a direct link to a deep link. + * @param directLink + */ +function convertToDeepLink(directLink: string): string { + const parsed = new URL(directLink); + const [, domain, appname] = parsed.pathname.split('/'); + const startapp = parsed.searchParams.get('startapp'); + return `tg://resolve?domain=${domain}&appname=${appname}&startapp=${startapp}`; +} diff --git a/packages/ui/src/app/utils/web-api.ts b/packages/ui/src/app/utils/web-api.ts index 7e46d97b..9a60a97f 100644 --- a/packages/ui/src/app/utils/web-api.ts +++ b/packages/ui/src/app/utils/web-api.ts @@ -1,13 +1,10 @@ import { THEME } from 'src/models/THEME'; -import { ReturnStrategy } from 'src/models/return-strategy'; import { disableScrollClass, globalStylesTag } from 'src/app/styles/global-styles'; import { toPx } from 'src/app/utils/css'; import { UserAgent } from 'src/models/user-agent'; import UAParser from 'ua-parser-js'; -import { encodeTelegramUrlParameters, isTelegramUrl } from '@tonconnect/sdk'; import { InMemoryStorage } from 'src/app/models/in-memory-storage'; import { TonConnectUIError } from 'src/errors'; -import { isInTMA, isTmaPlatform, sendOpenTelegramLink } from 'src/app/utils/tma-api'; /** * Opens a link in a new tab. @@ -63,37 +60,6 @@ export function subscribeToThemeChange(callback: (theme: THEME) => void): () => window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', handler); } -export function addQueryParameter(url: string, key: string, value: string): string { - const parsed = new URL(url); - parsed.searchParams.append(key, value); - return parsed.toString(); -} - -export function addReturnStrategy( - url: string, - strategy: - | ReturnStrategy - | { - returnStrategy: ReturnStrategy; - twaReturnUrl: `${string}://${string}` | undefined; - } -): string { - let returnStrategy; - if (typeof strategy === 'string') { - returnStrategy = strategy; - } else { - returnStrategy = isInTMA() ? strategy.twaReturnUrl || strategy.returnStrategy : 'none'; - } - const newUrl = addQueryParameter(url, 'ret', returnStrategy); - - if (!isTelegramUrl(url)) { - return newUrl; - } - - const lastParam = newUrl.slice(newUrl.lastIndexOf('&') + 1); - return newUrl.slice(0, newUrl.lastIndexOf('&')) + '-' + encodeTelegramUrlParameters(lastParam); -} - export function disableScroll(): void { if (document.documentElement.scrollHeight === document.documentElement.clientHeight) { return; @@ -257,119 +223,10 @@ export function getUserAgent(): UserAgent { }; } -function isOS(...os: UserAgent['os'][]): boolean { +export function isOS(...os: UserAgent['os'][]): boolean { return os.includes(getUserAgent().os); } -function isBrowser(...browser: UserAgent['browser'][]): boolean { +export function isBrowser(...browser: UserAgent['browser'][]): boolean { return browser.includes(getUserAgent().browser); } - -export function redirectToTelegram( - universalLink: string, - options: { - returnStrategy: ReturnStrategy; - twaReturnUrl: `${string}://${string}` | undefined; - forceRedirect: boolean; - } -): void { - options = { ...options }; - // TODO: Remove this line after all dApps and the wallets-list.json have been updated - const directLink = convertToDirectLink(universalLink); - const directLinkUrl = new URL(directLink); - - if (!directLinkUrl.searchParams.has('startapp')) { - directLinkUrl.searchParams.append('startapp', 'tonconnect'); - } - - if (isInTMA()) { - if (isTmaPlatform('ios', 'android')) { - // Use the `none` strategy, the current TMA instance will keep open. - // TON Space should automatically open in stack and should close - // itself after the user action. - - options.returnStrategy = 'back'; - options.twaReturnUrl = undefined; - - sendOpenTelegramLink(addReturnStrategy(directLinkUrl.toString(), options)); - } else if (isTmaPlatform('macos', 'tdesktop')) { - // Use a strategy involving a direct link to return to the app. - // The current TMA instance will close, and TON Space should - // automatically open, and reopen the application once the user - // action is completed. - - sendOpenTelegramLink(addReturnStrategy(directLinkUrl.toString(), options)); - } else if (isTmaPlatform('weba')) { - // Similar to macos/tdesktop strategy, but opening another TMA occurs - // through sending `web_app_open_tg_link` event to `parent`. - - sendOpenTelegramLink(addReturnStrategy(directLinkUrl.toString(), options)); - } else if (isTmaPlatform('web')) { - // Similar to iOS/Android strategy, but opening another TMA occurs - // through sending `web_app_open_tg_link` event to `parent`. - - options.returnStrategy = 'back'; - options.twaReturnUrl = undefined; - - sendOpenTelegramLink(addReturnStrategy(directLinkUrl.toString(), options)); - } else { - // Fallback for unknown platforms. Should use desktop strategy. - - openLinkBlank(addReturnStrategy(directLinkUrl.toString(), options)); - } - } else { - // For browser - if (isOS('ios', 'android')) { - // Use the `none` strategy. TON Space should do nothing after the user action. - - options.returnStrategy = 'none'; - - openLinkBlank(addReturnStrategy(directLinkUrl.toString(), options.returnStrategy)); - } else if (isOS('macos', 'windows', 'linux')) { - // Use the `none` strategy. TON Space should do nothing after the user action. - - options.returnStrategy = 'none'; - options.twaReturnUrl = undefined; - - if (options.forceRedirect) { - openLinkBlank(addReturnStrategy(directLinkUrl.toString(), options)); - } else { - const link = addReturnStrategy(directLinkUrl.toString(), options); - const deepLink = convertToDeepLink(link); - - openDeeplinkWithFallback(deepLink, () => openLinkBlank(link)); - } - } else { - // Fallback for unknown platforms. Should use desktop strategy. - - openLinkBlank(addReturnStrategy(directLinkUrl.toString(), options)); - } - } -} - -/** - * Converts a universal link to a direct link. - * @param universalLink - * TODO: Remove this method after all dApps and the wallets-list.json have been updated - */ -function convertToDirectLink(universalLink: string): string { - const url = new URL(universalLink); - - if (url.searchParams.has('attach')) { - url.searchParams.delete('attach'); - url.pathname += '/start'; - } - - return url.toString(); -} - -/** - * Converts a direct link to a deep link. - * @param directLink - */ -function convertToDeepLink(directLink: string): string { - const parsed = new URL(directLink); - const [, domain, appname] = parsed.pathname.split('/'); - const startapp = parsed.searchParams.get('startapp'); - return `tg://resolve?domain=${domain}&appname=${appname}&startapp=${startapp}`; -} diff --git a/packages/ui/src/app/views/modals/actions-modal/action-modal/index.tsx b/packages/ui/src/app/views/modals/actions-modal/action-modal/index.tsx index ebd9a4b7..f43ddea5 100644 --- a/packages/ui/src/app/views/modals/actions-modal/action-modal/index.tsx +++ b/packages/ui/src/app/views/modals/actions-modal/action-modal/index.tsx @@ -4,11 +4,12 @@ import { ActionModalStyled, ButtonStyled, H1Styled, TextStyled } from './style'; import { WithDataAttributes } from 'src/app/models/with-data-attributes'; import { useDataAttributes } from 'src/app/hooks/use-data-attributes'; import { TonConnectUiContext } from 'src/app/state/ton-connect-ui.context'; -import { addReturnStrategy, openLinkBlank, redirectToTelegram } from 'src/app/utils/web-api'; +import { openLinkBlank } from 'src/app/utils/web-api'; import { isTelegramUrl } from '@tonconnect/sdk'; import { appState } from 'src/app/state/app.state'; import { action } from 'src/app/state/modals-state'; import { isInTMA } from 'src/app/utils/tma-api'; +import { addReturnStrategy, redirectToTelegram } from 'src/app/utils/url-strategy-helpers'; interface ActionModalProps extends WithDataAttributes { headerTranslationKey: string; diff --git a/packages/ui/src/app/views/modals/wallets-modal/desktop-connection-modal/index.tsx b/packages/ui/src/app/views/modals/wallets-modal/desktop-connection-modal/index.tsx index 5e7237c7..2049264e 100644 --- a/packages/ui/src/app/views/modals/wallets-modal/desktop-connection-modal/index.tsx +++ b/packages/ui/src/app/views/modals/wallets-modal/desktop-connection-modal/index.tsx @@ -42,13 +42,14 @@ import { RetryIcon } from 'src/app/components'; import { appState } from 'src/app/state/app.state'; -import { addReturnStrategy, openLinkBlank, redirectToTelegram } from 'src/app/utils/web-api'; +import { openLinkBlank } from 'src/app/utils/web-api'; import { setLastSelectedWalletInfo } from 'src/app/state/modals-state'; import { Link } from 'src/app/components/link'; import { supportsDesktop, supportsExtension, supportsMobile } from 'src/app/utils/wallets'; import { AT_WALLET_APP_NAME } from 'src/app/env/AT_WALLET_APP_NAME'; import { IMG } from 'src/app/env/IMG'; import { Translation } from 'src/app/components/typography/Translation'; +import { addReturnStrategy, redirectToTelegram } from 'src/app/utils/url-strategy-helpers'; export interface DesktopConnectionProps { additionalRequest?: ConnectAdditionalRequest; diff --git a/packages/ui/src/app/views/modals/wallets-modal/desltop-universal-modal/index.tsx b/packages/ui/src/app/views/modals/wallets-modal/desltop-universal-modal/index.tsx index 50e2a049..f84e834f 100644 --- a/packages/ui/src/app/views/modals/wallets-modal/desltop-universal-modal/index.tsx +++ b/packages/ui/src/app/views/modals/wallets-modal/desltop-universal-modal/index.tsx @@ -12,7 +12,8 @@ import { setLastSelectedWalletInfo } from 'src/app/state/modals-state'; import { FourWalletsItem, H1, WalletLabeledItem } from 'src/app/components'; import { PersonalizedWalletInfo } from 'src/app/models/personalized-wallet-info'; import { IMG } from 'src/app/env/IMG'; -import { addReturnStrategy } from 'src/app/utils/web-api'; + +import { addReturnStrategy } from 'src/app/utils/url-strategy-helpers'; interface DesktopUniversalModalProps { additionalRequest: ConnectAdditionalRequest; diff --git a/packages/ui/src/app/views/modals/wallets-modal/mobile-connection-modal/index.tsx b/packages/ui/src/app/views/modals/wallets-modal/mobile-connection-modal/index.tsx index 22dd1e6a..d3dd3c64 100644 --- a/packages/ui/src/app/views/modals/wallets-modal/mobile-connection-modal/index.tsx +++ b/packages/ui/src/app/views/modals/wallets-modal/mobile-connection-modal/index.tsx @@ -16,11 +16,12 @@ import { import { ConnectorContext } from 'src/app/state/connector.context'; import { Button, H3, QRIcon, RetryIcon } from 'src/app/components'; import { appState } from 'src/app/state/app.state'; -import { addReturnStrategy, openLinkBlank } from 'src/app/utils/web-api'; +import { openLinkBlank } from 'src/app/utils/web-api'; import { setLastSelectedWalletInfo } from 'src/app/state/modals-state'; import { useTheme } from 'solid-styled-components'; import { MobileConnectionQR } from 'src/app/views/modals/wallets-modal/mobile-connection-modal/mobile-connection-qr'; import { Translation } from 'src/app/components/typography/Translation'; +import { addReturnStrategy } from 'src/app/utils/url-strategy-helpers'; export interface MobileConnectionProps { additionalRequest?: ConnectAdditionalRequest; diff --git a/packages/ui/src/app/views/modals/wallets-modal/mobile-connection-modal/mobile-connection-qr/index.tsx b/packages/ui/src/app/views/modals/wallets-modal/mobile-connection-modal/mobile-connection-qr/index.tsx index ec42e844..e269d16b 100644 --- a/packages/ui/src/app/views/modals/wallets-modal/mobile-connection-modal/mobile-connection-qr/index.tsx +++ b/packages/ui/src/app/views/modals/wallets-modal/mobile-connection-modal/mobile-connection-qr/index.tsx @@ -2,7 +2,8 @@ import { Component } from 'solid-js'; import { H1Styled, H2Styled, QrCodeWrapper } from './style'; import { QRCode } from 'src/app/components'; import { WalletInfo } from '@tonconnect/sdk'; -import { addReturnStrategy } from 'src/app/utils/web-api'; + +import { addReturnStrategy } from 'src/app/utils/url-strategy-helpers'; interface MobileConnectionQRProps { universalLink: string; diff --git a/packages/ui/src/app/views/modals/wallets-modal/mobile-universal-modal/index.tsx b/packages/ui/src/app/views/modals/wallets-modal/mobile-universal-modal/index.tsx index 918b386f..885baf63 100644 --- a/packages/ui/src/app/views/modals/wallets-modal/mobile-universal-modal/index.tsx +++ b/packages/ui/src/app/views/modals/wallets-modal/mobile-universal-modal/index.tsx @@ -21,7 +21,7 @@ import { TGImageStyled, UlStyled } from './style'; -import { addReturnStrategy, openLinkBlank, redirectToTelegram } from 'src/app/utils/web-api'; +import { openLinkBlank } from 'src/app/utils/web-api'; import { setLastSelectedWalletInfo } from 'src/app/state/modals-state'; import { appState } from 'src/app/state/app.state'; import { IMG } from 'src/app/env/IMG'; @@ -31,6 +31,7 @@ import { copyToClipboard } from 'src/app/utils/copy-to-clipboard'; import { TonConnectUIError } from 'src/errors'; import { MobileUniversalQR } from './mobile-universal-qr'; import { Translation } from 'src/app/components/typography/Translation'; +import { addReturnStrategy, redirectToTelegram } from 'src/app/utils/url-strategy-helpers'; interface MobileUniversalModalProps { walletsList: WalletInfo[]; diff --git a/packages/ui/src/app/views/modals/wallets-modal/mobile-universal-modal/mobile-universal-qr/index.tsx b/packages/ui/src/app/views/modals/wallets-modal/mobile-universal-modal/mobile-universal-qr/index.tsx index 023f76ed..180e5085 100644 --- a/packages/ui/src/app/views/modals/wallets-modal/mobile-universal-modal/mobile-universal-qr/index.tsx +++ b/packages/ui/src/app/views/modals/wallets-modal/mobile-universal-modal/mobile-universal-qr/index.tsx @@ -2,7 +2,8 @@ import { Component } from 'solid-js'; import { H1Styled, H2Styled, QrCodeWrapper } from './style'; import { QRCode } from 'src/app/components'; import { IMG } from 'src/app/env/IMG'; -import { addReturnStrategy } from 'src/app/utils/web-api'; + +import { addReturnStrategy } from 'src/app/utils/url-strategy-helpers'; interface MobileUniversalQRProps { universalLink: string; diff --git a/packages/ui/src/ton-connect-ui.ts b/packages/ui/src/ton-connect-ui.ts index 07a5e2a3..73c20f73 100644 --- a/packages/ui/src/ton-connect-ui.ts +++ b/packages/ui/src/ton-connect-ui.ts @@ -19,12 +19,10 @@ import { TonConnectUIError } from 'src/errors/ton-connect-ui.error'; import { TonConnectUiCreateOptions } from 'src/models/ton-connect-ui-create-options'; import { PreferredWalletStorage, WalletInfoStorage } from 'src/storage'; import { - addReturnStrategy, getSystemTheme, getUserAgent, openLinkBlank, preloadImages, - redirectToTelegram, subscribeToThemeChange } from 'src/app/utils/web-api'; import { TonConnectUiOptions } from 'src/models/ton-connect-ui-options'; @@ -42,6 +40,7 @@ import { WalletsModalManager } from 'src/managers/wallets-modal-manager'; import { TransactionModalManager } from 'src/managers/transaction-modal-manager'; import { WalletsModal, WalletsModalState } from 'src/models/wallets-modal'; import { isInTMA, sendExpand } from 'src/app/utils/tma-api'; +import { addReturnStrategy, redirectToTelegram } from 'src/app/utils/url-strategy-helpers'; export class TonConnectUI { public static getWallets(): Promise {