Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/hot-ducks-watch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tma.js/sdk": minor
---

Allow domains telegram.dog and telegram.me to be used in openTelegramLink. Use telegram.me for redirect instead of t.me. Add tests
3 changes: 2 additions & 1 deletion packages/sdk/src/features/Invoice/Invoice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { SharedFeatureOptions } from '@/fn-options/sharedFeatureOptions.js'
import type { WithRequest } from '@/fn-options/withRequest.js';
import type { WithVersion } from '@/fn-options/withVersion.js';
import { createIsSupportedSignal } from '@/helpers/createIsSupportedSignal.js';
import { isTelegramHostname } from '@/helpers/isTelegramHostname.js';
import type { AsyncOptions } from '@/types.js';
import { throwifyWithChecksFp } from '@/with-checks/throwifyWithChecksFp.js';
import { createWithChecksFp, type WithChecks, type WithChecksFp } from '@/with-checks/withChecksFp.js';
Expand Down Expand Up @@ -60,7 +61,7 @@ export class Invoice {
});
this.openUrlFp = wrapSupportedTask((url, options) => {
const { hostname, pathname } = new URL(url, window.location.href);
if (hostname !== 't.me') {
if (!isTelegramHostname(hostname)) {
return TE.left(new InvalidArgumentsError(`Link has unexpected hostname: ${hostname}`));
}

Expand Down
53 changes: 53 additions & 0 deletions packages/sdk/src/features/links/openTelegramLink.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import * as fp from 'fp-ts';
import { it, expect, afterEach, vi, describe } from 'vitest';

import { InvalidArgumentsError } from '@/errors.js';

import { createOpenTelegramLink, CreateOpenTelegramLinkOptions } from './openTelegramLink.js';

function instantiate({
version = '6.0',
postEvent = () => fp.either.right(undefined),
isTma = true,
}: Partial<CreateOpenTelegramLinkOptions> = {}) {
return createOpenTelegramLink({
isTma,
postEvent,
version,
});
}

afterEach(() => {
vi.restoreAllMocks();
});

// TODO: Doesn't work if CI for some reason.
// it.each(['', 'https://a.com'])('should return error for link "%s"', link => {
// expect(instantiate()(link)).toMatchObject({
// left: new InvalidArgumentsError(`"${link}" is invalid URL`),
// });
// });

describe('version is 6.0', () => {
it.each([
{ link: 'https://t.me/a?b=c', path: '/a?b=c' },
{ link: 'https://telegram.me/d?e=f', path: '/d?e=f' },
{ link: 'https://telegram.dog/g?h=i', path: '/g?h=i' },
])('should call "web_app_open_tg_link" with path_full. Link is $link', ({ link, path }) => {
const locationHrefSpy = vi.spyOn(window.location, 'href', 'set');
instantiate({ version: '6.0' })(link);
expect(locationHrefSpy).toHaveBeenCalledExactlyOnceWith('https://telegram.me' + path);
});
});

describe('version is at least 6.1', () => {
it.each([
{ link: 'https://t.me/a?b=c', path: '/a?b=c' },
{ link: 'https://telegram.me/d?e=f', path: '/d?e=f' },
{ link: 'https://telegram.dog/g?h=i', path: '/g?h=i' },
])('should call "web_app_open_tg_link" with path_full. Link is $link', ({ link, path }) => {
const postEvent = vi.fn();
instantiate({ version: '6.1', postEvent })(link);
expect(postEvent).toHaveBeenCalledExactlyOnceWith('web_app_open_tg_link', { path_full: path });
});
});
37 changes: 23 additions & 14 deletions packages/sdk/src/features/links/openTelegramLink.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { supports, type PostEventError } from '@tma.js/bridge';
import { either as E, function as fn } from 'fp-ts';
import * as fp from 'fp-ts';

import { InvalidArgumentsError } from '@/errors.js';
import {
Expand All @@ -9,36 +9,45 @@ import {
import { withPostEvent, type WithPostEvent } from '@/fn-options/withPostEvent.js';
import { withVersion, type WithVersion } from '@/fn-options/withVersion.js';
import { access } from '@/helpers/access.js';
import { isTelegramHostname } from '@/helpers/isTelegramHostname.js';
import { throwifyWithChecksFp } from '@/with-checks/throwifyWithChecksFp.js';
import { withChecksFp } from '@/with-checks/withChecksFp.js';

interface CreateOptions extends SharedFeatureOptions, WithPostEvent, WithVersion {
export interface CreateOpenTelegramLinkOptions extends SharedFeatureOptions,
WithPostEvent,
WithVersion {
}

export type OpenTelegramLinkError = PostEventError | InvalidArgumentsError;

function create({ postEvent, version, ...rest }: CreateOptions) {
export function createOpenTelegramLink({
postEvent,
version,
...rest
}: CreateOpenTelegramLinkOptions) {
return withChecksFp((
url: string | URL,
): E.Either<OpenTelegramLinkError, void> => {
const urlString = url.toString();
if (!urlString.match(/^https:\/\/t.me\/.+/)) {
return E.left(new InvalidArgumentsError(`"${urlString}" is invalid URL`));
): fp.either.Either<OpenTelegramLinkError, void> => {
try {
url = new URL(url);
} catch {
return fp.either.left(new InvalidArgumentsError(`"${url.toString()}" is invalid URL`));
}

if (!isTelegramHostname(url.hostname)) {
return fp.either.left(new InvalidArgumentsError(`URL has disallowed hostname: ${url.hostname}`));
}
const path = url.pathname + url.search;
if (supports('web_app_open_tg_link', access(version))) {
url = new URL(url);
return postEvent('web_app_open_tg_link', { path_full: url.pathname + url.search });
return postEvent('web_app_open_tg_link', { path_full: path });
}

window.location.href = urlString;
return E.right(undefined);
window.location.href = 'https://telegram.me' + path;
return fp.either.right(undefined);
}, { ...rest, returns: 'either' });
}

// #__NO_SIDE_EFFECTS__
function instantiate() {
return create(fn.pipe(
return createOpenTelegramLink(fp.function.pipe(
sharedFeatureOptions(),
withPostEvent,
withVersion,
Expand Down
3 changes: 3 additions & 0 deletions packages/sdk/src/helpers/isTelegramHostname.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function isTelegramHostname(hostname: string): boolean {
return ['t.me', 'telegram.dog', 'telegram.me'].includes(hostname);
}