UNPKG

@telegram-apps/bridge

Version:

TypeScript package to provide communication layer between Mini App and Telegram application.

1 lines 64.3 kB
{"version":3,"file":"index.cjs","sources":["../src/base64-url/encodeBase64Url.ts","../src/base64-url/decodeBase64Url.ts","../src/env/hasWebviewProxy.ts","../src/env/isIframe.ts","../src/events/createEmitter.ts","../src/events/emitEvent.ts","../src/debug.ts","../src/logger.ts","../src/events/emitter.ts","../src/errors.ts","../src/methods/postMessage.ts","../src/methods/targetOrigin.ts","../src/methods/postEvent.ts","../src/utils/request.ts","../src/launch-params/retrieveRawLaunchParams.ts","../src/launch-params/retrieveLaunchParams.ts","../src/env/isTMA.ts","../src/env/mockTelegramEnv.ts","../src/launch-params/retrieveRawInitData.ts","../src/methods/captureSameReq.ts","../src/utils/compareVersions.ts","../src/methods/supports.ts","../src/methods/createPostEvent.ts","../src/start-param/createStartParam.ts","../src/start-param/decodeStartParam.ts","../src/start-param/isSafeToCreateStartParam.ts","../src/utils/invokeCustomMethod.ts","../src/applyPolyfills.ts","../src/resetPackageState.ts"],"sourcesContent":["/**\n * Creates a base-64-url encoded ASCII string from the passed value.\n * @param value - the value to encode.\n * @see Learn more about base64url:\n * https://herongyang.com/Encoding/Base64URL-Encoding-Algorithm.html\n * @see Source:\n * https://developer.mozilla.org/ru/docs/Glossary/Base64#solution_1_–_escaping_the_string_before_encoding_it\n */\nexport function encodeBase64Url(value: string): string {\n // first we use encodeURIComponent to get percent-encoded UTF-8,\n // then we convert the percent encodings into raw bytes which\n // can be fed into btoa.\n return btoa(\n encodeURIComponent(value).replace(/%([0-9A-F]{2})/g, (_, p1) => {\n return String.fromCharCode(parseInt(`0x${p1}`));\n }),\n )\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_');\n}","/**\n * Decodes a base-64-url ASCII string.\n * @param value - the value to decode.\n * @throws {DOMException} If the passed value is an invalid base64url string.\n * @see Learn more about base64url:\n * https://herongyang.com/Encoding/Base64URL-Encoding-Algorithm.html\n * @see Source:\n * https://developer.mozilla.org/ru/docs/Glossary/Base64#solution_1_–_escaping_the_string_before_encoding_it\n */\nexport function decodeBase64Url(value: string): string {\n return decodeURIComponent(\n atob(value)\n .replace(/-/g, '+')\n .replace(/_/g, '/')\n .split('')\n .map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))\n .join(''),\n );\n}","import { looseObject, function as fn, is } from 'valibot';\n\n/**\n * Returns true in case, passed value contains path `TelegramWebviewProxy.postEvent` property and\n * `postEvent` is a function.\n * @param value - value to check.\n */\nexport function hasWebviewProxy<T>(value: T): value is T & {\n TelegramWebviewProxy: {\n postEvent: (...args: unknown[]) => unknown;\n };\n} {\n return is(\n looseObject({ TelegramWebviewProxy: looseObject({ postEvent: fn() }) }),\n value,\n );\n}\n","/**\n * @see https://stackoverflow.com/a/326076\n * @returns True, if current environment is iframe.\n */\nexport function isIframe(): boolean {\n try {\n return window.self !== window.top;\n } catch {\n return true;\n }\n}\n","import mitt, {\n type Emitter,\n type EventHandlerMap,\n type EventType,\n type Handler,\n} from 'mitt';\nimport type { If, IsNever, IsUndefined, Or } from '@telegram-apps/toolkit';\n\nexport type WildcardHandler<E> = Handler<{\n [K in keyof E]: [K, If<Or<IsNever<E[K]>, IsUndefined<E[K]>>, void, E[K]>]\n}[keyof E]>;\n\nexport interface OnFn<E> {\n /**\n * Adds a new listener for the specified event.\n * @param type - event name.\n * @param handler - event listener.\n * @param once - should listener be called only once.\n * @returns Function to remove bound event listener.\n */<K extends keyof E>(type: K, handler: Handler<E[K]>, once?: boolean): VoidFunction;\n /**\n * Adds a listener for all events.\n * @param type - event name.\n * @param handler - event listener.\n * @param once - should listener be called only once.\n * @returns Function to remove bound event listener.\n */\n (type: '*', handler: WildcardHandler<E>, once?: boolean): VoidFunction;\n}\n\nexport interface OffFn<E> {\n /**\n * Removes a listener for the specified event.\n * @param type - event to listen.\n * @param handler - event listener to remove.\n * @param once - had this listener to be called only once.\n */<K extends keyof E>(type: K, handler: Handler<E[K]>, once?: boolean): void;\n /**\n * Removes a listener for all events.\n * @param type - event to stop listening.\n * @param handler - event listener to remove.\n * @param once - had this listener to be called only once.\n */\n (type: '*', handler: WildcardHandler<E>, once?: boolean): void;\n}\n\nexport interface EmitFn<E> {\n <K extends keyof E>(type: K, event: E[K]): void;\n <K extends keyof E>(type: undefined extends E[K] ? K : never): void;\n}\n\n/**\n * Creates a new enhanced event emitter.\n * @param onFirst - will be called when the first event was added.\n * @param onEmpty - will be called when emitter's listeners' map was emptied.\n */\nexport function createEmitter<E extends object>(\n onFirst: VoidFunction,\n onEmpty: VoidFunction,\n): [\n on: OnFn<E>,\n off: OffFn<E>,\n emit: EmitFn<E>,\n clear: VoidFunction\n] {\n type EventMap = Map<\n (...args: any) => void,\n [handler: (...args: any) => void, once: boolean][]\n >;\n\n const emitter = (mitt as any as {\n <E extends Record<EventType, unknown>>(all?: EventHandlerMap<E>): Emitter<E>;\n })<E & Record<string | symbol, unknown>>();\n const map = new Map<keyof E | '*', EventMap>();\n\n const off: OffFn<E> = (event: keyof E | '*', handler: (...args: any) => void, once?: boolean) => {\n once ||= false;\n\n const eventMap: EventMap = map.get(event) || new Map();\n map.set(event, eventMap);\n\n const handlers = eventMap.get(handler) || [];\n eventMap.set(handler, handlers);\n\n const index = handlers.findIndex(item => item[1] === once);\n if (index >= 0) {\n // Remove the related handler.\n emitter.off(event, handlers[index][0]);\n\n // Remove the handler from the cache array.\n handlers.splice(index, 1);\n\n // If after removal, there are no handlers left, we should remove the entry from the cache.\n !handlers.length && eventMap.delete(handler);\n if (!eventMap.size) {\n map.delete(event);\n !map.size && onEmpty();\n }\n }\n };\n\n return [\n function on(event: keyof E | '*', handler: (...args: any[]) => any, once?: boolean) {\n !map.size && onFirst();\n\n function cleanup() {\n off(event as any, handler, once);\n }\n\n function fn(...args: any[]) {\n once && cleanup();\n if (event === '*') {\n handler(args);\n } else {\n handler(...args);\n }\n }\n\n emitter.on(event, fn);\n\n // Add this handler to the cache, so we could remove it using the passed listener.\n const eventMap = map.get(event) || new Map();\n map.set(event, eventMap);\n\n const handlers = eventMap.get(handler) || [];\n eventMap.set(handler, handlers);\n handlers.push([fn, once || false]);\n\n return cleanup;\n },\n off,\n // eslint-disable-next-line @typescript-eslint/unbound-method\n emitter.emit,\n function offAll() {\n const prevSize = emitter.all.size;\n emitter.all.clear();\n map.clear();\n prevSize && onEmpty();\n },\n ];\n}\n","import type { EventWithoutPayload, EventWithPayload, EventPayload } from '@/events/types/index.js';\n\n/**\n * Emits an event without payload sent from the Telegram native application like it was sent in\n * a default web environment between two iframes.\n *\n * It dispatches a new MessageEvent and expects it to be handled via\n * the `window.addEventListener('message', ...)` call, as a developer would do it to handle\n * messages sent from the parent iframe.\n * @param eventType - event name.\n */\nexport function emitEvent<E extends EventWithoutPayload>(eventType: E): void;\n\n/**\n * Emits an event with payload sent from the Telegram native application like it was sent in\n * a default web environment between two iframes.\n *\n * It dispatches a new MessageEvent and expects it to be handled via\n * the `window.addEventListener('message', ...)` call, as a developer would do it to handle\n * messages sent from the parent iframe.\n * @param eventType - event name.\n * @param eventData - event payload.\n */\nexport function emitEvent<E extends EventWithPayload>(\n eventType: E,\n eventData: EventPayload<E>,\n): void;\n\n/**\n * Emits an unknown event sent from the Telegram native application like it was sent in a default\n * web environment between two iframes.\n *\n * It dispatches a new MessageEvent and expects it to be handled via\n * the `window.addEventListener('message', ...)` call, as a developer would do it to handle\n * messages sent from the parent iframe.\n * @param eventType - event name.\n * @param eventData - event payload.\n */\nexport function emitEvent<E extends string>(\n eventType: E,\n eventData: E extends EventWithoutPayload\n ? never\n : E extends EventWithPayload\n ? EventPayload<E>\n : unknown,\n): void;\n\n/**\n * Emits an event sent from the Telegram native application like it was sent in a default web\n * environment between two iframes.\n *\n * It dispatches a new MessageEvent and expects it to be handled via\n * the `window.addEventListener('message', ...)` call, as a developer would do it to handle\n * messages sent from the parent iframe.\n * @param eventType - event name.\n * @param eventData - event payload.\n */\nexport function emitEvent(eventType: string, eventData?: unknown): void {\n window.dispatchEvent(new MessageEvent('message', {\n data: JSON.stringify({ eventType, eventData }),\n // We specify window.parent to imitate the case, the parent iframe sent us this event.\n source: window.parent,\n }));\n}\n","import type { SubscribeListener } from '@/events/types/index.js';\nimport { off, on } from '@/events/emitter.js';\nimport { logger } from '@/logger.js';\n\n/**\n * The package debug mode.\n *\n * Enabling debug mode leads to printing additional messages in the console related to the\n * processes inside the package.\n */\nexport let debug = false;\n\nconst listener: SubscribeListener = event => {\n logger().log('Event received:', event);\n};\n\n/**\n * Sets the package debug mode.\n *\n * Enabling debug mode leads to printing additional messages in the console related to the\n * processes inside the package.\n * @param value - enable debug mode.\n */\nexport function setDebug(value: boolean): void {\n if (value !== debug) {\n debug = value;\n debug ? on('*', listener) : off('*', listener);\n }\n}\n","import { createLogger, type Logger } from '@telegram-apps/toolkit';\nimport { signal } from '@telegram-apps/signals';\n\nimport { debug } from '@/debug.js';\n\nexport const logger = signal<Logger>(createLogger('Bridge', {\n bgColor: '#9147ff',\n textColor: 'white',\n shouldLog() {\n return debug;\n },\n}));\n","import {\n type InferOutput,\n parse,\n pipe,\n string,\n looseObject,\n optional,\n unknown,\n number,\n boolean,\n nullish,\n type BaseSchema,\n} from 'valibot';\nimport { jsonParse, MiniAppsMessageSchema, themeParams } from '@telegram-apps/transformers';\n\nimport { createEmitter } from '@/events/createEmitter.js';\nimport type { EventName, EventPayload, Events } from '@/events/types/index.js';\nimport { emitEvent } from '@/events/emitEvent.js';\nimport { logger } from '@/logger.js';\n\n/**\n * Transformers for problematic Mini Apps events.\n */\nconst transformers = {\n clipboard_text_received: looseObject({\n req_id: string(),\n data: nullish(string()),\n }),\n custom_method_invoked: looseObject({\n req_id: string(),\n result: optional(unknown()),\n error: optional(string()),\n }),\n popup_closed: nullish(\n looseObject({ button_id: nullish(string(), () => undefined) }),\n {},\n ),\n viewport_changed: looseObject({\n height: number(),\n width: nullish(number(), () => window.innerWidth),\n is_state_stable: boolean(),\n is_expanded: boolean(),\n }),\n theme_changed: looseObject({\n theme_params: themeParams(),\n }),\n} as const satisfies { [E in EventName]?: BaseSchema<unknown, EventPayload<E>, any> };\n\nfunction listener(event: MessageEvent): void {\n // Ignore non-parent window messages.\n if (event.source !== window.parent) {\n return;\n }\n\n // Parse incoming event data.\n let message: InferOutput<typeof MiniAppsMessageSchema>;\n try {\n message = parse(pipe(string(), jsonParse(), MiniAppsMessageSchema), event.data);\n } catch {\n // We ignore incorrect messages as they could be generated by any other code.\n return;\n }\n\n const { eventType, eventData } = message;\n const schema = transformers[eventType as keyof typeof transformers];\n\n let data: unknown\n try {\n data = schema ? parse(schema, eventData) : eventData;\n } catch (cause) {\n return logger().forceError(\n [\n `An error occurred processing the \"${eventType}\" event from the Telegram application.`,\n 'Please, file an issue here:',\n 'https://github.com/Telegram-Mini-Apps/telegram-apps/issues/new/choose',\n ].join('\\n'),\n message,\n cause,\n );\n }\n emit(eventType as any, data);\n}\n\nexport const [\n on,\n off,\n emit,\n offAll,\n] = createEmitter<Events>(\n () => {\n const w = window as any;\n\n // Define all functions responsible for receiving an event from the Telegram client.\n // All these \"ports\" should narrow the communication way to a single specific one - the way\n // accepted by the web version of Telegram between iframes.\n const obj = { receiveEvent: emitEvent };\n w.TelegramGameProxy_receiveEvent = emitEvent;\n w.TelegramGameProxy = obj;\n w.Telegram = { WebView: obj };\n\n // Add a listener handling events sent from the Telegram web application and also events\n // generated by the local emitEvent function.\n // This handler should emit a new event using the library event emitter.\n window.addEventListener('message', listener);\n },\n () => {\n ['TelegramGameProxy_receiveEvent', 'TelegramGameProxy', 'Telegram'].forEach((prop) => {\n delete (window as any)[prop];\n });\n window.removeEventListener('message', listener);\n },\n);\n","import { errorClass, errorClassWithData } from 'error-kid';\nimport type { Version } from '@telegram-apps/types';\n\nexport const [\n MethodUnsupportedError,\n isMethodUnsupportedError,\n] = errorClass<[method: string, version: Version]>(\n 'MethodUnsupportedError',\n (method, version) => [\n `Method \"${method}\" is unsupported in Mini Apps version ${version}`,\n ],\n);\n\nexport const [\n MethodParameterUnsupportedError,\n isMethodMethodParameterUnsupportedError,\n] = errorClass<[method: string, param: string, version: Version]>(\n 'MethodParameterUnsupportedError',\n (method, param, version) => [\n `Parameter \"${param}\" of \"${method}\" method is unsupported in Mini Apps version ${version}`,\n ],\n);\n\nexport const [\n LaunchParamsRetrieveError,\n isLaunchParamsRetrieveError,\n] = errorClassWithData<\n { errors: [source: string, error: unknown][] },\n [[source: string, error: unknown][]]\n>(\n 'LaunchParamsRetrieveError',\n errors => ({ errors }),\n errors => [\n [\n 'Unable to retrieve launch parameters from any known source. Perhaps, you have opened your app outside Telegram?',\n '📖 Refer to docs for more information:',\n 'https://docs.telegram-mini-apps.com/packages/telegram-apps-bridge/environment',\n '',\n 'Collected errors:',\n ...errors.map(([source, error]) => {\n return `Source: ${source} / ${error instanceof Error ? error.message : String(error)}`;\n }),\n ].join('\\n'),\n ],\n);\n\nexport const [\n InvalidLaunchParamsError,\n isInvalidLaunchParamsError,\n] = errorClass<[launchParams: string, cause: unknown]>(\n 'InvalidLaunchParamsError',\n (launchParams, cause) => [\n `Invalid value for launch params: ${launchParams}`,\n { cause },\n ],\n);\n\nexport const [UnknownEnvError, isUnknownEnvError] = errorClass('UnknownEnvError');\n\nexport const [\n InvokeCustomMethodError,\n isInvokeCustomMethodError,\n] = errorClass<[error: string]>(\n 'InvokeCustomMethodError',\n error => [`Server returned error: ${error}`],\n);","import { signal } from '@telegram-apps/signals';\n\nimport { logger } from '@/logger.js';\n\nexport type PostMessage = typeof window.parent.postMessage;\n\n/**\n * Signal containing a custom implementation of the method to post a message to the parent\n * window. We usually use it to send a message in web versions of Telegram.\n *\n * Initially, this value contains a function behaving like the `window.parent.postMessage` method.\n */\nexport const postMessageImplementation = signal<PostMessage>((...args: any[]) => {\n try {\n window.parent.postMessage(...args as Parameters<PostMessage>);\n } catch (e) {\n if (e instanceof SyntaxError) {\n logger().forceError(\n 'Unable to call window.parent.postMessage due to incorrectly configured target origin. Use the setTargetOrigin method to allow this origin to receive events',\n e,\n );\n } else {\n logger().forceError(e);\n }\n }\n});\n\n/**\n * Posts a message to the parent window. We usually use it to send a message in web versions of\n * Telegram.\n * @param args - `window.parent.postMessage` arguments.\n */\nexport const postMessage: PostMessage = (...args) => {\n return postMessageImplementation()(...args as unknown as Parameters<PostMessage>);\n};\n","import { signal } from '@telegram-apps/signals';\n\nimport { logger } from '@/logger.js';\n\n/**\n * Target origin used by the `postEvent` method.\n *\n * You don't need to override this value until you know what you are doing.\n * @default 'https://web.telegram.org'\n */\nexport const targetOrigin = signal('https://web.telegram.org');\n\n/**\n * Sets a new target origin that is being used when calling the `postEvent` function in Telegram\n * web versions.\n * @param origin - allowed target origin value.\n * @see targetOrigin\n */\nexport function setTargetOrigin(origin: string) {\n targetOrigin.set(origin);\n logger().log('New target origin set', origin);\n}","import { is, looseObject, function as fn } from 'valibot';\n\nimport { logger } from '@/logger.js';\nimport { isIframe } from '@/env/isIframe.js';\nimport { hasWebviewProxy } from '@/env/hasWebviewProxy.js';\nimport { UnknownEnvError } from '@/errors.js';\nimport type {\n MethodName,\n MethodNameWithOptionalParams,\n MethodNameWithoutParams,\n MethodNameWithRequiredParams,\n MethodParams,\n} from '@/methods/types/index.js';\n\nimport { postMessage } from './postMessage.js';\nimport { targetOrigin } from './targetOrigin.js';\n\nexport type PostEventFn = typeof postEvent;\n\n/**\n * Calls Mini Apps methods requiring parameters.\n * @param method - method name.\n * @param params - options along with params.\n * @throws {UnknownEnvError} The environment is unknown.\n */\nexport function postEvent<Method extends MethodNameWithRequiredParams>(\n method: Method,\n params: MethodParams<Method>,\n): void;\n\n/**\n * Calls Mini Apps methods accepting no parameters at all.\n * @param method - method name.\n * @throws {UnknownEnvError} The environment is unknown.\n */\nexport function postEvent(method: MethodNameWithoutParams): void;\n\n/**\n * Calls Mini Apps methods accepting optional parameters.\n * @param method - method name.\n * @param params - options along with params.\n * @throws {UnknownEnvError} The environment is unknown.\n */\nexport function postEvent<Method extends MethodNameWithOptionalParams>(\n method: Method,\n params?: MethodParams<Method>,\n): void;\n\nexport function postEvent(\n eventType: MethodName,\n eventData?: MethodParams<MethodName>,\n): void {\n logger().log('Posting event:', eventData ? { eventType, eventData } : { eventType });\n\n const w = window;\n\n const message = JSON.stringify({ eventType, eventData });\n\n // Telegram Web.\n if (isIframe()) {\n return postMessage(message, targetOrigin());\n }\n\n // Telegram for iOS, macOS, Android and Telegram Desktop.\n if (hasWebviewProxy(w)) {\n w.TelegramWebviewProxy.postEvent(eventType, JSON.stringify(eventData));\n return;\n }\n\n // Telegram for Windows Phone or Android.\n if (is(looseObject({ external: looseObject({ notify: fn() }) }), w)) {\n w.external.notify(message);\n return;\n }\n\n // Otherwise, the current environment is unknown, and we are not able to send event.\n throw new UnknownEnvError();\n}\n","import {\n createCbCollector,\n type If,\n type IsNever,\n} from '@telegram-apps/toolkit';\nimport { AbortablePromise, type PromiseOptions } from 'better-promises';\n\nimport { on } from '@/events/emitter.js';\nimport { postEvent, PostEventFn } from '@/methods/postEvent.js';\nimport type {\n MethodName,\n MethodNameWithOptionalParams,\n MethodNameWithoutParams,\n MethodNameWithRequiredParams,\n MethodParams,\n} from '@/methods/types/index.js';\nimport type { EventName, EventPayload } from '@/events/types/events.js';\n\ntype AnyEventName = EventName | EventName[];\n\nexport type RequestCaptureFnEventsPayload<E extends EventName[]> = E extends (infer U extends EventName)[]\n ? {\n [K in U]: If<\n IsNever<EventPayload<K>>,\n { event: K },\n { event: K; payload: EventPayload<K> }\n >\n }[U]\n : never;\n\nexport type RequestCaptureEventsFn<E extends EventName[]> = (\n payload: RequestCaptureFnEventsPayload<E>,\n) => boolean\n\nexport type RequestCaptureEventFn<E extends EventName> = If<\n IsNever<EventPayload<E>>,\n () => boolean,\n (payload: EventPayload<E>) => boolean\n>;\n\nexport type RequestCaptureFn<E extends AnyEventName> = E extends EventName[]\n ? RequestCaptureEventsFn<E>\n : E extends EventName\n ? RequestCaptureEventFn<E>\n : never;\n\nexport interface RequestOptions<E extends AnyEventName>\n extends Omit<PromiseOptions, 'rejectOnAbort'> {\n /**\n * Should return true if this event should be captured.\n * The first compatible request will be captured if this property is omitted.\n */\n capture?: RequestCaptureFn<E>;\n /**\n * Custom function to call mini apps methods.\n */\n postEvent?: PostEventFn;\n}\n\nexport type RequestResult<E extends AnyEventName> =\n E extends (infer U extends EventName)[]\n ? U extends infer K extends EventName\n ? If<IsNever<EventPayload<K>>, undefined, EventPayload<K>>\n : never\n : E extends EventName\n ? If<IsNever<EventPayload<E>>, undefined, EventPayload<E>>\n : never;\n\nexport type RequestFn = typeof request;\n\n/**\n * Performs a request waiting for specified events to occur.\n *\n * This overriding is used for methods, requiring parameters.\n * @param method - method name.\n * @param eventOrEvents - tracked event or events.\n * @param options - additional options.\n */\nexport function request<M extends MethodNameWithRequiredParams, E extends AnyEventName>(\n method: M,\n eventOrEvents: E,\n options: RequestOptions<E> & { params: MethodParams<M> },\n): AbortablePromise<RequestResult<E>>;\n\n/**\n * Performs a request waiting for specified events to occur.\n *\n * This overriding is used for methods with optional parameters.\n * @param method - method name.\n * @param eventOrEvents - tracked event or events.\n * @param options - additional options.\n */\nexport function request<M extends MethodNameWithOptionalParams, E extends AnyEventName>(\n method: M,\n eventOrEvents: E,\n options?: RequestOptions<E> & { params?: MethodParams<M> },\n): AbortablePromise<RequestResult<E>>;\n\n/**\n * Performs a request waiting for specified events to occur.\n *\n * This overriding is used for methods without parameters.\n * @param method - method name.\n * @param eventOrEvents - tracked event or events.\n * @param options - additional options.\n */\nexport function request<M extends MethodNameWithoutParams, E extends AnyEventName>(\n method: M,\n eventOrEvents: E,\n options?: RequestOptions<E>,\n): AbortablePromise<RequestResult<E>>;\n\nexport function request<M extends MethodName, E extends AnyEventName>(\n method: M,\n eventOrEvents: E,\n options?: RequestOptions<E> & { params?: MethodParams<M> },\n): AbortablePromise<RequestResult<E>> {\n options ||= {};\n const { capture } = options;\n const [addCleanup, cleanup] = createCbCollector();\n\n return new AbortablePromise<RequestResult<E>>((resolve) => {\n // We need to iterate over all tracked events and create their event listeners.\n ((Array.isArray(eventOrEvents) ? eventOrEvents : [eventOrEvents])).forEach(event => {\n // Each event listener waits for the event to occur.\n // Then, if the capture function was passed, we should check if the event should be captured.\n // If the function is omitted, we instantly capture the event.\n addCleanup(\n on(event, payload => {\n if (!capture || (\n Array.isArray(eventOrEvents)\n ? (capture as RequestCaptureEventsFn<EventName[]>)({\n event,\n payload,\n } as RequestCaptureFnEventsPayload<EventName[]>)\n : (capture as RequestCaptureEventFn<EventName>)(payload)\n )) {\n resolve(payload as RequestResult<E>);\n }\n }),\n );\n });\n\n (options.postEvent || postEvent)(method as any, (options as any).params);\n }, options)\n .finally(cleanup);\n}\n","import { isLaunchParamsQuery, parseLaunchParamsQuery } from '@telegram-apps/transformers';\nimport { getStorageValue, setStorageValue } from '@telegram-apps/toolkit';\n\nimport { LaunchParamsRetrieveError } from '@/errors.js';\n\nconst SESSION_STORAGE_KEY = 'launchParams';\n\n/**\n * @param urlString - URL to extract launch parameters from.\n * @returns Launch parameters from the specified URL.\n * @throws Error if function was unable to extract launch parameters from the passed URL.\n */\nfunction fromURL(urlString: string): string {\n return urlString\n // Replace everything before this first hashtag or question sign.\n .replace(/^[^?#]*[?#]/, '')\n // Replace all hashtags and question signs to make it look like some search params.\n .replace(/[?#]/g, '&');\n}\n\n/**\n * @returns Launch parameters in a raw format from any known source.\n * @throws {LaunchParamsRetrieveError} Unable to retrieve launch parameters. They are probably\n * invalid.\n */\nexport function retrieveRawLaunchParams(): string {\n const errors: [source: string, error: unknown][] = [];\n for (const [retrieve, source] of [\n // Try to retrieve launch parameters from the current location. This method can return\n // nothing in case, location was changed, and then the page was reloaded.\n [() => fromURL(window.location.href), 'window.location.href'],\n // Then, try using the lower level API - window.performance.\n [() => {\n const navigationEntry = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming | undefined;\n return navigationEntry && fromURL(navigationEntry.name);\n }, 'performance navigation entries'],\n [() => getStorageValue<string>(SESSION_STORAGE_KEY), 'local storage'],\n ] as const) {\n const v = retrieve();\n if (!v) {\n errors.push([source, new Error('Source is empty')]);\n continue;\n }\n if (isLaunchParamsQuery(v)) {\n setStorageValue(SESSION_STORAGE_KEY, v);\n return v;\n }\n try {\n parseLaunchParamsQuery(v);\n } catch (e) {\n errors.push([source, e]);\n }\n }\n throw new LaunchParamsRetrieveError(errors);\n}","import { LaunchParamsSchema, parseLaunchParamsQuery } from '@telegram-apps/transformers';\nimport {\n type DeepConvertSnakeKeysToCamelCase,\n deepSnakeToCamelObjKeys,\n} from '@telegram-apps/toolkit';\nimport type { InferOutput } from 'valibot';\n\nimport { retrieveRawLaunchParams } from '@/launch-params/retrieveRawLaunchParams.js';\n\nexport type RetrieveLPResult = InferOutput<typeof LaunchParamsSchema>;\nexport type RetrieveLPResultCamelCased =\n DeepConvertSnakeKeysToCamelCase<InferOutput<typeof LaunchParamsSchema>>;\n\n/**\n * @returns Launch parameters from any known source.\n * @param camelCase - should the output be camel-cased.\n * @throws {LaunchParamsRetrieveError} Unable to retrieve launch parameters. They are probably\n * invalid.\n */\nexport function retrieveLaunchParams(camelCase?: false): RetrieveLPResult;\n\n/**\n * @returns Launch parameters from any known source.\n * @param camelCase - should the output be camel-cased.\n * @throws {LaunchParamsRetrieveError} Unable to retrieve launch parameters. They are probably\n * invalid.\n */\nexport function retrieveLaunchParams(camelCase: true): RetrieveLPResultCamelCased;\n\n/**\n * @returns Launch parameters from any known source.\n * @throws {LaunchParamsRetrieveError} Unable to retrieve launch parameters. They are probably\n * invalid.\n */\nexport function retrieveLaunchParams(camelCase?: boolean):\n | RetrieveLPResult\n | RetrieveLPResultCamelCased {\n const launchParams = parseLaunchParamsQuery(retrieveRawLaunchParams());\n return camelCase ? deepSnakeToCamelObjKeys(launchParams) : launchParams;\n}\n","import { AbortablePromise, type PromiseOptions } from 'better-promises';\n\nimport { request } from '@/utils/request.js';\nimport { hasWebviewProxy } from '@/env/hasWebviewProxy.js';\nimport { retrieveLaunchParams } from '@/launch-params/retrieveLaunchParams.js';\n\n/**\n * Returns true if the current environment is Telegram Mini Apps.\n *\n * It uses the `retrieveLaunchParams` function to determine if the environment contains\n * launch parameters. In case it does, true will be returned.\n *\n * In case you need stricter checks, use async override of this function.\n */\nexport function isTMA(): boolean;\n\n/**\n * Returns promise with true if the current environment is Telegram Mini Apps.\n *\n * First of all, it checks if the current environment contains traits specific to the\n * Mini Apps environment.\n * Then, it attempts to call a Mini Apps method and waits for a response to be received.\n *\n * In case you need less strict checks, use sync override of this function.\n */\nexport function isTMA(type: 'complete', options?: PromiseOptions): AbortablePromise<boolean>\n\nexport function isTMA(\n type?: 'complete',\n options?: PromiseOptions,\n): boolean | AbortablePromise<boolean> {\n if (!type) {\n try {\n retrieveLaunchParams();\n return true;\n } catch {\n return false;\n }\n }\n\n return AbortablePromise.fn(async context => {\n if (hasWebviewProxy(window)) {\n return true;\n }\n try {\n await request('web_app_request_theme', 'theme_changed', context);\n return true;\n } catch {\n return false;\n }\n }, options || { timeout: 100 });\n}\n","import { is, parse, pipe, string } from 'valibot';\nimport {\n isLaunchParamsQuery,\n jsonParse,\n type LaunchParamsLike,\n MiniAppsMessageSchema,\n serializeLaunchParamsQuery,\n parseLaunchParamsQuery,\n} from '@telegram-apps/transformers';\nimport { If, IsNever, setStorageValue } from '@telegram-apps/toolkit';\n\nimport { logger } from '@/logger.js';\nimport { isIframe } from '@/env/isIframe.js';\nimport type { MethodName, MethodParams } from '@/methods/types/index.js';\nimport { InvalidLaunchParamsError } from '@/errors.js';\nimport { postMessageImplementation } from '@/methods/postMessage.js';\n\n/**\n * Mocks the environment and imitates Telegram Mini Apps behavior.\n *\n * We usually use this function in the following cases:\n * 1. We are developing an application outside the Telegram environment and would like to imitate\n * the Telegram client in order to re-create the same communication behavior.\n * 2. We would like to intercept some Telegram Mini Apps methods' calls in order to enhance them\n * or write a custom behavior. It is extremely useful in some Telegram clients improperly handling\n * Mini Apps methods' calls and not even responding.\n *\n * Note that calling this function in Telegram web clients, the `postMessageImplementation` signal\n * value will be updated with a new one, enhancing previously set signal value to allow wrapping\n * the original `window.parent.postMessage` function. In other words, calling `mockTelegramEnv`\n * function N times, you will effectively wrap previously set implementation N times, so be\n * careful calling this function several times during a single lifecycle of the app. In case you\n * would like to avoid such kind of behavior, use the `resetPostMessage` option.\n */\nexport function mockTelegramEnv({ launchParams, onEvent, resetPostMessage }: {\n /**\n * Launch parameters to mock. They will be saved in the storage, so the SDK functions could\n * retrieve them.\n *\n * Note that this value must have `tgWebAppData` presented in a raw format as long as you will\n * need it when retrieving init data in this format. Otherwise, init data may be broken.\n */\n launchParams?:\n | (Omit<LaunchParamsLike, 'tgWebAppData'> & { tgWebAppData?: string | URLSearchParams })\n | string\n | URLSearchParams;\n /**\n * Function that will be called if a Mini Apps method call was requested by the mini app.\n *\n * It receives a Mini Apps method name along with the passed payload.\n *\n * Note that using the `next` function, in non-web environments it uses the\n * `window.TelegramWebviewProxy.postEvent`.\n *\n * Talking about the web versions of Telegram, the value is a bit more complex - it will\n * equal to the value stored in the `postMessageImplementation` signal set previously. By default,\n * this value contains a function utilizing the `window.parent.postMessage` method.\n * @param event - event information.\n * @param next - function to call the original method used to call a Mini Apps method.\n */\n onEvent?: (\n event: {\n [M in MethodName]: [M, If<IsNever<MethodParams<M>>, void, MethodParams<M>>]\n }[MethodName] | [string, unknown],\n next: () => void,\n ) => void;\n /**\n * Removes all previously set enhancements of the `window.parent.postMessage` function set\n * by other `mockTelegramEnv` calls.\n * @default false\n */\n resetPostMessage?: boolean;\n} = {}): void {\n if (launchParams) {\n // If launch parameters were passed, save them in the session storage, so\n // the retrieveLaunchParams function would return them.\n const launchParamsQuery =\n typeof launchParams === 'string' || launchParams instanceof URLSearchParams\n ? launchParams.toString()\n : (\n // Here we have to trick serializeLaunchParamsQuery into thinking, it serializes a valid\n // value. We are doing it because we are working with tgWebAppData presented as a\n // string, not an object as serializeLaunchParamsQuery requires.\n serializeLaunchParamsQuery({ ...launchParams, tgWebAppData: undefined })\n // Then, we just append init data.\n + (launchParams.tgWebAppData ? `&tgWebAppData=${encodeURIComponent(launchParams.tgWebAppData.toString())}` : '')\n );\n\n // Remember to check if launch params are valid.\n if (!isLaunchParamsQuery(launchParamsQuery)) {\n try {\n parseLaunchParamsQuery(launchParamsQuery);\n } catch (e) {\n throw new InvalidLaunchParamsError(launchParamsQuery, e);\n }\n }\n setStorageValue('launchParams', launchParamsQuery);\n }\n\n // Original postEvent firstly checks if the current environment is iframe.\n // That's why we have a separate branch for this environment here too.\n if (isIframe()) {\n if (!onEvent) {\n return;\n }\n const MiniAppsMessageJson = pipe(\n string(),\n jsonParse(),\n MiniAppsMessageSchema,\n );\n\n // As long as the postEvent function uses the postMessage method, we should rewire it.\n resetPostMessage && postMessageImplementation.reset();\n const original = postMessageImplementation();\n postMessageImplementation.set((...args) => {\n const [message] = args;\n const next = () => {\n (original as any)(...args);\n };\n\n // Pass only Telegram Mini Apps events to the handler. All other calls should be passed\n // to the original handler (window.parent.postMessage likely).\n if (is(MiniAppsMessageJson, message)) {\n const data = parse(MiniAppsMessageJson, message);\n onEvent([data.eventType, data.eventData], next);\n } else {\n next();\n }\n });\n\n return;\n }\n\n // In all other environments, it is enough to define window.TelegramWebviewProxy.postEvent.\n const proxy = (window as any).TelegramWebviewProxy || {};\n const postEventDefaulted = proxy.postEvent || (() => undefined);\n (window as any).TelegramWebviewProxy = {\n ...proxy,\n postEvent(eventType: string, eventData: string) {\n const next = () => {\n postEventDefaulted(eventType, eventData);\n };\n onEvent\n ? onEvent([eventType, eventData ? JSON.parse(eventData) : undefined], next)\n : next();\n },\n };\n\n logger().log('Environment was mocked by the mockTelegramEnv function');\n}\n","import { retrieveRawLaunchParams } from '@/launch-params/retrieveRawLaunchParams.js';\n\n/**\n * @returns Raw init data from any known source.\n * @throws {LaunchParamsRetrieveError} Unable to retrieve launch params from any known source.\n */\nexport function retrieveRawInitData(): string | undefined {\n return new URLSearchParams(retrieveRawLaunchParams()).get('tgWebAppData') || undefined;\n}","type CaptureSameReqFn = (payload: { req_id: string }) => boolean;\n\n/**\n * Returns a function which can be used in `request` function `capture` property to capture\n * the event with the same request identifier.\n * @param reqId - request identifier.\n */\nexport function captureSameReq(reqId: string): CaptureSameReqFn {\n return ({ req_id }) => req_id === reqId;\n}\n","import type { Version } from '@telegram-apps/types';\n\nfunction parts(a: Version): number[] {\n return a.split('.').map(Number);\n}\n\n/**\n * @param a - first version.\n * @param b - second version.\n * @returns\n * - `1` if the version \"a\" is greater than \"b\".\n * - `0` the version \"a\" is equal to \"b\".\n * - `-1` the version \"a\" is lower than \"b\".\n */\nexport function compareVersions(a: Version, b: Version): number {\n const aParts = parts(a);\n const bParts = parts(b);\n const len = Math.max(aParts.length, bParts.length);\n\n // Iterate over each part of versions and compare them. In case, part is\n // missing, assume its value is equal to 0.\n for (let i = 0; i < len; i += 1) {\n const aVal = aParts[i] || 0\n const bVal = bParts[i] || 0;\n\n if (aVal === bVal) {\n continue;\n }\n return aVal > bVal ? 1 : -1;\n }\n return 0;\n}\n","import type { Version } from '@telegram-apps/types';\n\nimport { compareVersions } from '@/utils/compareVersions.js';\nimport type {\n MethodName,\n MethodNameWithVersionedParams,\n MethodVersionedParams,\n} from '@/methods/types/index.js';\n\n/**\n * Returns true if \"a\" version is less than or equal to \"b\" version.\n * @param a\n * @param b\n */\nfunction versionLessOrEqual(a: Version, b: Version): boolean {\n return compareVersions(a, b) <= 0;\n}\n\n/**\n * Returns true in case, passed parameter in specified method is supported.\n * @param method - method name\n * @param param - method parameter\n * @param inVersion - platform version.\n */\nexport function supports<M extends MethodNameWithVersionedParams>(\n method: M,\n param: MethodVersionedParams<M>,\n inVersion: Version,\n): boolean;\n\n/**\n * Returns true in case, specified method is supported in a passed version.\n * @param method - method name.\n * @param inVersion - platform version.\n */\nexport function supports(method: MethodName, inVersion: Version): boolean;\n\nexport function supports(\n method: MethodName,\n paramOrVersion: Version | string,\n inVersion?: string,\n): boolean {\n // Method name, parameter, target version.\n if (typeof inVersion === 'string') {\n if (method === 'web_app_open_link') {\n if (paramOrVersion === 'try_instant_view') {\n return versionLessOrEqual('6.4', inVersion);\n }\n if (paramOrVersion === 'try_browser') {\n return versionLessOrEqual('7.6', inVersion);\n }\n }\n\n if (method === 'web_app_set_header_color') {\n if (paramOrVersion === 'color') {\n return versionLessOrEqual('6.9', inVersion);\n }\n }\n\n if (method === 'web_app_close' && paramOrVersion === 'return_back') {\n return versionLessOrEqual('7.6', inVersion);\n }\n\n if (method === 'web_app_setup_main_button' && paramOrVersion === 'has_shine_effect') {\n return versionLessOrEqual('7.10', inVersion);\n }\n }\n\n switch (method) {\n case 'web_app_open_tg_link':\n case 'web_app_open_invoice':\n case 'web_app_setup_back_button':\n case 'web_app_set_background_color':\n case 'web_app_set_header_color':\n case 'web_app_trigger_haptic_feedback':\n return versionLessOrEqual('6.1', paramOrVersion);\n case 'web_app_open_popup':\n return versionLessOrEqual('6.2', paramOrVersion);\n case 'web_app_close_scan_qr_popup':\n case 'web_app_open_scan_qr_popup':\n case 'web_app_read_text_from_clipboard':\n return versionLessOrEqual('6.4', paramOrVersion);\n case 'web_app_switch_inline_query':\n return versionLessOrEqual('6.7', paramOrVersion);\n case 'web_app_invoke_custom_method':\n case 'web_app_request_write_access':\n case 'web_app_request_phone':\n return versionLessOrEqual('6.9', paramOrVersion);\n case 'web_app_setup_settings_button':\n return versionLessOrEqual('6.10', paramOrVersion);\n case 'web_app_biometry_get_info':\n case 'web_app_biometry_open_settings':\n case 'web_app_biometry_request_access':\n case 'web_app_biometry_request_auth':\n case 'web_app_biometry_update_token':\n return versionLessOrEqual('7.2', paramOrVersion);\n case 'web_app_setup_swipe_behavior':\n return versionLessOrEqual('7.7', paramOrVersion);\n case 'web_app_share_to_story':\n return versionLessOrEqual('7.8', paramOrVersion);\n case 'web_app_setup_secondary_button':\n case 'web_app_set_bottom_bar_color':\n return versionLessOrEqual('7.10', paramOrVersion);\n case 'web_app_request_safe_area':\n case 'web_app_request_content_safe_area':\n case 'web_app_request_fullscreen':\n case 'web_app_exit_fullscreen':\n case 'web_app_set_emoji_status':\n case 'web_app_add_to_home_screen':\n case 'web_app_check_home_screen':\n case 'web_app_request_emoji_status_access':\n case 'web_app_check_location':\n case 'web_app_open_location_settings':\n case 'web_app_request_file_download':\n case 'web_app_request_location':\n case 'web_app_send_prepared_message':\n case 'web_app_start_accelerometer':\n case 'web_app_start_device_orientation':\n case 'web_app_start_gyroscope':\n case 'web_app_stop_accelerometer':\n case 'web_app_stop_device_orientation':\n case 'web_app_stop_gyroscope':\n case 'web_app_toggle_orientation_lock':\n return versionLessOrEqual('8.0', paramOrVersion);\n default:\n return [\n 'iframe_ready',\n 'iframe_will_reload',\n 'web_app_close',\n 'web_app_data_send',\n 'web_app_expand',\n 'web_app_open_link',\n 'web_app_ready',\n 'web_app_request_theme',\n 'web_app_request_viewport',\n 'web_app_setup_main_button',\n 'web_app_setup_closing_behavior',\n ].includes(method);\n }\n}\n","import { any, is, looseObject } from 'valibot';\nimport type { Version } from '@telegram-apps/types';\n\nimport { supports } from '@/methods/supports.js';\nimport { type PostEventFn, postEvent } from '@/methods/postEvent.js';\nimport type {\n MethodName,\n MethodNameWithVersionedParams,\n MethodVersionedParams,\n} from '@/methods/types/index.js';\nimport { MethodParameterUnsupportedError, MethodUnsupportedError } from '@/errors.js';\nimport { logger } from '@/logger.js';\n\nexport type OnUnsupportedFn = (\n data: { version: Version } & (\n | { method: MethodName }\n | {\n [M in MethodNameWithVersionedParams]: {\n method: M;\n param: MethodVersionedParams<M>;\n };\n }[MethodNameWithVersionedParams]),\n) => void;\n\nexport type CreatePostEventMode = 'strict' | 'non-strict';\n\n/**\n * Creates a function that checks if the specified method and parameters are supported.\n *\n * If the method or parameters are unsupported, the `onUnsupported` function will be called.\n *\n * If `strict` or `non-strict` value was passed as the second argument, the function\n * will create its own `onUnsupported` function with behavior depending on the value passed.\n *\n * - Passing `strict` will make the function to throw a `MethodParameterUnsupportedError`\n * or a `MethodUnsupportedError` error.\n * - Passing `non-strict` will just warn you about something being unsupported.\n *\n * @param version - Telegram Mini Apps version.\n * @param onUnsupportedOrMode - function or strict mode. Default: `strict`\n */\nexport function createPostEvent(\n version: Version,\n onUnsupportedOrMode?: OnUnsupportedFn | CreatePostEventMode,\n): PostEventFn {\n onUnsupportedOrMode ||= 'strict';\n const onUnsupported: OnUnsupportedFn = typeof onUnsupportedOrMode === 'function'\n ? onUnsupportedOrMode\n : data => {\n const { method, version } = data;\n const error = 'param' in data\n ? new MethodParameterUnsupportedError(method, data.param, version)\n : new MethodUnsupportedError(method, version);\n\n if (onUnsupportedOrMode === 'strict') {\n throw error;\n }\n return logger().forceWarn(error.message);\n };\n\n return ((method: any, params: any) => {\n // Firstly, check if the method is supported.\n if (!supports(method, version)) {\n return onUnsupported({ version, method });\n }\n\n // Method could use parameters, which are supported only in specific versions of Mini Apps.\n // We are validating only those parameters, which are not backward compatible.\n if (\n method === 'web_app_set_header_color'\n && is(looseObject({ color: any() }), params)\n && !supports(method, 'color', version)\n ) {\n return onUnsupported({ version, method, param: 'color' });\n }\n\n return postEvent(method, params);\n }) as PostEventFn;\n}\n","import { encodeBase64Url } from '@/base64-url/encodeBase64Url.js';\n\n/**\n * Creates a safe start parameter value.\n * @param value - value to create start parameter from.\n * @throws {Error} If the value length is too big for the allowed one.\n * @see Learn more about start parameter:\n * https://docs.telegram-mini-apps.com/platform/start-parameter\n */\nexport function createStartParam(value: string): string {\n const b64 = encodeBase64Url(value);\n if (b64.length > 512) {\n throw new Error('Value is too long for start parameter');\n }\n return b64;\n}","import { decodeBase64Url } from '@/base64-url/decodeBase64Url.js';\n\n/**\n * Decodes the start parameter.\n * @see decodeBase64Url\n */\nexport const decodeStartParam = decodeBase64Url;","import { encodeBase64Url } from '@/base64-url/encodeBase64Url.js';\n\n/**\n * @returns True if the passed value is safe to be used to create a start parameter value from it.\n * If true is returned, the value can be safely passed to the `createStartParam` function.\n * @param value - value to check.\n * @see createStartParam\n */\nexport function isSafeToCreateStartParam(value: string): boolean {\n return encodeBase64Url(value).length <= 512;\n}","import { AbortablePromise } from 'better-promises';\n\nimport { captureSameReq } from '@/methods/captureSameReq.js';\nimport type { CustomMethodName, CustomMethodParams } from '@/methods/types/index.js';\nimport { InvokeCustomMethodError } from '@/errors.js';\n\nimport { request, type RequestOptions } from './request.js';\n\nexport type InvokeCustomMethodOptions = Omit<RequestOptions<'custom_method_invoked'>, 'capture'>;\nexport type InvokeCustomMethodFn = typeof invokeCustomMethod;\n\n/**\n * Invokes known custom method. Returns method execution result.\n * @param method - method name.\n * @param params - method parameters.\n * @param requestId - request identifier.\n * @param options - additional options.\n * @throws {InvokeCustomMethodError} Invocation completed with some error.\n */\nexport function invokeCustomMethod<M extends CustomMethodName>(\n method: M,\n params: CustomMethodParams<M>,\n requestId: string,\n options?: InvokeCustomMethodOptions,\n): AbortablePromise<unknown>;\n\n/**\n * Invokes unknown custom method. Returns method execution result.\n * @param method - method name.\n * @param params - method parameters.\n * @param requestId - request identifier.\n * @param options - additional options.\n * @throws {InvokeCustomMethodError} Invocation completed with some error.\n */\nexport function invokeCustomMethod(\n method: string,\n params: object,\n requestId: string,\n options?: InvokeCustomMethodOptions,\n): AbortablePromise<unknown>;\n\nexport function invokeCustomMethod(\n method: string,\n params: object,\n requestId: string,\n options?: InvokeCustomMethodOptions,\n): AbortablePromise<unknown> {\n return request('web_app_invoke_custo