Skip to content

Commit

Permalink
fix(ui): adjust window access for ssr compatibility and refactor code
Browse files Browse the repository at this point in the history
Closes #125
  • Loading branch information
thekiba committed Dec 19, 2023
1 parent ea622ce commit ae10e14
Show file tree
Hide file tree
Showing 11 changed files with 199 additions and 157 deletions.
16 changes: 13 additions & 3 deletions packages/ui/src/app/utils/tma-api.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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';
}

Expand All @@ -43,6 +44,7 @@ if (initParams?.tgWebAppVersion) {
webAppVersion = initParams.tgWebAppVersion;
}
if (!webAppVersion) {
const window = getWindow();
webAppVersion = window?.Telegram?.WebApp?.version ?? '6.0';
}

Expand Down Expand Up @@ -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;
Expand All @@ -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) {
Expand Down
169 changes: 169 additions & 0 deletions packages/ui/src/app/utils/url-strategy-helpers.ts
Original file line number Diff line number Diff line change
@@ -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}`;
}
147 changes: 2 additions & 145 deletions packages/ui/src/app/utils/web-api.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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}`;
}
Loading

0 comments on commit ae10e14

Please sign in to comment.