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/chilled-bees-unite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@telegram-apps/bridge": minor
---

Implement `postMessage` function and related `postMessageImplementation` signal. Enhance `mockTelegramEnv` with `resetPostMessage` option and properly wrap the `window.parent.postMessage` method. Add explanation on why passed launch parameters are invalid.
79 changes: 58 additions & 21 deletions packages/bridge/src/env/mockTelegramEnv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,39 @@ import {
type LaunchParamsLike,
MiniAppsMessageSchema,
serializeLaunchParamsQuery,
parseLaunchParamsQuery,
} from '@telegram-apps/transformers';
import { If, IsNever, setStorageValue } from '@telegram-apps/toolkit';

import { logInfo } from '@/debug.js';
import { isIframe } from '@/env/isIframe.js';
import type { MethodName, MethodParams } from '@/methods/types/index.js';
import { InvalidLaunchParamsError } from '@/errors.js';
import { postMessageImplementation } from '@/methods/postMessage.js';

/**
* Mocks the environment and imitates Telegram Mini Apps behavior.
*
* We usually use this function in the following cases:
* 1. We are developing an application outside the Telegram environment and would like to imitate
* the Telegram client in order to re-create the same communication behavior.
* 2. We would like to intercept some Telegram Mini Apps methods' calls in order to enhance them
* or write a custom behavior. It is extremely useful in some Telegram clients improperly handling
* Mini Apps methods' calls and not even responding.
*
* Note that calling this function in Telegram web clients, the `postMessageImplementation` signal
* value will be updated with a new one, enhancing previously set signal value to allow wrapping
* the original `window.parent.postMessage` function. In other words, calling `mockTelegramEnv`
* function N times, you will effectively wrap previously set implementation N times, so be
* careful calling this function several times during a single lifecycle of the app. In case you
* would like to avoid such kind of behavior, use the `resetPostMessage` option.
*/
export function mockTelegramEnv({ launchParams, onEvent }: {
export function mockTelegramEnv({ launchParams, onEvent, resetPostMessage }: {
/**
* Launch parameters to mock. They will be saved in the session storage making
* the `retrieveLaunchParams` function return them.
* Launch parameters to mock. They will be saved in the storage, so the SDK functions could
* retrieve them.
*
* Note that this value must have tgWebAppData presented in a raw format as long as you will
* Note that this value must have `tgWebAppData` presented in a raw format as long as you will
* need it when retrieving init data in this format. Otherwise, init data may be broken.
*/
launchParams?:
Expand All @@ -30,16 +46,30 @@ export function mockTelegramEnv({ launchParams, onEvent }: {
| URLSearchParams;
/**
* Function that will be called if a Mini Apps method call was requested by the mini app.
*
* It receives a Mini Apps method name along with the passed payload.
*
* Note that using the `next` function, in non-web environments it uses the
* `window.TelegramWebviewProxy.postEvent`.
*
* Talking about the web versions of Telegram, the value is a bit more complex - it will
* equal to the value stored in the `postMessageImplementation` signal set previously. By default,
* this value contains a function utilizing the `window.parent.postMessage` method.
* @param event - event information.
* @param next - function to call the originally wrapped function (window.parent.postMessage
* or window.TelegramWebviewProxy.postEvent).
* @param next - function to call the original method used to call a Mini Apps method.
*/
onEvent?: (
event: {
[M in MethodName]: [M, If<IsNever<MethodParams<M>>, void, MethodParams<M>>]
}[MethodName] | [string, unknown],
next: () => void,
) => void;
/**
* Removes all previously set enhancements of the `window.parent.postMessage` function set
* by other `mockTelegramEnv` calls.
* @default false
*/
resetPostMessage?: boolean;
} = {}): void {
if (launchParams) {
// If launch parameters were passed, save them in the session storage, so
Expand All @@ -58,54 +88,61 @@ export function mockTelegramEnv({ launchParams, onEvent }: {

// Remember to check if launch params are valid.
if (!isLaunchParamsQuery(launchParamsQuery)) {
throw new InvalidLaunchParamsError(launchParamsQuery);
try {
parseLaunchParamsQuery(launchParamsQuery);
} catch (e) {
throw new InvalidLaunchParamsError(launchParamsQuery, e);
}
}
setStorageValue('launchParams', launchParamsQuery);
}

// Original postEvent firstly checks if the current environment is iframe.
// That's why we have a separate branch for this environment here too.
if (isIframe()) {
if (!onEvent) {
return;
}
const MiniAppsMessageJson = pipe(
string(),
jsonParse(),
MiniAppsMessageSchema,
);

// As long as postEvent uses window.parent.postMessage, we should rewire it.
const postMessage = window.parent.postMessage.bind(window.parent);
window.parent.postMessage = (...args) => {
// As long as the postEvent function uses the postMessage method, we should rewire it.
resetPostMessage && postMessageImplementation.reset();
const original = postMessageImplementation();
postMessageImplementation.set((...args) => {
const [message] = args;
const next = () => {
(postMessage as any)(...args);
(original as any)(...args);
};

if (is(MiniAppsMessageJson, message) && onEvent) {
// Pass only Telegram Mini Apps events to the handler. All other calls should be passed
// to the original handler (window.parent.postMessage likely).
if (is(MiniAppsMessageJson, message)) {
const data = parse(MiniAppsMessageJson, message);
onEvent([data.eventType, data.eventData], next);
} else {
next();
}
};
});

return;
}

// In all other environments, it is enough to define window.TelegramWebviewProxy.postEvent.
const proxy = (window as any).TelegramWebviewProxy || {};
const { postEvent } = proxy;
const postEventDefaulted = proxy.postEvent || (() => undefined);
(window as any).TelegramWebviewProxy = {
...proxy,
postEvent(eventType: string, eventData: string) {
const next = () => {
postEvent && postEvent(eventType, eventData);
postEventDefaulted(eventType, eventData);
};

if (onEvent) {
onEvent([eventType, eventData ? JSON.parse(eventData) : undefined], next);
} else {
next();
}
onEvent
? onEvent([eventType, eventData ? JSON.parse(eventData) : undefined], next)
: next();
},
};

Expand Down
10 changes: 7 additions & 3 deletions packages/bridge/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,13 @@ export const [
export const [
InvalidLaunchParamsError,
isInvalidLaunchParamsError,
] = errorClass<[string]>('InvalidLaunchParamsError', value => [
`Invalid value for launch params: ${value}`,
]);
] = errorClass<[launchParams: string, cause: unknown]>(
'InvalidLaunchParamsError',
(launchParams, cause) => [
`Invalid value for launch params: ${launchParams}`,
{ cause },
],
);

export const [UnknownEnvError, isUnknownEnvError] = errorClass('UnknownEnvError');

Expand Down
1 change: 1 addition & 0 deletions packages/bridge/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export { retrieveRawLaunchParams } from '@/launch-params/retrieveRawLaunchParams
export { retrieveRawInitData } from '@/launch-params/retrieveRawInitData.js';

export type * from '@/methods/types/index.js';
export { postMessage, postMessageImplementation, type PostMessage } from '@/methods/postMessage.js';
export { targetOrigin } from '@/methods/targetOrigin.js';
export { captureSameReq } from '@/methods/captureSameReq.js';
export {
Expand Down
8 changes: 5 additions & 3 deletions packages/bridge/src/methods/postEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { logInfo } from '@/debug.js';
import { isIframe } from '@/env/isIframe.js';
import { hasWebviewProxy } from '@/env/hasWebviewProxy.js';
import { UnknownEnvError } from '@/errors.js';
import { targetOrigin } from '@/methods/targetOrigin.js';
import type {
MethodName,
MethodNameWithOptionalParams,
Expand All @@ -13,6 +12,9 @@ import type {
MethodParams,
} from '@/methods/types/index.js';

import { postMessage } from './postMessage.js';
import { targetOrigin } from './targetOrigin.js';

export type PostEventFn = typeof postEvent;

/**
Expand Down Expand Up @@ -56,10 +58,10 @@ export function postEvent(

// Telegram Web.
if (isIframe()) {
return w.parent.postMessage(message, targetOrigin());
return postMessage(message, targetOrigin());
}

// Telegram for iOS and macOS and Telegram Desktop.
// Telegram for iOS, macOS, Android and Telegram Desktop.
if (hasWebviewProxy(w)) {
w.TelegramWebviewProxy.postEvent(eventType, JSON.stringify(eventData));
return;
Expand Down
21 changes: 21 additions & 0 deletions packages/bridge/src/methods/postMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { signal } from '@telegram-apps/signals';

export type PostMessage = typeof window.parent.postMessage;

/**
* Signal containing a custom implementation of the method to post a message to the parent
* window. We usually use it to send a message in web versions of Telegram.
*
* Initially, this value contains a function behaving like the `window.parent.postMessage` method.
*/
export const postMessageImplementation = signal<PostMessage>((...args: any[]) => {
return window.parent.postMessage(...args as Parameters<PostMessage>);
});

/**
* Posts a message to the parent window. We usually use it to send a message in web versions of Telegram.
* @param args - `window.parent.postMessage` arguments.
*/
export const postMessage: PostMessage = (...args) => {
return postMessageImplementation()(...args as unknown as Parameters<PostMessage>);
};
7 changes: 5 additions & 2 deletions packages/bridge/src/resetPackageState.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { targetOrigin } from '@/methods/targetOrigin.js';
import { setDebug } from '@/debug.js';
import { offAll } from '@/events/emitter.js';
import { postMessageImplementation } from '@/methods/postMessage.js';

/**
* Resets the package state. Normally, you don't use this function in your application.
Expand All @@ -9,6 +10,8 @@ import { offAll } from '@/events/emitter.js';
export function resetPackageState() {
offAll();
setDebug(false);
targetOrigin.unsubAll();
targetOrigin.reset();
[postMessageImplementation, targetOrigin].forEach(s => {
s.unsubAll();
s.reset();
});
}
3 changes: 3 additions & 0 deletions packages/sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ export {
TimeoutError,
AbortablePromise,
ManualPromise,
postMessage,
postMessageImplementation,
type PostMessage,
} from '@telegram-apps/bridge';
export {
isRGB,
Expand Down